agentleFS
kurofunekurofune/claudit/.agents/skills/wish/SKILL.md

wish

Skill4 starsChanged 7 days agoMITView on GitHub

Interactive orchestrator for working a single Djinn bead end-to-end. Reads the iter formula's step graph via `djinn formula describe <path> --json` and walks it in `needs` topological order, dispatching each step shape to its handler: go-mode bookends (`claim`, `commit-push`) run in-process; single-agent steps (`design`, `implement`, `fix-*`) dispatch to context-isolated subagents; the loop shapes run the iterate-until-pass loop driver (run the body, read its verdict, pass or spawn the paired fix step until budget). This is the interactive mirror of the autonomous `core/iter.formula.toml` run — each step gets the same per-step context isolation the autonomous harness gets for free by spawning a fresh subprocess per step. Triggers on: /wish, work a bead, orchestrate a bead, run the iter formula interactively, drive a bead through the contract.

  • Deletes or force-pushes
  • Commits and pushes

4 other projects use a version of this.

Adopted by

Most recently taken up by

The file

Show its rules and text

We split this skill into the rules it gives an agent

33 rules. 0 are common, 8 are distinctive, and 25 are very specific to this project.

  • 0

    Common

    Most of the 4 edited versions we read keep these.

  • 8

    Distinctive

    This author's own choices. Worth a look.

  • 25

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Before anything else: run djinn skills check --here in the repo you are working in.

    line 11
  2. Distinctive:

    Explicit second argument wins: If the operator passed a second positional argument, it overrides the label scan entirely.

    line 77
  3. Distinctive:

    Only when the embedded tier ALSO reports no such formula do you stop and report the error — its diagnostic lists the variants this binary …

    line 163
  4. Very specific:

    If the djinn binary is not on PATH, build it once (go build -o /tmp/djinn ./cmd/djinn) and invoke /tmp/djinn instead.

    line 277
  5. Very specific:

    NOT hand-author the step list — the JSON from formula describe is the single source of truth so the orchestrator tracks formula edits …

    line 282
  6. Very specific:

    Never run such a step on codex exec instead: that would silently replace the model the operator chose.

    line 295
  7. Very specific:

    NOT re-run the walk and do NOT re-dispatch any step.

    line 325
  8. Very specific:

    The resume scan: When .djinn/state/<bead>/ exists and is non-empty, scan it for the per-step artifacts each step writes, and for each step …

    line 353
  9. Distinctive:

    Stop ONLY on disagreement: After printing the announce, compare the walked formula against Step 1a's shadow resolution.

    line 465
  10. Distinctive:

    No explicit argument: When no explicit formula argument was passed, there is nothing to disagree with: the bead's label-or-default IS the …

    line 476
  11. Distinctive:

    An explicit argument that agrees: When the explicit argument resolves to the SAME formula the bead's label-or-default would have, the …

    line 479
  12. Distinctive:

    When that stop fired, the operator answered by naming the formula, and this step's confirmation does not fire on top of it: treat the …

    line 491
  13. Very specific:

    If the needs graph has a cycle (no step is selectable but steps remain), stop and report the malformed formula rather than guessing an …

    line 510
  14. Very specific:

    Write the same text, from revision: down, to .djinn/state/<bead-id>/<loop-id>.iter-<N>/suite-run, and append the block to that attempt's …

    line 608
  15. Very specific:

    Never force-remove a dirty worktree: Never run git worktree remove --force on a path whose git -C <path> status --porcelain is non-empty — …

    line 644
  16. Very specific:

    Every step after claim runs with <worktree> as its working directory.

    line 654
  17. Very specific:

    Run the whole finish as one command, from <repo-root> (never from <worktree>, which it removes):

    line 660
  18. Very specific:

    Never perform any of its stages by hand, and never close any bead but <bead-id> — not its parent epic, even when it has no other open child.

    line 667
  19. Very specific:

    Commit whatever is left in <worktree>.

    line 676
  20. Very specific:

    No dispatch names the shared checkout or its path as a place to work.

    line 827
  21. Very specific:

    loop-with-review has a terminal-iter rescue: When the final iter's verdict is SHIP-WITH-FIXES with no BLOCKER, the loop passes: file the …

    line 950
  22. Very specific:

    When a loop reaches max_attempts without a pass verdict and without a review rescue, it is exhausted.

    line 969
  23. Very specific:

    Before the loop's first attempt, capture <pre_iter_sha> once (e.g.

    line 986
  24. Very specific:

    When a review loop's check detects a structural fault — a BLOCKER-count mismatch or an invented-rate threshold trip — formula.Decide …

    line 1032
  25. Very specific:

    Only the review loops (loop-with-review) carry this halt.

    line 1060
  26. Distinctive:

    When you read either structural-fault signal, HALT the walk immediately — regardless of attempts remaining — and escalate to the operator: …

    line 1064
  27. Very specific:

    When the walk ran a design loop, append the approved design note after it under ## The approved design note, with reviewDesignRule's text …

    line 1145
  28. Very specific:

    Read model (and effort) off djinn formula describe <path> --json for each step you dispatch — body children AND fix-* children alike — and …

    line 1166
  29. Very specific:

    Run it from <worktree>, outside this session's sandbox (it needs the network, and bd its database).

    line 1193
  30. Very specific:

    The codex-exec rule: When djinn formula describe <path> --json reports adapter: "codex" for a step, that step's declared model and effort …

    line 1207
  31. Distinctive:

    Two prompt shapes — only ONE of them embeds a rubric: Before building anything, decide which shape the step is:

    line 1223
  32. Very specific:

    The argv: Run it from <worktree> with the prompt fed on stdin — the bare trailing - is what makes codex exec read its instructions from …

    line 1288
  33. Very specific:

    Never leave a write step's sandbox off the argv: codex then falls back to ~/.codex/config.toml, read-only on a fresh install, and even …

    line 1335

“Common” is measured: kept by at least 6 in 10 of the 4 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md
<!-- djinn-compat-check: begin -->

Before anything else: run djinn skills check --here in the repo you are working in. If it exits non-zero, stop: relay its output to the user verbatim — it names the fix — and run no other step of this skill. On exit 0, continue.

<!-- djinn-compat-check: end -->

/wish — interactive bead orchestrator

/wish works a single bead end-to-end by driving the same iter formula the autonomous Djinn harness runs (core/iter.formula.toml, the canonical on-disk home of the embedded default), but gives each step the per-step context isolation that autonomous mode gets for free — autonomous mode spawns a fresh subprocess per step; this orchestrator dispatches a fresh subagent per single-agent step so no step's working context crowds the next.

The orchestrator is a walker over a step graph plus a dispatch table. It does not hard-code the contract steps — it reads them from the formula at runtime, so the orchestrator and the autonomous harness can never drift in which steps run or in what order.

Scope of this skill. This body implements the spine (read the graph, order it, dispatch the non-loop shapes — go-mode bookends in-process, single-agent steps to subagents), the iterate-until-pass loop control flow for the loop shapes (§4c: run the loop body, read its on-disk verdict, and either pass, spawn the next attempt dispatching the paired fix-* step, or escalate when the attempt budget is exhausted), the on_exhausted escalation matrix (soft_fail / leave_and_reopen / hard_reset; §4c exhaustion matrix), the --interactive checkpoint flag (Step 1b) — autonomous remains the default when the flag is absent — and the structural-fault halt (ActionHalt, exit code 3; §4c structural-fault halt), a hard mid-budget stop distinct from budget exhaustion. The design single-agent step writes the bead's design note first, and the implement step dispatches djinn-implementer unconditionally once the design-review loop has approved it (§4b).


Step 1 — Read the bead id argument

The first argument is the bead id (e.g. phoenix-abcd). The optional second argument is the formula path or bare variant name (see Step 1a for how it is resolved when omitted). If no bead id was passed, stop and ask for one — the orchestrator works exactly one bead per invocation.

Throughout this body, <bead-id> denotes that argument and <formula-path> the resolved formula path.

The bead worktree. A /wish run never edits the shared checkout. Resolve these once, from wherever you were invoked:

  • <repo-root> — the shared checkout: the path on the first worktree line of git worktree list --porcelain (the main worktree, from any linked one too).
  • <repo-name> — the basename of <repo-root>.
  • <worktree> — <repo-root>/../<repo-name>-wish-<bead-id>, checked out on branch wish/<bead-id>. §4a claim creates or reuses it.

From claim on, <worktree> is the working directory for every command this body runs and every subagent it dispatches, and the ONLY place anything is edited. Every relative path in this body (.djinn/state/<bead-id>/, the codex $OUT dirs) resolves inside <worktree>. <repo-root> appears only in the §4a bookends: the worktree add/remove and the fast-forward of its main.

Step 1a — Resolve the formula path (explicit arg > label > default)

