Skip to content

Architecture

For anyone sending a patch. GLIMPSE layering under src/cabinet, enforced by import-linter:

pacts/     contracts: payloads, the Project config, the forge/scm/tasks/agent protocols
specs.py   the numbers only a rule reads
mills/     pure logic: which pull requests, what a task said, what the agent is told
links/     adapters: forge/github, forge/gitlab, scm/git, tasks/mise, agent/claude, config
gates/     the @ritual and @step bodies, reaching everything through pacts
inits/     the one place the adapters are chosen and bound
rituals/   what vekna loads: one module per ritual, re-exporting its steps
layer may import
pacts/ nothing internal
specs.py pacts
mills/ pacts, specs
links/ pacts only; each links/* subpackage independent of the others
gates/ pacts only
inits/ everything
rituals/ the layers; nothing imports it back

The services seam

vekna registers a step when it is decorated and routes by payload class, so nothing constructs a step and nothing can inject into one. Every step reaches its collaborators through services() in pacts/services.py: a single slot filled once by bind(). Importing cabinet.rituals wires it, exactly once, before any facade body runs.

Services.project() reads [cabinet] from .vekna.toml at the current directory; forge() picks GitHub or GitLab from project.forge.

Payloads are the graph

A step returns the next step's payload, or Done(result), and its return annotation names every exit. vekna routes by the payload's exact class, so every step has a class of its own, named for it and adding no fields: SetAside(Work) and Survey(Run) in pacts/heal.py, Leaf(Identifying) in pacts/identify.py. payload.to(NextStep) rebuilds one as another — one to per carrier, taking only that carrier's own steps — and mypy checks every return against the annotation.

Facades

rituals/heal_mine.py, heal_this.py, merge.py, ci.py, answer.py and review.py all wrap the heal steps and each re-export the full step list, so importing any one registers the whole graph. A new heal step is added to all six. A task ritual is heal.this with tasks narrowed to one; tend hands a branch its next task, and every task ends back at tend.

Development

Python 3.11 through 3.15 via mise; mise tasks lists everything.

mise run test:py       # the suite
mise run lint:py       # every linter
mise run fullcheck     # the gate before a commit
mise run site:dev      # the docs site, with live reload
mise run site:build    # the docs site, as the Pages workflow builds it
vekna rituals show heal.mine
  • tests/unit/: mills and pacts, plain pytest, no shell.
  • tests/integration/: vekna's trial fixture walks steps with a scripted shell and an agent double. A test asserts on transitions: trial.walk(step, work.to(Step)) == work.to(Next).

mypy runs fully strict, ruff selects ALL, and tingle counts every suppression as debt against main. Fix the code rather than suppress.

This site

.github/workflows/site.yml builds docs/ on every pull request and publishes it on a push to main. strict: true in mkdocs.yml fails that build on a link to a page that is not there. Pages is sourced from the workflow rather than from a branch, so the custom domain is a repository setting — a CNAME file in docs/ would be copied into the artifact and ignored.