<formula-path> is resolved in this strict precedence order, mirroring the autonomous harness's per-bead dispatch (resolvePerBeadFormula, internal/djinn/session/run_formula.go) so an interactive /wish run and an unattended drain pick the same formula for the same bead:

  1. Explicit second argument wins. If the operator passed a second positional argument, it overrides the label scan entirely. Two accepted forms, disambiguated by shape:

    • A formula path — the argument contains a path separator (/ or \), starts with ., or ends in .toml. Use it verbatim as <formula-path> — e.g. /wish <bead-id> core/iter-codex.formula.toml forces the all-codex variant regardless of labels.
    • A bare variant name — anything else: /wish <bead-id> codex is shorthand for the iter-codex variant. The name is the SHORT variant name (codex, the <name> in iter-<name>.formula.toml and in the formula:<name> label) — NOT the full file stem (iter-codex), which would probe a doubled iter-iter-codex.formula.toml and error.

      Resolve it by the formula:<name> label rule below, in full — all THREE tiers, including the embedded one. /wish <bead-id> codex and a formula:codex label name the same formula, so they must resolve identically: .djinn/iter-<name>.formula.toml → core/iter-<name>.formula.toml → djinn formula describe --embedded iter-<name> --json, stopping and reporting the error only when the EMBEDDED tier also reports no such formula. Do not stop at the two on-disk probes: a consumer repo has no core/ checkout and, since djinn-myttl, no scaffolded .djinn/ copy either, so a two-tier explicit argument would fail in exactly the repos where the equivalent label succeeds — the drift this rule exists to prevent. Which variant names resolve is therefore a property of the binary (its diagnostic lists them), not a list restated here. The label rule's <name> guard applies unchanged: reject a path separator or a leading . (already excluded by the shape test above, restated so the two rules cannot drift).

  2. Otherwise, resolve from the bead's formula:* label. Read the bead's labels and scan for the formula: namespace:

    bd show <bead-id> --json | jq -r '.[0].labels // [] | map(select(startswith("formula:"))) | .[]'
    • Zero formula:* labels → the default production formula, resolved with the same override-else-default intent the harness uses for the embedded default (ResolvePayload, core/payload.go, which falls through from a local .djinn/ override to the embedded default bytes — not an on-disk core/ probe): if .djinn/iter.formula.toml exists (the optional local override) use it; otherwise fall through to core/iter.formula.toml — the canonical on-disk home whose bytes ARE that embedded default, which djinn itself dogfoods (the local .djinn/ default was deleted in djinn-ccyz.45). Because this skill shells out to formula describe, the first two tiers resolve an on-disk PATH, so the concrete .djinn/-then-core/ probe matches resolvePerBeadFormula (internal/djinn/session/run_formula.go) rather than ResolvePayload's embed fallthrough. This is the common case, and in this repo it resolves to core/iter.formula.toml. Probing .djinn/ first then core/ is mandatory — naming the local override path unconditionally would formula describe a file that no longer exists and stop the walk (Step 2's failure posture) for every normally-labeled bead.

      Third tier — the binary, only when NEITHER file exists. A consumer repo has no core/ checkout, and since djinn-myttl djinn init no longer scaffolds .djinn/iter.formula.toml (a scaffolded copy shadowed the binary's embedded default forever, with no resync path back), so in a consumer repo BOTH on-disk probes above miss. Do not stop there — fall through to the default the binary itself embeds:

      djinn formula describe --embedded iter --json

      This tier is LAST, never first: a .djinn/iter.formula.toml override wins over it, and an on-disk core/iter.formula.toml wins over it, exactly as they do for the autonomous harness. Note the shape difference — --embedded takes a formula NAME (iter), not a path, and composes the graph out of the binary's payload FS, so Step 2 consumes this command's JSON directly instead of being handed a <formula-path>. Everywhere below that says "the resolved <formula-path>", read it as "the resolved describe invocation".

    • Exactly one formula:<name> label → the formula:<name> variant, resolved by the same override → canonical probe order as the harness's resolvePerBeadFormula (internal/djinn/session/run_formula.go): probe .djinn/iter-<name>.formula.toml first (the optional local override), and if it does not exist fall through to core/iter-<name>.formula.toml — the canonical home where the variant formulas ship next to the embedded default. For example formula:codex resolves to core/iter-codex.formula.toml in this repo. If neither path exists, take the SAME third tier the default does — djinn formula describe --embedded iter-<name> --json, the binary's own copy — mirroring LoadFormulaVariant's three tiers exactly (.djinn/ → core/ → core.PayloadFS()); a consumer repo carries neither on-disk file, so stopping at tier two would strand every labeled bead there. Only when the embedded tier ALSO reports no such formula do you stop and report the error — its diagnostic lists the variants this binary ships, so an operator typo names itself. Reject a <name> containing a path separator or a leading . — the label is operator-set and formula:../etc/passwd must not resolve outside the formula dirs (this mirrors the harness's ValidateFormulaName guard).
    • Two or more formula:* labels → stop and ask the operator which formula to use. Ambiguous intent is never resolved by guessing; the autonomous harness errors here (ErrAmbiguousFormulaLabel) and the interactive path must not be more permissive.

The resolved <formula-path> — or, at the embedded tier, the resolved describe --embedded <name> invocation — then feeds Step 2 unchanged. Precedence summary: explicit arg (path or bare variant name) > formula:<name> label > default, and within each of the last two the same three tiers in order: .djinn/ override → on-disk core/ canonical → djinn formula describe --embedded <name> (the binary).

<!-- djinn-dispatch: codex -->

On Codex, the default arm resolves the codex variant. Where arm 3 would take the default formula, resolve the bare variant name codex instead, by the same three tiers (.djinn/iter-codex.formula.toml → core/iter-codex.formula.toml → djinn formula describe --embedded iter-codex --json), and record the arm as default. It is the formula a drain runs under the codex provider profile: every step pins a codex model, where the default formula builds on Claude, which a Codex session cannot start.

<!-- djinn-dispatch: end -->

Record what you took, and — when the explicit arm won — what the bead alone would have taken. Carry two facts out of this step for Step 2c to announce: the precedence arm that chose the formula (explicit arg, formula:<name> label, or default) and the probe tier that found it (.djinn/ override, on-disk core/, or embedded). Additionally, when arm 1 (the explicit second argument) won, run arms 2 and 3 anyway — they are pure reads (bd show plus file-existence probes) and cost nothing — and keep the formula the bead's own label-or-default WOULD have resolved to as the shadow resolution. Step 2c compares the two. This resolution never changes the formula the walk uses; arm 1 still wins.

Step 1b — Interactive mode (--interactive checkpoint flag)

/wish is autonomous by default: with no flag it walks the step graph (Step 3), runs the loop control flow (§4c), and applies the on_exhausted escalation matrix end-to-end with NO operator interaction — the same hands-off path the autonomous core/iter.formula.toml run takes and the summoner supervisor (W7) depends on. This is the locked default; --interactive only opts into pauses, it never opts out of autonomy elsewhere.

Detect the flag. Scan the invocation arguments ($ARGUMENTS) for the literal token --interactive. Strip it before reading the positional <bead-id> / <formula-path> (Step 1) so the flag may appear in any position. If --interactive is absent, run fully autonomous — do NOT pause at any checkpoint below; the walk runs straight through to commit-push. This no-pause default is the regression guard: an interactive checkpoint must NEVER fire without the flag.

When --interactive IS present, pause at each checkpoint enumerated below. At a pause: surface the relevant state (the just-produced verdict, the diff, or the escalation), then await an operator decision — continue (proceed to the next step) or abort (stop the walk and leave the bead as-is). The implementer retains full autonomy between checkpoints (locked decision: checkpoints are operator inspect/continue/abort points, not the orchestrator second-guessing the implementer mid-step).

Checkpoint points (interactive mode pauses at each; autonomous mode runs past all of them):

  1. After implement returns — surface the implementer's summary, or its already-done verdict and evidence before the bead is closed (§4b). (Fires at §4b implement.)
  2. After each loop reaches its pass verdict — the review loop (review-loop) and the regression-loop. Surface the pass verdict (SHIP, or a green tree) and the body artifact path so the operator can read the findings before the spine advances. (Fires in the §4c loop driver's pass branch.)
  3. Before commit-push — the full working-tree diff is inspectable. Surface git diff <pre>..HEAD --stat (and offer the full diff on request) so the operator can review exactly what is about to be committed and pushed before the terminal go-mode bookend runs. (Fires at §4a commit-push, before the commit.)
  4. At any on_exhausted escalation — before the §4c exhaustion matrix HALTs, pause so the operator can inspect the failing loop's last verdict and choose continue (accept the escalation and apply its disposition) or abort. This ties into the W4.1 failure-handling branch — the operator gets a say before the soft_fail / leave_and_reopen / hard_reset disposition is applied.

A continue resumes the walk at the next step; an abort stops immediately and reports the bead's current state. Aborting does NOT reopen or reset beyond what the current step already did — it is a clean operator-requested stop, not a rollback. Concretely, after an abort the bead stays in_progress, any commits landed by earlier checkpoints are retained on disk, and uncommitted working-tree changes are left as-is in <worktree> — the operator resumes or cleans up by hand.

Step 2 — Obtain the step graph

Shell out to the djinn binary to describe the formula as JSON:

djinn formula describe <formula-path> --json

This emits a steps[] array. Each step object carries:

  • id — the step id (e.g. claim, implement, review-loop).
  • type — one of the shapes in core/SHAPE-REGISTRY.md (the dispatch key — see Step 4).
  • title — a human label (may contain runtime placeholders; ignore for dispatch).
  • needs[] — the ids this step depends on. This is the edge set the walker orders by.
  • loop — null for non-loop steps; otherwise {max_attempts, check_function, on_exhausted}. Present only on the loop shapes.
  • children[] — the loop body steps (the review and fix children), present only on loop shapes. The spine does not descend into children at the top level — the loop driver (§4c) descends into them per loop step.

If the djinn binary is not on PATH, build it once (go build -o /tmp/djinn ./cmd/djinn) and invoke /tmp/djinn instead. If formula describe exits non-zero or emits JSON that does not parse, stop and report the failure to the operator rather than proceeding on a partial or guessed graph (the same stop-and-report posture as the cycle check in Step 3). Do NOT hand-author the step list — the JSON from formula describe is the single source of truth so the orchestrator tracks formula edits automatically.

<!-- djinn-dispatch: codex -->

Check that every declared provider can start, before claiming. When any step or loop-body child in the JSON resolves to adapter: "claude-code" (§4c's claude-p rule runs it on claude -p), run command -v claude. When it prints nothing, stop here — before claim touches the bead — and print:

stop: <step-id> declares adapter=claude-code, and this Codex session cannot start it: claude is not on PATH. Install Claude Code, run /wish from a Claude session, or pick a codex formula: /wish <bead-id> codex

Never run such a step on codex exec instead: that would silently replace the model the operator chose.

<!-- djinn-dispatch: end -->

Step 2b — Resume preamble (re-invocation resumes at the first incomplete step)

/wish is resume-safe by re-invocation: when a prior run was interrupted partway through a bead (session ended, context lost, crash) and the operator re-invokes /wish <bead-id>, this preamble runs BEFORE the Step 3 step walk and resumes at the first incomplete step rather than redoing completed work. There is no resume flag — re-invocation auto-detects existing in-progress on-disk state. (Locked decision: resume is a re-invocation behavior, not a new flag.)

Autonomous parity. The autonomous harness already resumes — internal/djinn/session/resume.go applies crash-recovery resume anchors so the SECOND binary invocation resumes at the anchored iter, not iter 1 (closing the phoenix-ccyz.18 "crash kill mid-iter-3 followed by Djinn restart resumes at iter 3, not iter 1" gap). Interactive /wish mirrors that behavior, but by a different mechanism: the Go harness reads an in-process ResumeAnchors map (s.Cfg.ResumeAnchors, threaded from Sweep); this skill is prose, not the Go binary, so it has no ResumeAnchors map. The interactive mechanism is on-disk-state inspection — it scans the same forensic artifacts the loop execution (§4c/§4d) already wrote, NOT the Go ResumeAnchors map. Reading the on-disk state is the interactive realization of the autonomous anchor lookup, exactly as §4c reads the on-disk verdict artifact instead of re-running the Go check.

Idempotency guard (already-closed = finish, then stop). First, run bd show <bead-id> and read its status. If the bead is already closed, do NOT re-run the walk and do NOT re-dispatch any step. When wish/<bead-id> or <worktree> still exists, an earlier commit-push was interrupted: run cd "<repo-root>" && djinn wish land <bead-id>, which finishes the remaining stages (§4a commit-push). Then report that the bead is complete and stop.

Fresh-run guard (empty/absent state = clean start). Next, check <worktree>/.djinn/state/<bead>/ — run state lives inside the bead worktree, so an absent <worktree> means absent state. (A wish/<bead-id> worktree at any path other than <worktree> halts here, as §4a claim does.) When that directory is empty or absent, there is no prior run to resume from: start the walk at the first step in topo order — identical to a clean run (clean-run parity; this is the regression guard that an absent state dir must never skip a step). Only a non-empty .djinn/state/<bead>/ triggers the resume scan below.

Unlanded work = resumed run. Classify the run fresh only when the state dir is empty or absent AND wish/<bead-id> holds no commit missing from main:

git -C "<repo-root>" show-ref --verify --quiet refs/heads/wish/<bead-id> &&
  git -C "<repo-root>" rev-list --count main..wish/<bead-id>   # > 0 → resumed

An implementer can commit and crash before any artifact is written, so a missing state dir alone never proves a fresh run. Any other case is a resumed run: the §4b already-done check before design is skipped, the §4b implement dispatch omits the already-done exit, and the walk continues through review and commit-push, which lands those commits on main.

The resume scan. When .djinn/state/<bead>/ exists and is non-empty, scan it for the per-step artifacts each step writes, and for each step in the Step 3 topo order decide whether it is DONE. Resume at the first not-DONE step in topo order; every step before it is treated as already complete and is not re-run.

Per-shape DONE detection (mirrors the §4 dispatch shapes):

  • go-mode claim → DONE when bd show <bead-id> reports the bead in_progress AND <worktree> exists on branch wish/<bead-id> (the claim's effects are the bd state change and the worktree, so they ARE the evidence). Otherwise resume at claim; it is idempotent (§4a).
  • loop-with-review step → DONE when a <step>-iter-<N>.json with a pass verdict is present. On disk this is the §4d dir-per-iter layout: scan the .djinn/state/<bead>/<loop-id>.iter-<N>/ iter directories, read the body step's verdict artifact inside (the file named by the body step id, no .json suffix — §4d), and treat the loop as DONE when the highest recorded iter-<N> holds the pass token (SHIP, per the §4c body→verdict map). (The bead's <step>-iter-<N>.json framing names the per-iter verdict record; §4d is its canonical on-disk realization — see §4d, do not duplicate the layout here.)
  • loop-with-regression step → never DONE on resume: it writes no verdict artifact, so re-run it (a green tree costs one test + lint run and no agent).
  • keep-loop step → DONE when any step after it in topo order is DONE: it writes no verdict artifact, and its kept rounds are already on the branch. Otherwise re-run it from round 1 with the current HEAD as the last kept commit.
  • single-agent step (implement / fix-*) → DONE when its per-iter state artifact is present under .djinn/state/<bead>/. The implement step's artifact present means authoring already ran; resume past it. (The fix-* children write no structured artifact — §4d — so their done-ness is subsumed by the enclosing loop step's verdict scan above.)

Loop resume granularity (highest recorded iter, re-run only the unfinished one). A loop step that is not DONE may still be partway through its budget — earlier iterations passed their body→fix→re-check cycle but the loop has not yet reached a pass verdict. Do NOT restart that loop at iter 1. Instead resume at the HIGHEST recorded iter-<N> for that loop — comparing N numerically, not lexicographically, so iter-10 ranks above iter-9 (a string compare would wrongly pick iter-9, re-running a completed iteration on any 10+-iteration rework bead) — and re-run only that unfinished iteration, continuing the §4c cycle from there with N set to that highest recorded index. This mirrors the autonomous anchored-iter resume (internal/djinn/session/resume.go sets s.iter = anchor.IterIndex rather than restarting at iter 1) — the interactive loop driver resumes at the highest on-disk iter-<N> rather than re-running iterations that already completed.

Once the resume target is identified, hand control to the Step 3 walk starting at that step (for a partway loop, at that loop with N pre-positioned to the highest recorded iter). Steps before the resume target are skipped as already-DONE; the walk proceeds normally from the resume point through commit-push.

Step 2c — Announce the resolved formula, its knobs, and any disagreement

Before Step 3 dispatches ANY step, print a resolution announce. Front-door, never a closing summary: a run summary naming the formula that was walked arrives after the spend has already happened, which is exactly how four beads on 2026-09-07 were walked on the default iter with fable-pinned steps hand-substituted, while the variant encoding that contingency already shipped. The announce is cheap, and on the ordinary path it is non-blocking.

What the announce must name.

  1. The resolved formula — the <formula-path> Step 1a produced, or, at the embedded tier, the describe --embedded <name> invocation.
  2. The precedence arm that chose it — exactly one of Step 1a's three arms, named with that arm's literal name: explicit arg, formula:<name> label, or default.
  3. Where the formula came from — exactly one provenance value. For a resolution that went through Step 1a's probe sequence — the default arm, the formula:<name> label arm, and the bare-variant-name form of the explicit arm — name the probe tier that found it: exactly one of Step 1a's three tiers, named with that tier's literal name: .djinn/ override, on-disk core/, or embedded. A formula path the operator supplied directly (Step 1a's explicit-arg path form) is used verbatim and is probed by no tier at all — /wish <bead-id> /tmp/custom.formula.toml is a supported resolution that came from none of the three — so it has no tier to name: print the fourth literal provenance value supplied path instead. Never force a directly supplied path into one of the three probe tiers — an announce naming a tier the resolution never visited is a false announce, and the tier names are what an operator reads to tell a local override from the binary's own copy.
  4. A per-step knob table — one row per step the walk will dispatch, with the four columns step id, adapter, model and effort: the knobs this run will ACTUALLY spend on. Include loop-body children and fix-* children, not just top-level steps — they are where the model pins live. Print — for a field the step does not declare.

Read the table off the graph you already have. The four knob fields come from the Step 2 djinn formula describe <formula-path> --json output — top-level steps at .steps[], loop-body children at .steps[].children[], the same records the §4 dispatch knob rule reads. do NOT invoke formula describe a second time: a second invocation can resolve differently from the graph the walk dispatches from, and an announce naming knobs that are not the knobs used is worse than no announce at all.

Announce shape (illustrative — the values come from the JSON, never typed):

formula: core/iter-codex.formula.toml
  arm:  formula:<name> label      tier: on-disk core/
  step id             adapter  model         effort
  claim               —        —             —
  design              codex    gpt-6-sol     high
  design-review-loop  —        —             —
  revise-design       codex    gpt-6-sol     high
  design-review       codex    gpt-6-astra   high
  implement           codex    gpt-6-sol     high
  review-loop         —        —             —

The announce names design and design-review whenever the formula declares them — the default and both variants do — so the operator sees, before any spend, that the bead's design note is written and checked before code.

Stop ONLY on disagreement. After printing the announce, compare the walked formula against Step 1a's shadow resolution. When an explicit formula argument was passed AND the bead's own label-or-default would have resolved to a DIFFERENT formula, name BOTH formulas and STOP for operator confirmation before dispatching the first step — an explicit argument must never outrank the bead silently. That case is operator-typed by construction, so a human is present to answer.

Everything else proceeds. Both non-disagreement cases proceed with no pause — print the announce and walk on:

  • No explicit argument. When no explicit formula argument was passed, there is nothing to disagree with: the bead's label-or-default IS the resolution. Proceed with no pause.
  • An explicit argument that agrees. When the explicit argument resolves to the SAME formula the bead's label-or-default would have, the operator restated what the bead already said. Proceed with no pause.

This narrowness is load-bearing. Step 1b locks /wish as autonomous by default with NO operator interaction, and unattended use (the summoner supervisor, W7) rests on that default. An unconditional confirmation prompt here would break it. The disagreement stop is the only pause Step 2c may introduce, and --interactive remains the only way to opt into the Step 1b checkpoints.

One stop, not two. Step 1a already stops on two or more formula:* labels. When that stop fired, the operator answered by naming the formula, and this step's confirmation does not fire on top of it: treat the answered ambiguity as the bead's own resolution and proceed with no pause — one stop, not two consecutive prompts. The same holds in reverse: this step's disagreement confirmation never re-opens the label ambiguity Step 1a already settled.

Step 3 — Order the steps by needs (topological walk)

Walk the top-level steps[] in dependency order: a step runs only after every id in its needs[] has run. Concretely, repeatedly select the next step whose needs are all already-completed; ties (two independent steps both ready) keep their original steps[] declaration order — not the order of the shrinking ready-frontier. The production core/iter.formula.toml is a linear chain (claim → design → design-review-loop → implement → review-loop → regression-loop → commit-push), so the topological order is the declared order — but order by needs, not by array position, so a future non-linear formula still walks correctly. Never hardcode this chain: a consumer formula may insert, drop or reorder steps, and the shipped variants patch knobs the describe output carries — always walk the graph djinn formula describe returns.

If the needs graph has a cycle (no step is selectable but steps remain), stop and report the malformed formula rather than guessing an order.

Step 4 — Per-shape dispatch table

For each step in topological order, dispatch on its type. The authoritative shape→handler mapping is core/SHAPE-REGISTRY.md; this table is its orchestrator-side realization.

type (shape)HandlerRuns
go-mode-stepin-process bd/git bookend (see §4a)in this context
single-agent-stepsubagent dispatch — design → a design subagent; implement → djinn-implementer; fix-* → djinn-fixer (see §4b)isolated subagent
loop-with-design-reviewloop driver — body via ONE design reviewer on the other model family, after a revise-design rewrite on an iteration that follows a REVISE; you post each note and verdict to the bead (see §4c loop-with-design-review)driver + isolated body
loop-with-reviewloop driver — body via the single-lens reviewer (see §4c)driver + isolated body
loop-with-regressionloop driver with NO procedure body — you run the tree yourself, then dispatch fix-regression to djinn-fixer only when it is broken (see §4c)driver + isolated fix
keep-loopkeep-loop driver — dispatch the body children each round, run the decide, then KEEP the round's commits or RESET the branch to the last kept commit, until a stop verdict or max_attempts (see §4c keep-loop)driver + isolated body

A step whose type matches none of the rows is a contract violation (the parity test TestFormulaStepsAllHaveKnownShapes would have caught it upstream) — stop and report the unknown shape rather than skipping it silently.

<!-- djinn-dispatch: codex -->

Starting a dispatch. Codex has no in-session agent call this walk can wait on, so every dispatch is its own headless run: a fresh process with its own context, the same isolation the autonomous harness gets. A step whose resolved adapter is claude-code runs on claude -p by the claude-p rule in §4c; every other step is a codex exec run built by the codex-exec rule in §4c (the prompt on stdin, the run's files under $OUT, the step's resolved model / effort on the argv). The implementer and fixer are the Codex agents djinn init writes, .codex/agents/djinn-implementer.toml and .codex/agents/djinn-fixer.toml; in a repo carrying no .codex/agents/, they are the user-scope copies djinn skills install --global writes, ~/.codex/agents/<name>.toml. A write step's prompt opens with the line naming its agent file, <agent file> being whichever of the two exists, the repo's first:

Act as the <name> agent: read <agent file> and follow its developer_instructions as your own.
codex exec --skip-git-repo-check -C "<worktree>" \
  -m "<resolved model>" -c model_reasoning_effort="<resolved effort>" \
  -s danger-full-access -c approval_policy=never \
  --json -o "$OUT/last-message.md" - \
  < "$OUT/prompt.txt" > "$OUT/rollout.jsonl" 2> "$OUT/stderr.log"

The agent's summary is $OUT/last-message.md. That argv is a write step's; its sandbox flags are §4c's write-step rule. A reviewer takes no agent file: its prompt embeds the review rubric instead and its argv passes -s read-only (§4c). codex exec needs the network and bd its database, so run it outside this session's sandbox (approve the escalation).

<!-- djinn-dispatch: end -->

§4a — go-mode-step (in-process bookends)

go-mode-step bodies bypass the LLM and run in this orchestrator's context. The two top-level go-mode steps in the iter formula, claim and commit-push, are the bookends of the run. go-mode-step writes no per-iter LLM artifact — its output is the side effect (the bd state change, the commit). The file_nits go-mode steps (file-nits-impl, file-nits-tests) are loop children, not top-level steps, so the spine never reaches them directly — they run inside the W3.2 loop driver. mutation-check is a go-mode review-loop child too, and /wish skips it: an interactive run wires no mutation runner, so the review prompt carries no ## Mutation check report and the reviewer sweeps rubric 7 from the diff alone.

suite-run is the other go-mode review-loop child, and /wish does not skip it: the reviewer may run in a sandbox where part of the suite cannot run, so it reads the full-suite result from you. Before each review dispatch, commit the worktree's tree, then run the module's full test command in this orchestrator's own context, in <worktree> — go test -short ./... from the Go module root, or the repo's djinn.test_argv when it sets one — and build the ## Test suite block from that run:

## Test suite

The harness ran the module's full test command on this iteration's tree
before you, outside your sandbox. This is the full-suite result; cite it:

```
revision: <git rev-parse HEAD in <worktree>, run before the tests>
command: <the exact command>
exit: <its exit code>
failing tests: <count>
- <package> --- FAIL: <TestName>     (one line per `--- FAIL:` line)

--- gate output (tail) ---
<the last 200 lines of its output>
```

Copy its revision and exit into the sidecar as
`"suite": {"revision": "<revision>", "exit": <exit>}`.

Write the same text, from revision: down, to .djinn/state/<bead-id>/<loop-id>.iter-<N>/suite-run, and append the block to that attempt's review dispatch prompt. A failing suite is still a result: the reviewer grades it. When the command cannot run at all (the binary is missing, it hangs past its timeout), do not dispatch the reviewer; stop the loop and report it. The sidecar's suite must match the block's revision and exit, as the autonomous review_approved check requires: a sidecar whose suite is missing or differs is a review-invalid result — re-run the reviewer, do not spawn the fixer.

claim

Open (or reuse) the bead worktree, then claim the bead in-process:

want="$(cd "<repo-root>/.." && pwd -P)/<repo-name>-wish-<bead-id>"
# The path of the porcelain record that holds wish/<bead-id>, if any.
have=$(git -C "<repo-root>" worktree list --porcelain | awk -v b="branch refs/heads/wish/<bead-id>" \
  '/^worktree /{p=substr($0,10)} $0==b{print p}')
if [ -n "$have" ] && [ "$have" != "$want" ]; then
  echo "HALT: wish/<bead-id> is checked out at $have, not $want" >&2; exit 1
elif [ -n "$have" ]; then
  echo "reusing existing worktree $have (wish/<bead-id>) from an earlier run"
elif git -C "<repo-root>" show-ref --verify --quiet refs/heads/wish/<bead-id>; then
  git worktree add "$want" wish/<bead-id>    # branch kept, worktree gone
else
  git worktree add "$want" -b wish/<bead-id> main
fi
bd update <bead-id> --claim --status=in_progress
  • Reuse, and say so. Reuse is keyed on the path AND the branch of one porcelain record. When wish/<bead-id> is already checked out at <worktree> (a previous run crashed), reuse it, tell the operator, and keep its commits and uncommitted changes. When it is checked out at any other path, halt and name that path. When the branch exists with no worktree, attach it without -b.
  • Never force-remove a dirty worktree. Never run git worktree remove --force on a path whose git -C <path> status --porcelain is non-empty — not to recreate it, not to clean up. A worktree in the way that you cannot reuse is an escalation: name its path and stop.
  • The name <repo-name>-wish-<bead-id> on wish/* stays clear of the summoner's summoner-<bead-id> worktrees on summoner/work/*, which its startup reap removes.
  • The bead MUST be in_progress before any implementation step runs (the cardinal beads rule). This mirrors the autonomous djinn.bead_claim StepFunc.

Every step after claim runs with <worktree> as its working directory. Every HALT or escalation below leaves <worktree> and wish/<bead-id> in place and names the worktree path.

commit-push

Run the whole finish as one command, from <repo-root> (never from <worktree>, which it removes):

cd "<repo-root>" && djinn wish land <bead-id>

Never perform any of its stages by hand, and never close any bead but <bead-id> — not its parent epic, even when it has no other open child. djinn wish land (cmd/djinn/wish_land.go) runs, in order:

  1. A refusal check: the latest review sidecar under <worktree>/.djinn/state/<bead-id>/ must approve the bead (SHIP, or SHIP-WITH-FIXES whose every actionable IMPORTANT finding is fixed or filed as a review-follow-up bead discovered from <bead-id>), <repo-root> must be on main, and main must still fast-forward to wish/<bead-id>.
  2. Commit whatever is left in <worktree>.
  3. The repo's test command inside <worktree>: djinn.test_argv from .djinn/config.json, else go test -short ./....
  4. git -C "<repo-root>" merge --ff-only wish/<bead-id>.
  5. The §6 evidence comment and the four evidence_* labels, transcribed from that sidecar with the mapping the autonomous djinn.commit_push StepFunc uses (ReviewEvidence, internal/djinn/session/formula_evidence.go).
  6. git push of main, bd close <bead-id>, then bd dolt commit and bd dolt push — a code-only push leaves the bead's closed status and labels invisible to collaborators (core/bead-work-contract.md §8).
  7. git worktree remove "<worktree>" (never --force), then git branch -d wish/<bead-id>.

Each stage first reads whether it already happened, so a rerun after an interruption anywhere finishes the rest without a second evidence comment or a second close. On a non-zero exit, relay its message to the operator and stop:

  • Red tree, or a review that did not approve: nothing landed; the bead stays in_progress and the worktree is kept. Escalate.
  • Fast-forward refused (main moved on): the bead, worktree and branch are untouched. The message names the worktree and the rebase — git -C <worktree> rebase main, then re-run /wish <bead-id> (it resumes at regression-loop, which re-tests the rebased tree, then commit-push).
  • Anything later (a push, bd, or a dirty worktree at teardown): fix the cause it names, then re-run djinn wish land <bead-id>.

Interactive checkpoint (Step 1b #3): in --interactive mode, pause BEFORE commit-push and surface git diff <pre>..HEAD --stat so the operator can review the working-tree diff before it is committed and pushed. Autonomous mode commits and pushes without pausing.

§4b — single-agent-step (subagent dispatch)

single-agent-step bodies dispatch to a context-isolated subagent so the step's work does not crowd the orchestrator's context. Route by step id:

  • design → ONE context-isolated design subagent under the worktree dispatch preamble (below), on the step's resolved adapter, model and effort (a codex step is a codex exec write-step run, §4c). It runs right after the claim go-mode bookend (§4a) — the step order is claim → design → design-review-loop → implement — so nothing is written before the bead is claimed, and no code before its design is approved.

    The already-done check comes first (fresh run only). On a fresh run (Step 2b, as for implement's exit below), before any design subagent, dispatch ONE read-only subagent on the design step's resolved adapter, model and effort with the prompt buildAlreadyDoneCheckPrompt renders (internal/djinn/session/design_step.go): the bead block and one question — does every acceptance criterion already hold at HEAD? It returns already-done with the file:line or command output proving each criterion, or not done with the first unmet one. On already-done, take implement's close-and-skip below: no design, design review, implement or review subagent is ever dispatched, so an already-done bead pays for no design. On not done, or on a resumed run (no check), dispatch the design subagent.

    Build the design prompt in the shape buildDesignPrompt renders (internal/djinn/session/design_step.go): the header line, the bead block, ## Step contract (read the bead, the parent epic, DECISIONS.md and the code; write no file and no commit; end the reply with the note), the five-section note contract, the parent epic's description (bd show <parent> --json), ## Beads whose touches overlap (every open or in-progress bead whose metadata.touches shares a path with this bead's, by the summoner's overlap rule: equal paths, or a path under one that ends in /), and ## What this step does NOT do.

    The subagent's final reply IS the design note. Save it verbatim to .djinn/state/<bead-id>/design.iter-1/design and post it to the bead with bd comments add <bead-id> -f <tmpfile>, the body opening with the line djinn design note — round 1 (design) — the same comment the drain posts. Then check the tree: git status --porcelain empty and HEAD unmoved since claim; if the subagent wrote anything, git reset --hard <claim HEAD> and git clean -fd outside .djinn/ — the drain's review tree guard does the same. A note or verdict that bd comments add refuses stops the walk before implement, as a drain stops: fix the cause, post it, then continue.

  • implement → the djinn-implementer subagent. This is the orchestrator's first code-writing step: it runs after the design-review-loop has approved the design note — so the bead is claimed and its design approved before any code is written. Append the approved note (the note in force when the design loop passed, §4c loop-with-design-review) to the dispatch under ## The approved design note, with the rule implementDesignRule states (internal/djinn/session/design_step.go): build to the note; the criteria win where they disagree; any departure carries a Design departure: <what changed> — <why> line in the body of the commit that departs. Dispatch the implement step unconditionally to djinn-implementer as a subagent, passing the claimed <bead-id> under the worktree dispatch preamble (below). The implementer reads that bead's acceptance_criteria / design / notes and builds the complete feature to every criterion per core/bead-work-contract.md §3 (committing incrementally, honoring backend TDD), then returns a compact summary of what it built. The orchestrator does not implement in its own context — the §3 authoring work runs in the implementer's isolated context window.

    The already-done exit (first claim only). On a fresh run (Step 2b found no prior state AND no wish/<bead-id> commit missing from main), add to the dispatch: "If every acceptance criterion already holds at HEAD, commit nothing and return already-done with the file:line or command output proving each criterion." On a resumed run, omit it — the bead's own earlier commits would read as done. When the implementer returns already-done, take the close-and-skip disposition, the mirror of the autonomous ErrFreshnessStale route (the 2026-09-26 lean-formula ruling): bd close <bead-id> with the implementer's evidence as the reason, skip every later step, run bd dolt commit -m "<bead-id>: already done" && bd dolt push, tear the worktree down with §4a commit-push's stage 7 commands, and end the walk. It is a successful finish — never an on_exhausted disposition, never a reopen.

    Interactive checkpoint (Step 1b #1): in --interactive mode, pause after implement returns and surface its summary — or its already-done evidence, before the close. An already-done check that returns already-done pauses the same way. Autonomous mode does not pause.

    Implementer-absent degraded note (not the primary path). The unconditional dispatch above is the primary instruction. Only if no agent named djinn-implementer is resolvable in the current adapter — a misconfigured install, not the normal case — fall back to an explicit manual-implementation checkpoint: tell the operator to implement the bead by hand per core/bead-work-contract.md §3, then resume the walk at the next step once implementation is committed. This degraded note exists only to keep a broken install from hanging; it is not the W3.1 graceful-degradation default, which W5.2 replaced.

  • revise-design → ONE design subagent like design's, dispatched by the §4c loop-with-design-review driver, never at the top level.
  • fix-* (fix-review, fix-regression) → dispatch to djinn-fixer as a subagent under the worktree dispatch preamble, passing the <bead-id>, the findings from the paired review (or the failing tree output for fix-regression), and a fix_threshold. These steps are loop children, so they are dispatched by the W3.2 loop driver — the spine lists them here for completeness of the dispatch table, but does not invoke them at the top level.
<!-- djinn-dispatch: codex -->

The Codex agent files pin no model: the step's resolved model and effort ride the codex exec argv (the claude -p argv for a claude-code step), per the §4c knob pass-through rule.

<!-- djinn-dispatch: end -->

Worktree dispatch preamble

Open EVERY dispatch — implementer, fixer, the review reviewer — with this block, <worktree> and <bead-id> filled in, and start the dispatched agent in <worktree>:

WORKING DIRECTORY: <worktree> — a git worktree on branch wish/<bead-id>.
It is the ONLY place you may edit files. cd into it for every command.
Do not edit any file outside it. Commit on wish/<bead-id>; never push, pull,
or create or switch branches.

No dispatch names the shared checkout or its path as a place to work. A codex step gets the same scope through -C "<worktree>" (§4c).

§4c — loop shapes (the iterate-until-pass loop driver)

The three walked loop shapes (loop-with-design-review, loop-with-review, loop-with-regression) each drive an iterate-until-pass cycle; loop-with-design-review has its own subsection below. The third loop shape, keep-loop, has no pass token and no exhaustion; it has its own driver in the keep-loop subsection below, and nothing else in §4c applies to it. loop-with-regression is the one whose body is not a procedure — you run the check yourself; see its subsection below. They reuse the spine's walker and dispatch table — the loop driver is what descends into a loop step's children[]. This is the interactive mirror of the autonomous harness's loop dispatch (internal/djinn/session/run_formula.go plus checks.ReviewApproved / checks.RegressionClean): the harness runs an in-process Go check that parses the body's sidecar and returns pass/spawn/halt; this skill has no in-process check (it is prose, not the Go binary), so it reads the SAME on-disk verdict artifact the body skill already wrote. Reading the artifact — not reimplementing the Go parser — is the interactive equivalent.

The loop algorithm (identical across the loop shapes)

For a loop step, first read its control fields from the formula describe --json output:

  • loop.max_attempts — the attempt budget (e.g. review-loop = 3, regression-loop = 2). The cycle runs the body→verdict→branch sequence at most this many times.
  • loop.check_function — djinn.check.review_approved / djinn.check.regression_clean. This is the cross-check that confirms which shape you are in; it is not something you run — it names the autonomous check whose verdict logic you mirror by reading the artifact.

Then identify the body children from the step's children[] array (the describe JSON does not emit the check's review_step / fix_threshold args, so derive them from the children themselves):

  • body child — the single-agent-step child whose id does NOT start with fix-: review. Its id selects the handler (the body→handler map below). loop-with-regression has none.
  • fix-* child — the single-agent-step child whose id starts with fix-: fix-review, or fix-regression.
  • Run the two in the order the children[] array declares them, honouring their needs[] — do NOT assume body-then-fix. On a loop-with-review the order is INVERTED and the inversion is the point (djinn-np4m4.3): the fix child runs FIRST, acting on the PRIOR attempt's review sidecar (so it does nothing on attempt 1), and the review child runs LAST, so the attempt ends with the artifact you read the verdict from. Reading a review that a fix has since edited out from under is what deferred djinn-iang4.3.
  • mutation-check child (loop-with-review only) — a go-mode-step child; skip it (§4a).
  • suite-run child (loop-with-review only) — a go-mode-step child; run it yourself before each review dispatch and hand the reviewer the ## Test suite block (§4a).
  • file-nits-* child (loop-with-review only, file-nits) — a go-mode-step child; the per-iter NIT-filing dispatch. The reviewer is a bare single-lens subagent that files nothing, so you, the orchestrator, own this child's work. After reading the reviewer's sidecar, for each promoted finding: file every at-or-above-floor kind=fix NIT as a review-follow-up bead labelled proposed (it has not passed /create-bead, so drains skip it until the lead authors it), and append every kind=counter finding to the loop's bead as a COUNTER: <file:line> <action> note. Skipping this is a silent parity loss against the autonomous run, which performs both mechanically (internal/djinn/beads/filer.go — FileNitFollowUps and AppendCounterNote / CounterNotePrefix).

    Honor the same three filters the autonomous filer applies: the priority floor (default P2, operator ruling D1 — a finding whose graded priority is numerically greater than the floor, e.g. a P3 or P4, or one left ungraded (it falls back to the batch default P3), stays in the sidecar for human reading and does not become a bead; only a P2-graded NIT files), kind (never file a counter; never file BLOCKER/IMPORTANT — those route to fix-review), and structural dedup (same file:line under the same parent → skip, because the loop may re-run the body on a later attempt). File with these commands:

    # Dedup pre-check: what is already filed against this bead?
    bd list --label=review-follow-up --status=open,closed --json \
      | jq -r --arg b "<bead_id>" '.[] | select(any(.dependencies[]?; .type == "discovered-from" and .depends_on_id == $b)) | .title'
    
    # File one at-or-above-floor kind=fix NIT at its graded priority.
    bd create --type=task --priority="<finding's priority grade, e.g. P2>" --labels=review-follow-up,proposed \
      --title="<action>" \
      --description="$(printf '**Finding:** %s\n**Location:** %s' "<action>" "<file:line>")"
    bd dep add <new-bead-id> <bead_id> --type=discovered-from
    
    # Append one kind=counter finding as a note on the loop's bead.
    bd update <bead_id> --notes="COUNTER: <file:line> <action>"

Now run the cycle. For attempt N from 1 to max_attempts:

  1. Dispatch the body child to its handler (per the map below). On N == 1 the fix-* child is a no-op (no prior feedback exists yet); on N > 1 you have already dispatched the fix below before re-entering here.
  2. Read the body's on-disk verdict artifact at .djinn/state/<bead-id>/<loop-id>.iter-<N>/<body-step-id> (the dir-per-iter canonical layout — see §4d). Extract the verdict token (the body→verdict map below names WHERE in the artifact and the pass token).
  3. Branch on the verdict — this is the pass-vs-spawn-next decision, mirroring formula.Decide (internal/djinn/formula/dispatch.go):
    • structural-fault halt (evaluated first, mirroring formula.Decide's HaltExitCode/ActionHalt check ahead of the budget and pass checks) → if the body artifact carries a structural-fault signal on a loop-with-review step (BLOCKER-count mismatch or invented-rate trip — see §4c), HALT the walk immediately regardless of N/remaining budget: never route it through spawn-next or terminal-iter/exhaustion. The detection logic and thresholds live in §4c and internal/djinn/formula/checks/review_approved.go — do not restate them here.
    • pass (verdict == the shape's pass token) → the loop is satisfied; exit the loop and advance the spine to the next step. Interactive checkpoint (Step 1b #2): in --interactive mode, pause here and surface the pass verdict
      • body artifact path before advancing; autonomous mode advances without pausing.
    • spawn-next (verdict is a non-pass, non-halt value AND N < max_attempts) → dispatch the paired fix-* child via djinn-fixer (passing the body's findings + the fix_threshold, see §4c severity filter), then increment N and loop back to step 1 to re-run the body and re-read its verdict.
    • terminal-iter (N == max_attempts AND still non-pass) → first apply the shape's rescue rule; if the iter is not rescued, the loop is exhausted and you escalate via its on_exhausted mode (the §4c exhaustion matrix immediately below). The rescue rule is shape-dependent, mirroring the autonomous checks exactly:
      • loop-with-review has a terminal-iter rescue. When the final iter's verdict is SHIP-WITH-FIXES with no BLOCKER, the loop passes: file the remaining actionable IMPORTANT/NIT findings as review-follow-up beads (you file them, per the file-nits-* child rule above) and advance the spine — do not escalate. This mirrors tryTerminalIterRescue (internal/djinn/formula/checks/review_approved.go), which files the residue and returns exit 0. A BLOCKER on the final iter is not rescued (the "BLOCKER means do not ship" gate fires before the rescue), nor is a REWORK verdict — both fall through to the exhaustion matrix below.
      • loop-with-regression has NO rescue. A tree still broken on the final iter falls through to the exhaustion matrix.

A verdict the body emits that is none of the recognized tokens (a malformed or missing artifact) is treated as a non-pass for the branch above — iterate if budget remains, and on the final iter apply the rescue rule (loop-with-review) or fall through to the exhaustion matrix — never silently passed.

§4c — the on_exhausted escalation matrix (terminal-iter exhaustion)

When a loop reaches max_attempts without a pass verdict and without a review rescue, it is exhausted. The orchestrator does not just stop — it applies the loop's configured on_exhausted escalation, the interactive mirror of the autonomous harness's on_exhausted dispatch (the runtime routing is runOneFormula in internal/djinn/session/run_formula.go, with the per-class reason stamped by classifyIterOutcome in internal/djinn/session/iter_beads.go).

Read on_exhausted from the loop step's formula describe --json block — the loop.on_exhausted field that djinn formula describe emits per loop step (e.g. regression-loop → leave_and_reopen, the review loops → soft_fail). Do NOT parse the TOML — describe JSON is the orchestrator's only formula source, exactly as for max_attempts and check_function. When on_exhausted is absent from the describe block, default to soft_fail — this mirrors CheckSpec.OnExhaustedOrDefault (internal/djinn/formula/spec.go); describe populates the field for every loop today, but the absent→soft_fail default keeps the branch correct against an older describe payload.

Before the loop's first attempt, capture <pre_iter_sha> once (e.g. git -C "<worktree>" rev-parse HEAD) — the commit HEAD pointed at on loop entry. Both leave_and_reopen (which reports the retained <pre_iter_sha>..HEAD range) and hard_reset (which resets to it) need it. Then branch on the on_exhausted value:

on_exhaustedWorking treebd stateUsed by
soft_failworking tree unchanged — no git reset, no commit rollbackstays in_progress, except design-review-loop, which defers with needs-operator (§4c loop-with-design-review)review loops (design-review-loop, review-loop) and the on_exhausted-absent default
leave_and_reopeniteration commits retained on disk — no git reset; commit any dirty tree as WIP firstreopen via bd update <bead-id> --status=openregression-loop
hard_resetgit reset --hard <pre_iter_sha> in <worktree> — discard the iteration's commitsreopen via bd update <bead-id> --status=opensupported, currently unused by core/iter.formula.toml

For every branch, HALT the walk and emit an escalation message naming the failing loop, the last verdict read, the artifact path, and (for leave_and_reopen / hard_reset) the bead reopen plus the <pre_iter_sha>..HEAD commit range. The three branches differ only in what they do to the working tree and the bead, never in whether they stop — exhaustion is always a hard stop, never a silent pass.

Interactive checkpoint (Step 1b #4): in --interactive mode, pause at the on_exhausted escalation BEFORE applying the disposition — surface the failing loop, its last verdict, and the configured on_exhausted mode so the operator can intervene (inspect, retry by hand, or accept the escalation) before the working-tree / bead disposition is applied. Autonomous mode applies the disposition and HALTs without pausing.

  • soft_fail (review loops' default, and the absent-field default): leave the working tree unchanged and the bead in_progress; the operator resumes the same bead. This is the current review-loop semantics. The one exception is design-review-loop: its exhaustion defers the bead (below).
  • leave_and_reopen (the regression-loop mode): commit any dirty tree as WIP, retain the iteration commits, and bd update <bead-id> --status=open so the queue re-offers the bead with its partial work intact — the same work-preserving disposition the commit-push regression gate applies to a broken tree.
  • hard_reset (supported, unused today): git reset --hard <pre_iter_sha> in <worktree> to discard the iteration's commits, then bd update <bead-id> --status=open.

§4c — structural-fault halt (ActionHalt), distinct from budget exhaustion

The on_exhausted matrix above is the budget-exhaustion path: it fires only at terminal-iter exhaustion (N == max_attempts with no pass and no rescue). The autonomous dispatcher has a fourth, earlier action that is a different failure class — ActionHalt — and the orchestrator must recognize it as distinct, never fold it into the exhaustion matrix.

When a review loop's check detects a structural fault — a BLOCKER-count mismatch or an invented-rate threshold trip — formula.Decide returns ActionHalt (internal/djinn/formula/dispatch.go, HaltExitCode = 3), and it does so before the budget check (Decide evaluates HaltExitCode ahead of latest.Number >= c.MaxAttempts). A structural fault therefore halts the loop immediately regardless of remaining max_attempts budget — a fault on iter 1 of a 5-attempt loop stops at iter 1, not iter 5. There is no rescue, no spawn-next, and no on_exhausted disposition: it is a hard stop the moment you read it, and it is categorically different from reaching max_attempts (the exhaustion matrix above never runs for a structural fault, because the halt fires mid-budget).

Where you read the structural-fault signal. You read it from the same on-disk artifact the loop's verdict token comes from (the §4c "Body → verdict map" below): for a loop-with-review step, the reviewer's fenced json triage sidecar. This mirrors checks.ReviewApproved's exit-3 branch (internal/djinn/formula/checks/review_approved.go) — do not re-derive its thresholds in prose; read the same two signals off that sidecar:

  • a BLOCKER-count mismatch between the sidecar's severity_counts BLOCKER bucket and the count of promoted_findings[] entries with severity=BLOCKER (the ValidateBlockerCount invariant); and
  • an invented-rate threshold trip, read off the sidecar's invented_rate_pct field: the halt fires when invented_rate_pct exceeds 30 (percent) and the total finding count (sum of every severity_counts bucket) is at least 10 — the floor below which a high rate is small-sample noise and stays advisory. Those two constants (inventedRateHaltThreshold = 30.0, inventedRateFloorCount = 10) are pinned in internal/djinn/formula/checks/review_approved.go as the source of truth — mirror that read-condition here, do not re-judge or re-derive its logic.

Only the review loops (loop-with-review) carry this halt; a loop-with-regression non-pass always routes through the verdict map and, at terminal iter, the on_exhausted matrix above.

When you read either structural-fault signal, HALT the walk immediately — regardless of attempts remaining — and escalate to the operator: name the failing loop, the structural fault detected (BLOCKER-count mismatch or invented-rate threshold), and the artifact path. Do not apply an on_exhausted disposition (that is the budget-exhaustion class, a different failure), and never treat a structural fault as a silent pass. In --interactive mode this surfaces at the same on_exhausted escalation checkpoint (Step 1b #4), labeled as a structural-fault halt rather than a budget exhaustion so the operator knows the loop stopped mid-budget, not at its attempt ceiling.

Body → handler map

Shapebody child idHandlerInvokes
loop-with-design-reviewdesign-review (after revise-design)ONE context-isolated design reviewer on the step's resolved adapterthe prompt buildDesignReviewPrompt renders (internal/djinn/session/design_step.go) — a Go builder, not a skill
loop-with-reviewreviewONE context-isolated single-lens reviewer subagent — NOT a multi-agent panelthe /review skill, run by that ONE subagent — the same body the autonomous harness dispatches
loop-with-regressionnone — the loop has no procedure childYOU, in this context: run the project's full-tree test command and its lint commandthe tree itself, not a skill
(all, on spawn-next)fix-*djinn-fixer subagentapplies findings ≥ threshold

review — the single-lens body child. The default formula (core/iter.formula.toml), and so every variant that extends it, declares review-loop, whose body child is review — the only review binding a formula can declare. Dispatch it as ONE context-isolated subagent under the §4b worktree dispatch preamble, running the /review skill:

<!-- djinn-dispatch: codex -->

one codex exec run built as a §4c procedure step (a claude -p run by the claude-p rule when review resolves to adapter: "claude-code"): its prompt embeds the review rubric (djinn skills print review), and it runs read-only.

<!-- djinn-dispatch: end -->

The subagent MUST NOT invoke any other review skill or command, or any further subagent — fanning out would silently reinstate the ~12-agent cost the single-lens chain exists to cut, which is the whole point of the step. (The skill body carries that prohibition itself, so honoring it is not a matter of remembering this paragraph.)

Do not restate the rubric in the dispatch prompt. /review is a registered procedure (internal/djinn/runtime/procbind.go), which means its rubric — the nine-concern sweep, the severity grading, the sidecar field contract — lives in exactly ONE generated body (core/skills/review/SKILL.body.md) that the autonomous BuildReviewPrompt and this handler both dispatch. Paraphrasing it here is what let this section drift from the Go filer before (djinn-itudg). Pass the subagent the review dispatch prompt below; let the skill supply the rubric.

Review dispatch prompt

The reviewer reads no tracker: it may run in a sandbox with no beads database. You read the bead for it — bd show <bead-id> --long --json (--long carries notes) — and fill this block, each field copied verbatim, an empty field left as an empty section. Every review dispatch carries it whole, after the §4b preamble: the Claude subagent's prompt, and step 3 of a §4c procedure prompt.

Review bead <bead-id> with the review procedure. Judge the bead from the block
below, copied verbatim from the tracker; do not query the tracker yourself.

<bead>
ID: <bead-id>
Title: <title>

Description:
<description>

Acceptance Criteria:
<acceptance_criteria>

Design:
<design>

Notes:
<notes>
</bead>

The diff under review is `git diff <base>..HEAD`, run in <worktree>. Every
promoted fix finding must cite a file:line inside it.

When the walk ran a design loop, append the approved design note after it under ## The approved design note, with reviewDesignRule's text (internal/djinn/session/design_step.go): the review checks the diff against the note as well as the criteria, and a departure with no Design departure: line in git log <base>..HEAD is a finding.

Append the attempt's ## Test suite block (§4a suite-run) after it, on every attempt. On a re-review, also append the two prior-iteration blocks the review skill names ("What iteration N-1 found", "What the last fix changed").

The skill emits a fenced json sidecar as the LAST thing in the subagent's visible response text — verdict (SHIP / SHIP-WITH-FIXES / REWORK), severity_counts, invented_rate_pct, promoted_findings[], suite ({revision, exit} from the ## Test suite block), and criteria[] (one {index, status, file, line} row per acceptance line — the record §4a commit-push transcribes as the bead's evidence). A sidecar missing a criteria[] row, or whose suite is missing or disagrees with the ## Test suite block, is a review-invalid result: re-run the reviewer, do not spawn the fixer.

Pass per-step model knobs through on EVERY dispatch, not just review. Read model (and effort) off djinn formula describe <path> --json for each step you dispatch — body children AND fix-* children alike — and pass any declared knob through on the dispatch.

<!-- djinn-dispatch: codex -->

A step whose resolved adapter is codex runs through codex exec by the codex-exec rule below. A step whose resolved adapter is claude-code (a formula:claude label, or a mixed formula passed explicitly) RUNS ON CLAUDE — shell out to claude -p; never run it on codex exec, and never drop its knobs. The operator chose that provider; Step 2's claude check has already stopped the walk when this machine cannot honor the choice.

  • The claude-p rule. Build the step exactly as the codex-exec rule below builds it — the same prompt shape, the same $OUT dir, the same artifact, verdict and fallback rules — and change only the argv, to the one the harness's claude-code driver builds (internal/djinn/runtime/claudecode/profile.go), carrying that step's resolved model and effort off the same formula describe --json record:

    claude -p --dangerously-skip-permissions --output-format stream-json --verbose \
      --model "<resolved model>" --effort "<resolved effort>" \
      < "$OUT/prompt.txt" > "$OUT/stream.jsonl" 2> "$OUT/stderr.log"
    jq -rs 'map(select(.type == "result")) | last | .result // empty' \
      "$OUT/stream.jsonl" > "$OUT/last-message.md"

    Run it from <worktree>, outside this session's sandbox (it needs the network, and bd its database). A write step's prompt opens with the line naming its Claude agent file, <agent file>: the repo's .claude/agents/<name>.md, which djinn init writes, or, in a repo carrying none, ~/.claude/agents/<name>.md, which djinn skills install --global writes:

    Act as the <name> agent: read <agent file> and follow its instructions as your own.

    Print notice: <step-id> declares adapter=claude-code; running it on claude -p (model=<resolved model>, effort=<resolved effort>) before the dispatch.

<!-- djinn-dispatch: end -->
  • The codex-exec rule. When djinn formula describe <path> --json reports adapter: "codex" for a step, that step's declared model and effort are codex-family values and ARE passed through — on the codex exec argv. Read both off that same formula describe --json record for that step (top-level steps at .steps[], loop-body children at .steps[].children[]) and never hardcode them. In the shipped default formula review resolves to model: "gpt-6-astra", effort: "high" even though its TOML step carries no model = line, because the codex model is pinned once at the adapter — which is exactly why the values must be read, not typed.

    This rule covers every step whose resolved adapter is codex, not only review steps — implement included — and every step shape /wish dispatches: top-level single-agent steps, every loop-body child (review) and every fix-* child.

    Two prompt shapes — only ONE of them embeds a rubric. Before building anything, decide which shape the step is:

    • A procedure step is one registered in procedureRubricPaths (internal/djinn/runtime/procbind.go) — the only one is review. Its prompt embeds a rendered rubric body, and that body's path is the registry's value, NOT the step id:

      procedureembedded rubric body (djinn skills print <skill> prints it)
      reviewskills/review/SKILL.md

      internal/djinn/runtime/procbind.go is canonical — read the mapping there if it and this table ever disagree. Resolve the path from the step's prompt binding, not its id: a renamed step id points at a directory that does not exist, and hands codex an empty rubric with no error.

    • A write step — implement, crucible's try, and every fix-* child — is NOT a registered procedure and has no generated skill in the binary's embedded rubric tree. There is no rubric to embed, and a run that tries to embed one feeds codex an empty PROCEDURE RUBRIC block. Its prompt is the one the Go builder for that step renders, all in internal/djinn/session/: buildImplementPrompt for implement, buildTryPrompt for try, BuildFixPrompt for fix-review, BuildFixRegressionPrompt for fix-regression. Build the prompt in that builder's shape, with no BEGIN / END PROCEDURE RUBRIC markers at all. There are two shapes, not one, and a fix-* step built in implement's shape carries no findings — the fixer then has nothing to act on:

      • implement (buildImplementPrompt, internal/djinn/session/formula_prompts.go) — the header line, the bead block, ## Step contract with its instruction list, the ## Already done at HEAD exit on a first claim, then the ## What this step does NOT do list. No payload: the bead IS the input. A codex implement that answers already-done leaves its verdict in the handoff file the prompt names, under the git common dir (<git common dir>/djinn/last-status.json, or <git common dir>/djinn/worktrees/<name>/last-status.json in a linked worktree); read it there and take §4b's close-and-skip.
      • try (buildTryPrompt, internal/djinn/session/formula_prompts.go) — implement's shape without the already-done exit, plus ## The number (the bead's crucible measure, direction, baseline and best) and ## Tries so far (the rows of the bead's .djinn/crucible/<bead-id>.tsv record) ahead of ## Step contract.
      • Every fix-* — the builder's own header/contract framing wrapped around the verbatim prior-step payload. The payload is the substance of a fix prompt; a fix prompt without it does nothing. Fence it as the builder fences it, and reproduce it byte-for-byte rather than paraphrasing:

        stepbuildershape
        fix-reviewbuildReviewFixPrompt, internal/djinn/session/fix_prompt.go## Fix step <id> + a bead_id: <bead-id> line + a fix_threshold: <threshold> line + the INVENTED/counter guardrail clause + the slow-tier clause + ## Findings to apply (the fixer's findings input) + a numbered list, one line per finding: N. [SEVERITY] `<file>:<line>` — <action>. The two input lines are the djinn-fixer's own inputs, which the Go builder omits; without bead_id the fixer stops and changes nothing. No bead block, no ## Step contract, no does-NOT-do list. The findings are the ratcheted actionable set for this iter (this review's findings minus the latent IMPORTANTs filed as follow-ups, plus the unresolved carry from earlier passes) — for a codex run, the findings you extracted from the codex sidecar.
        fix-regressionBuildFixRegressionPrompt, internal/djinn/session/formula_prompts.goHeader + ## Step contract + a numbered list whose item 1 embeds the gate's full-tree go test -short ./... + lint output verbatim, fenced with a backtick run longer than the longest run inside the payload (markdownFenceFor) — a fixed ``` fence is closed early by a transcript that quotes backticks, and the fixer then reads the step contract as test output.

      The argv, sandbox and artifact rules on this page apply to a write step unchanged. Verdict extraction and the sidecar check do NOT — a write step emits no structured artifact at all (§4d), so there is no sidecar to extract and nothing to parse. Its success signal is the ordinary one: a zero exit with the work committed. Running the sidecar check against a write step would fail it on every successful run and take the fallback every time, doing the work twice — and implement is the one step this rule fires on under the default formula, so that failure would be the common case, not the corner case. Its ONLY fallback trigger is trigger 1 below, the run failing.

    The argv. Run it from <worktree> with the prompt fed on stdin — the bare trailing - is what makes codex exec read its instructions from stdin:

    # Codex scratch for this step. It sits DELIBERATELY OUTSIDE the
    # verdict-artifact namespace the §4b resume scan reads (`.djinn/state/<bead-id>/`
    # itself and the `<loop-id>.iter-<N>/` iter dirs): a crash mid-`implement` must
    # not leave a `.codex` entry beside the artifacts where a resumer reads it as
    # "implement already ran" and skips implementation. It also keeps the multi-MB
    # rollout.jsonl out of the dirs the loop driver scans.
    #   .djinn/state/<bead-id>/codex/<step-id>.iter-<N>/   (loop body child)
    #   .djinn/state/<bead-id>/codex/<step-id>/            (top-level step)
    OUT=".djinn/state/<bead-id>/codex/<step-id>.iter-<N>"
    mkdir -p "$OUT"
    
    # PROCEDURE STEPS ONLY. Rubric body = the GENERATED skill named by the
    # procbind.go registry above — NOT <step-id> — YAML frontmatter stripped.
    # A WRITE step (implement / fix-*) SKIPS this line entirely.
    # <registry skill> is the directory in the registry path (review).
    djinn skills print "<registry skill>" \
      | awk 'BEGIN{n=0} /^---$/{n++; next} n>=2{print}' > "$OUT/rubric.md"
    
    # ...build "$OUT/prompt.txt" in the shape for this step (below)...
    
    codex exec --skip-git-repo-check -C "<worktree>" \
      -m "<resolved model>" -c model_reasoning_effort="<resolved effort>" \
      -s read-only --json -o "$OUT/last-message.md" - \
      < "$OUT/prompt.txt" > "$OUT/rollout.jsonl" 2> "$OUT/stderr.log"

    -s read-only is for procedure/review steps only. A read-only step (review) passes -s read-only, matching the "Do not modify code" clause its prompt closes with. A WRITE step — implement, and any fix-* resolved to the codex adapter — passes -s danger-full-access -c approval_policy=never in its place, the policy the drain gives the same steps (CapabilityDefault, internal/djinn/runtime/codex/profile.go):

    codex exec --skip-git-repo-check -C "<worktree>" \
      -m "<resolved model>" -c model_reasoning_effort="<resolved effort>" \
      -s danger-full-access -c approval_policy=never \
      --json -o "$OUT/last-message.md" - \
      < "$OUT/prompt.txt" > "$OUT/rollout.jsonl" 2> "$OUT/stderr.log"

    Never leave a write step's sandbox off the argv: codex then falls back to ~/.codex/config.toml, read-only on a fresh install, and even workspace-write cannot write a linked worktree's index under the main repo's .git/worktrees/ or the beads database, so git add fails on index.lock and the step cannot commit. Never retry a failed write step with a different sandbox either; the argv above is the only one.

    The prompt — procedure steps. Build it on disk in the exact shape buildCodexProcedurePrompt builds (internal/djinn/session/formula_prompts.go) — that builder is the canonical shape, and mirroring it is what makes an interactive codex step transcript-identical to an autonomous one. (A write step does not use this shape at all: it mirrors its own Go builder, named in the two-shapes list above, and skips steps 2 and 4 entirely.)

    1. Header, one line: You are running the `<step-id>` step of the Djinn harness for bead <bead-id>.
    2. ## Step contract, then the paragraph: "This adapter (codex) has no Skill/slash mechanism, so the procedure rubric is embedded below — follow it verbatim against bead <bead-id>."
    3. Any step-specific inputs the Claude dispatch would have passed. For a review body that is the filled review dispatch prompt (§4c), with the prior-iteration blocks on a re-review after a fix — never an instruction to read the bead: a read-only sandbox has no beads database.
    4. The literal marker line ----- BEGIN PROCEDURE RUBRIC -----, then the rubric body ($OUT/rubric.md), then the literal marker line ----- END PROCEDURE RUBRIC -----.
    5. The closing sentence: "Emit the procedure's required output (the fenced json sidecar or report.json the rubric specifies) as the LAST thing in your VISIBLE response text — not inside a tool-result, and not delegated to a subagent. Do not modify code. After the output is in your visible text, exit."

    A write step's builder-rendered prompt carries no such clause and must not gain one — "Do not modify code" contradicts the one thing implement and fix-* exist to do. A write step closes on its builder's own terms: commit the change and exit.

    There is deliberately no leading slash-command token anywhere in the prompt: codex has no Skill/slash mechanism, so the embedded rubric IS the instruction.

    Artifacts and the verdict. The three files above are the run's evidence trail and stay in the step's $OUT scratch dir — never beside the verdict artifacts: rollout.jsonl (the --json JSONL event stream), last-message.md (written by -o), and stderr.log. Do not re-judge the run — extract the fenced json block that sits at the END of last-message.md and write it, unwrapped, to the path the loop driver already reads for that step, .djinn/state/<bead-id>/<loop-id>.iter-<N>/<body-step-id> (§4d — the body step id, no .json suffix):

    awk '/^```json$/ {buf=""; inb=1; next}
         inb && /^```$/ {last=buf; inb=0; next}
         inb {buf = buf $0 ORS}
         END {printf "%s", last}' "$OUT/last-message.md" \
      > ".djinn/state/<bead-id>/<loop-id>.iter-<N>/<body-step-id>"

    Check the extracted output UNCONDITIONALLY — exit status is not the test. A zero exit proves the CLI ran, not that it emitted what the loop reads. A trailing space on a fence line, an indented fence, or a rubric-shaped answer that closed without one all leave a zero-length file at the loop-driver path on a ZERO exit, and the loop then reads an empty verdict and reports no error.

    Assert what the review procedure emits: a fenced json sidecar that is non-empty, parses, and carries verdict.

    A failed assertion is a fallback trigger exactly as an auth failure is — do not leave the unusable file at the driver path; remove it and take the fallback below:

    SIDECAR=".djinn/state/<bead-id>/<loop-id>.iter-<N>/<body-step-id>"
    if ! jq -e '.verdict' "$SIDECAR" >/dev/null 2>&1; then
      rm -f "$SIDECAR"   # an empty/unparseable artifact must not reach the loop
      # → fallback (below), reason: "no parseable verdict artifact in last-message.md"
    fi

    With a checked sidecar at that path the loop driver branches on the codex verdict through the same §4c body→verdict table it uses for a Claude body — SHIP passes review-loop; SHIP-WITH-FIXES / REWORK spawns fix-review, which then acts on the codex findings inside the loop instead of leaving them to land as hand-commits outside it.

    The notice line. Print, before the dispatch, one line of exactly this form, naming what actually ran:

    notice: <step-id> declares adapter=codex; running it on codex exec (model=<resolved model>, effort=<resolved effort>)
    <!-- djinn-dispatch: codex -->

    The fallback is one retry. Take it on either trigger, and only these two:

    <!-- djinn-dispatch: end -->
    1. The run failed. The binary is not on PATH, an auth or quota failure, a sandbox refusal, or any non-zero exit.
    2. The run produced no usable artifact — the unconditional check above did not find a non-empty, JSON-parseable file carrying verdict. This trigger stands independently of exit status: a zero-exit run whose fence the extractor missed fails it too, and silently passing that empty file to the loop is the failure mode this trigger exists to catch. It does not apply to a write step, which emits no artifact by design.
    <!-- djinn-dispatch: codex -->

    Only then re-run the same command once (codex exec, or claude -p for a claude-code step), printing notice: <step-id> <command> failed (<reason>) — retrying once. A second failure on either trigger is not retried again: stop the walk, keep the bead in_progress and <worktree>, record it on the bead with bd update <bead-id> --append-notes="codex dispatch failed: <step-id> — <reason>" (never --notes, which overwrites), and report the step, the reason and $OUT/stderr.log to the operator. There is no other harness to fall back to here.

    <!-- djinn-dispatch: end -->

The shipped pins (operator ruling 2026-09-25, guarded by core/variant_formula_knobs_test.go and TestDefaultReviewModel_CrossFamilyFromImplement): Claude builds, Astra reviews. design and revise-design take implement's knobs and design-review takes review's. implement = opus[1m] / high; review = codex / high with NO model line — the codex model is pinned once at the adapter, and it is the step the codex-exec rule above reads the resolved gpt-6-astra / high for, run READ-ONLY; fix-review and fix-regression = opus[1m] / high, because a fixer edits code a judge just condemned. Every single-agent step declares its adapter and effort.

Body → verdict map (WHERE each verdict is read + its pass token)

Each loop reads its verdict from the body artifact written under the iter dir. Mirror the autonomous check's verdict logic exactly — do not re-judge:

ShapeVerdict source (on disk)Pass tokenNon-pass → spawn fix
loop-with-design-review (design-review-loop)the design reviewer's fenced json sidecar verdict field (<loop-id>.iter-<N>/design-review), AND the note in force carrying all five section headings and naming every overlapping beadAPPROVEREVISE, or an APPROVE of a note missing a section or an overlapping bead → revise-design
loop-with-review (review-loop)the single-lens reviewer's own fenced json triage sidecar verdict field, captured to the iter dir as the body step's artifact (<loop-id>.iter-<N>/review)SHIPSHIP-WITH-FIXES / REWORK → fix-review
loop-with-regressionthe exit status and output of the full-tree test + lint commands you ran — there is no artifact and no sidecar, because there is no body agentboth commands exit 0any non-zero exit → fix-regression, handed the captured output verbatim

These are the same pass tokens the autonomous check functions branch on (VerdictShip, a clean regression run); reading the artifact the skill already wrote is the interactive realization of the in-process parse.

loop-with-design-review — the design is approved before any code

loop-with-design-review (design-review-loop: max_attempts = 3, check_function = djinn.check.design_approved, on_exhausted = soft_fail) sits between design and implement. Its args name the three steps (design_step, revise_step, review_step). Each attempt N:

  1. revise-design, on N ≥ 2 only. Skip it on attempt 1, and when attempt N-1 ended in a review-invalid re-run rather than a verdict. Otherwise dispatch it with the prompt buildReviseDesignPrompt renders: the note in force after attempt N-1, and every finding held against it — the reviewer's REVISE findings plus the mechanical ones (a missing section, an overlapping bead the note never names) — as a numbered list. Its reply is the whole rewritten note ending in ## Answers to the review: save it to .djinn/state/<bead-id>/design-review-loop.iter-<N>/revise-design and post it as djinn design note — round <N> (revise-design). Restore the tree as after design.
  2. design-review. Dispatch ONE reviewer on the step's resolved adapter, model and effort — read-only, the same sandbox as review (a codex step passes -s read-only) — with the prompt buildDesignReviewPrompt renders, the bead block appended as for review: the note in force (the newest revise-design capture at or before N, else the design capture), the parent epic, the overlapping beads, and on N ≥ 2 the previous round's findings. Save its reply to <loop-id>.iter-<N>/design-review and post djinn design review — round <N>: <verdict> with its findings.
  3. The check — you run it, mirroring checks.DesignApproved. No fenced json, an unknown verdict, or a REVISE with no findings is review-invalid: re-run the reviewer once in place. APPROVE passes only when the note in force has a heading for each of Data shapes, Parts touched, Approaches, Proof and Out of scope, and names every overlapping bead; then the loop passes and implement runs next. Anything else iterates. When the bd list read of the overlapping beads fails, never approve: stop the walk before implement and re-run the check once bd answers, as a drain halts (design_transport) and retries the bead.

On a REVISE at N = max_attempts (the third), leave the tree as it is and defer the bead for the operator, as a drain does — the bead does NOT stay in_progress. Write the reason to a temp file: loop design-review-loop exhausted budget (last verdict: fail): design-review-loop exhausted budget; design not approved after <N> review rounds (last verdict <verdict>). Last findings: <every finding the check held against the last note, numbered, on one line> (the text designExhaustionReason builds), then run:

bd update <bead-id> --status=deferred --add-label needs-operator \
  --append-notes "deferred (operator-required): $(cat <reason-file>)"

Confirm with bd show <bead-id> --json that the status is deferred and the labels carry needs-operator, then HALT with an escalation message naming design-review-loop, the rounds spent and the same findings. No implement runs and no commit was made: the design steps never write.

loop-with-regression — you run the check, and reopening is the LAST resort

loop-with-regression (regression-loop: max_attempts = 2, check_function = djinn.check.regression_clean, on_exhausted = leave_and_reopen) sits between review-loop and commit-push. It exists because the full-tree regression gate used to run ONLY inside the autonomous commit-push handler, where its single disposition was rollback — a two-line lint finding could discard a finished iteration with no agent ever reading it.

It inverts the usual loop shape, so do not pattern-match it onto its siblings:

  • There is no procedure body child and no verdict artifact. The autonomous check runs the tree in-process. In this context YOU run it: the project's full-tree test command (this repo: go test -short ./...) and its configured lint command (djinn.lint_argv in .djinn/config.json), from the configured module root inside <worktree>. Run the SAME commands the gate runs — a looser check here passes a loop that the commit-push gate will still reject.
  • A green tree ends the loop in one attempt with no subagent at all. Do not dispatch fix-regression on a clean run; its autonomous counterpart renders an empty prompt and is skipped.
  • A broken tree dispatches fix-regression to djinn-fixer with the captured failure output verbatim in the prompt, plus the standing instruction that weakening or deleting a failing test — or silencing a lint rule — is not an acceptable fix. Then re-run both commands.
  • Exhaustion is the reopen, not the first response: a tree still broken after the budget is the escalation the operator sees, and in autonomous mode it reopens the bead under the gate label regression with the iteration's commits retained — never reset.

keep-loop — keep or reset each round until a stop rule fires

keep-loop (loop.check_function = djinn.check.keep_decision) repeats its body and, after each round, KEEPS the round's commits or RESETS the branch to the last kept commit. It mirrors formula.KeepRunner (internal/djinn/formula/keep_runner.go). It has no pass token, no fix child, no rescue and no exhaustion: ignore loop.on_exhausted (describe prints the default; a keep-loop never reads it).

  1. Before round 1, record the last kept commit: git -C "<worktree>" rev-parse HEAD.
  2. For round N from 1 to loop.max_attempts:
    1. Dispatch the body children in children[] declaration order, each per its shape (single-agent children per §4b).
    2. A body child that fails (the subagent errors, crashes or times out) makes the round a non-keep: reset it (step 4) and go to the next round without running the decide.
    3. Run the decide. The autonomous decide is the in-process check djinn.check.keep_decision; there is no command for it, so apply the rule the loop step's description states (for crucible: measure, and keep only a gain larger than the baseline spread). Its answer is two bits: keep or reset, and stop or continue.
    4. Keep → git -C "<worktree>" rev-parse HEAD is the new last kept commit. Reset → git -C "<worktree>" reset --hard <last kept> then git -C "<worktree>" clean -fd.
    5. Stop, or N == max_attempts → exit the loop and advance the spine to the next step. Reaching max_attempts is a normal end, never an escalation.

The branch is the loop's record: kept rounds' commits stay on it, reset rounds leave nothing.

The fix-* severity filter (spawn-next iterations)

When a loop spawns the paired fix-* child, dispatch djinn-fixer with the body's findings and a fix_threshold (default important). The fixer applies the same severity filter the autonomous fix step uses (the fix-* children in core/iter.formula.toml, core/bead-work-contract.md §5) — flip the threshold in one place and both surfaces flip:

  • Act on findings at severity >= fix_threshold AND kind=fix. With the default important threshold that is BLOCKER + IMPORTANT fix-kind findings (nit additionally admits NIT). Apply each one's action at its file:line.
  • ALWAYS skip COUNTER — a kind=counter finding is an anti-recommendation; never apply it and never codify its rationale, at any threshold.
  • NEVER act on INVENTED — a severity=INVENTED finding is a reviewer hallucination the triage layer already discarded; leave it untouched.

Findings below the threshold that are neither COUNTER nor INVENTED (e.g. a NIT under the default important threshold) are deferred — never the fixer's job. You file them, per the file-nits-* child rule in §4c. A model knob on the dispatched fix-* step reaches the fixer per the knob pass-through rule above. The fixer owns one fix pass and returns a summary, and the loop driver re-dispatches the body to re-check.

§4d — loop-driver artifact parity (autonomous-equivalent on-disk layout)

The loop driver writes the same on-disk artifacts an autonomous formula run writes, so an interactive run is schema-equivalent to an autonomous one (core/SHAPE-REGISTRY.md "Canonical output path", reconciled by djinn-ipee.14):

  • Per-iter loop-body state → .djinn/state/<bead>/<loop-id>.iter-<N>/<step-id>. The iter is a directory segment named <loop-id>.iter-<N> (e.g. review-loop.iter-2); the file inside is named by the body step id with no .json suffix (the defaultStepFiles convention). The fix-* child writes no structured artifact — its output is the working-tree mutation. There is no <session> segment in the path — provenance lives in fields inside the artifacts, not as path segments.
  • Evidence → a JSON bd comment (schema: "djinn-evidence", run, iter) on the bead (bd comments add <bead> -f <tmpfile>, read back with bd comments <bead> --json), one criteria entry per acceptance_criteria segment, per core/bead-work-contract.md §6. It is a comment, not a file, so it outlives the run's worktree (djinn-mr56i). djinn wish land writes it at commit-push, transcribed from the final review sidecar (§4a) — the same point, source and mapping the autonomous harness uses.

Writing the body verdict to .djinn/state/<bead>/<loop-id>.iter-<N>/<step-id> is what lets the loop driver re-read "the latest body output" the same way the autonomous LatestStepOutput does — the read path and the write path share one layout across both modes.

Step 5 — Walk to completion

Dispatch each step in topological order until commit-push has run (the terminal go-mode bookend) or a stub/checkpoint pauses the walk for the operator. The orchestrator owns the order and the dispatch; each subagent owns one step's work and reports back. This is the interactive realization of the autonomous formula run — same steps, same order, same shape→handler contract, with per-step context isolation supplied by subagent dispatch instead of subprocess spawning.


Why this exists

The autonomous harness (internal/djinn/session/run_formula.go) and this orchestrator implement the same core/SHAPE-REGISTRY.md contract from two sides — the registry's three-point contract names both. Driving the formula from its formula describe --json description, rather than re-encoding the steps here, is what guarantees the interactive and autonomous paths cannot disagree about which steps run or in what order: edit the formula, and both walkers follow.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.