@sabaiway/agent-workflow-kit 5.9.0 → 5.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/README.md +2 -2
  3. package/SKILL.md +1 -1
  4. package/bridges/antigravity-cli-bridge/SKILL.md +32 -11
  5. package/bridges/antigravity-cli-bridge/bin/agy-envelope.mjs +160 -0
  6. package/bridges/antigravity-cli-bridge/bin/agy-envelope.test.mjs +235 -0
  7. package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +23 -1
  8. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +242 -38
  9. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +482 -38
  10. package/bridges/antigravity-cli-bridge/capability.json +3 -2
  11. package/bridges/antigravity-cli-bridge/references/models-and-flags.md +45 -12
  12. package/bridges/antigravity-cli-bridge/references/review-prompt.md +6 -3
  13. package/bridges/antigravity-cli-bridge/setup/README.md +18 -5
  14. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +1 -1
  15. package/capability.json +1 -1
  16. package/package.json +1 -1
  17. package/references/hooks/state-block-guard.mjs +107 -45
  18. package/references/modes/commit-guard.md +11 -8
  19. package/references/modes/core-evidence.md +1 -1
  20. package/references/modes/dispatch.md +32 -10
  21. package/references/modes/set-recipe.md +8 -5
  22. package/references/modes/state-block-guard.md +39 -31
  23. package/references/modes/worktrees.md +47 -3
  24. package/references/scripts/check-docs-size-cli.test.mjs +2 -2
  25. package/references/shared/report-footer.md +2 -2
  26. package/references/templates/agent_rules.md +1 -0
  27. package/tools/advisor-matrix.mjs +165 -0
  28. package/tools/commands.mjs +2 -2
  29. package/tools/commit-guard.mjs +74 -17
  30. package/tools/core-evidence.mjs +10 -0
  31. package/tools/detect-backends.mjs +1 -0
  32. package/tools/dispatch-advisor.mjs +323 -0
  33. package/tools/dispatch.mjs +174 -109
  34. package/tools/doc-parity.mjs +68 -14
  35. package/tools/ensure-configs.mjs +4 -4
  36. package/tools/flow-check-cores.mjs +35 -6
  37. package/tools/flow-check-rungs.mjs +20 -2
  38. package/tools/flow-check.mjs +20 -5
  39. package/tools/lens-region.mjs +13 -1
  40. package/tools/observation-builder.mjs +123 -0
  41. package/tools/satellite-locator.mjs +179 -0
  42. package/tools/source-size-scope.mjs +3 -1
  43. package/tools/worktree-handoff-return.mjs +369 -0
  44. package/tools/worktree-prompt.mjs +190 -0
  45. package/tools/worktrees-record.mjs +171 -0
  46. package/tools/worktrees.mjs +308 -297
@@ -4,17 +4,39 @@
4
4
 
5
5
  The **delegation engine** (delegation Plans 1–2): the sub-task **contract check**, the records a human hand-writes into the delegation ledger, the four **writer verbs** that put a delegated thread on the record, the **arrival waiter**, and the L0 **acceptance report** over one wave. It exists because nothing today records `{dispatched → returned → folded}` for EXEC work, so "how much does delegating actually buy?" is unanswerable. The ledger answers it; this mode is the surface a number enters it through, and the surface it is read back from.
6
6
 
7
- Run **`node ${CLAUDE_SKILL_DIR}/tools/dispatch.mjs <check|register|observe|open|await|return|fold|degrade|aggregate> …`**:
7
+ Run **`node ${CLAUDE_SKILL_DIR}/tools/dispatch.mjs <check|advise|register|observe|open|await|return|fold|degrade|handoff-return|aggregate> …`**:
8
8
 
9
9
  1. **`check <dispatch-file>`** — validates the ONE ```` ```aw-dispatch-contract ```` fenced block the dispatch file carries: exit `0` when its form is valid, `1` naming the FIRST violated field. The contract of the whole mode, in one sentence: the contract check is FORM-only — fields present, grammars respected, never boundedness, design-decidedness or acceptance adequacy — and `aggregate` REFUSES instead of computing acceptance for a wave with no pre-registration record, over an OPEN thread in scope, over a PRE-DISPATCH degrade that opens no thread, or across several waves with no `--wave`; the writer verbs add NO second legality door — the store's preflight is the only one, and its refusals travel verbatim — while `open` copies every mint-time field from the contract header and refuses a deadline below the wrapper cap plus the kill grace, `return` absorbs only a TERMINAL exec receipt whose backend, nonce and independently computed contractDigest match the dispatch it answers, and `fold` binds the CURRENT tree to the folded return's postTreeDigest, so a tree that moved between the two never folds, and `await` is satisfied ONLY by the TERMINAL exec receipt of its own dispatch's {backend, nonce} — never by a review receipt, a ledger line or a finding manifest — while an expiry names a supervision question and releases NO writer slot. Whether the sub-task is genuinely bounded, whether its design is decided, and whether its acceptance criteria are adequate stay **explicit orchestrator judgment** — a well-formed absurdity passes here by construction.
10
- 2. **`register`**appends the wave's **pre-registration** record (`--wave --step-classes --pairing-key --min-per-class --mean-l-threshold --first-pass-num --first-pass-den`). Acceptance is pre-registered so thresholds can never be chosen after the observations that will be judged against them; a registration is **immutable per wave** and a second one refuses. `--pairing-key` accepts only a key the aggregator actually implements (today `stepClass`) a wave registered under any other key would record a contract the computation never follows, and the aggregator re-checks the registration independently, since a store can be hand-written.
11
- 3. **`observe`** — appends ONE **observation** (`--wave --step-class --scope --plan --phase --provenance [--denominator-bytes]`). Provenance is `solo-construction` or `self-reported` **only**: delegated accounting is DERIVED from nonce threads and is never hand-appended. The numerator is the post-image bytes of the objects the scope names; a **solo** observation takes that same number as its denominator — `L = 1` by construction, so `--denominator-bytes` is refused — while a self-reported one states its own. One stated exception: a solo scope that measures **zero** bytes has no ratio at all, and is recorded INELIGIBLE by name (`zero-denominator`) rather than as `L = 1` — `0/0` is undefined before it is one, and a named ineligibility is what this family records instead of a silent number. **`--scope` is repeatable**, one repo-relative path per occurrence: no separator is safe inside a POSIX path, so a split list could not express `docs/my file.md` and would silently measure the wrong set wherever its fragments happened to exist. Paths are anchored at the **git top-level**, never at the caller's cwd, and the record carries a **JSON-encoded array of canonical repo-relative paths, carried in the schema's string field** (the record's `scope` is a string by D3, so the array travels encoded). An object's identity IS its canonical path, so two files with identical bytes are two objects and count twice, while one path reached twice (a repeated listing, an in-repo ancestor symlink) is one object and counts once. The run refuses by name outside a git work tree, for an absent or non-regular path, and for a path whose **real** location leaves the repository (an ancestor symlink is not a way out).
12
- 4. **`open`** — appends the **dispatch** record that opens a thread (`--contract <dispatch-file> --wave --backend --rationale --wrapper-cap-s --kill-grace-s [--retry-of <nonce>]`). Every mint-time field — `nonce`, `stepClass`, `vehicle`, `deadlineS`, `retry.index`, `retry.cap` — is **COPIED from the contract header** and bound by `contractDigest`, so a dispatch can never disagree with the header it claims to carry. It fingerprints the tree (`preTreeDigest`) and probes it for a CLEAN baseline; a **dirty** one is recorded as such, and the eventual return is then metric-INELIGIBLE by the name `dirty-baseline` the uncommitted-state fingerprint is blind to the index↔worktree split, so a dirty baseline cannot attribute bytes to a dispatch. It **refuses a `deadlineS` below `--wrapper-cap-s` + `--kill-grace-s`** (both explicit operands: the kit never reads another package's default), because a dispatch that cannot be capped inside its own deadline could never honour the terminal-exit rule. Every retry rule one successor per thread, the cap read off the chain's ORIGIN, wave and class equality, a changed contract after a `contract-refusal` — is the **store's**, and `open` surfaces its refusals verbatim.
13
- 5. **`await`** — watches for ONE dispatch to **answer**, and writes nothing at all (`--nonce [--timeout <s>]`). Satisfaction is the arrival of the **TERMINAL** exec receipt for the dispatch's own `{backend, nonce}`: a `reserved` artifact means the run holds the nonce and has published nothing about how it ended, so the wait continues — the same artifact that is a **supervision refusal** at `return`, because absorbing it would record an outcome nobody observed. Satisfaction is decided **positively**, by the exec-receipt reader itself, so an artifact of a neighbouring family standing at that path — a review receipt, a delegation ledger line, a finding manifest — **refuses** rather than satisfies: only an exec receipt answers an exec dispatch, which is the D10 rule the review waiter states from its own side. The bound is the **ABSOLUTE** deadline, measured from the DISPATCH record's timestamp rather than from whenever this wait began: `--timeout` defaults to the time **remaining**, and one reaching past the deadline is **refused** rather than silently clamped (a shortened wait would let a caller believe they waited longer than they did). An already-expired dispatch is **answered immediately**, never waited on. **Arrival is read first, before EVERY bound** — the two wait bounds and the `--timeout` admissibility check alike, so a receipt already on disk answers `ARRIVED` even when the supplied `--timeout` is inadmissible, and the check runs only once there genuinely IS a wait to bound. A receipt already on disk is a fact this verb reports; whether a *late* one may be absorbed is the absorb door's question, refused there by name — **lateness has exactly one decision site, and it is `return`**. The review waiter checks its deadline first for the opposite reason: a receipt *line* carries no dispatch-bound timestamp at all, so there the clock is the only evidence there is. Each pass consults the clock over the poll that has already happened, then sleeps and polls again, so a receipt landing during the last sleep is reported rather than lost to a cutoff a moment later. An unanswered wait exits **`3`** — its own status so a caller that **branches** on the code can tell it from a refusal, never a defence against one that discards failure wholesale — names whether the **deadline** or the `--timeout` ended it, and states that **no writer slot was released**: a wait that ended without an answer never authorizes the next dispatch. Every artifact read rides the store's own no-follow reader, so a symlinked, FIFO, unreadable or malformed artifact refuses **by class** instead of standing in for an answer.
14
- 6. **`return`** — absorbs the wrapper's exec receipt and appends the **return** record (`--nonce [--outcome <o>] [--no-receipt --exit-status <n>]`). The receipt and its report live **beside the ledger**, in the store's own directory. It reads the **TERMINAL** receipt — a `reserved` one is a **supervision question**, not a timeout, and refuses by saying so — then checks the receipt's independently computed `contractDigest` against the dispatch's (a run that executed a *different* contract would otherwise correlate cleanly), its `{backend, nonce}` body against the dispatch it answers (the filename alone is composed by this side), its `capS + killGraceS` against the recorded `deadlineS`, and its timestamp against the **ABSOLUTE** deadline (`dispatch.timestamp + deadlineS`) — a late return refuses by name. It re-verifies the report's digest and length against the receipt, refuses a change set **hidden from the plain `git diff`** the metric is computed over (assume-unchanged, skip-worktree, `diff.ignoreSubmodules` — the numerator and the denominator would both be blind to it), and then enumerates the returned objects and frames the canonical integration bundle, **bracketed by the tree fingerprint**: a tree that moves mid-computation refuses rather than mixing two change sets. `--outcome` records an orchestrator judgment under the **closed override form** — a wrapper outcome either stays itself or moves to `contract-refusal | partial-edit | acceptance-failure | stale-return | store-failure`, and `success` is recordable only from a receipt that already says success; the record vocabulary's own cross-field rules cut the remaining incompatible pairs. **`--no-receipt`** means "no TERMINAL receipt", never "no evidence": it absorbs a **reservation ONLY** — over a `terminal` artifact it refuses, because a hand-stated outcome there would discard an exit status, a session id and a report digest the run actually proved, and skip the report check that guards them. It builds the return from that reservation (`wrapperVersion` and `posture`, never hand-typed), requires `--exit-status` and an `--outcome` from `transport-failure | contract-refusal | store-failure | missing-identity` (so `partial-edit` and `stale-return` are inexpressible there by name), and reads the report **if present** — an absent one records length 0 and the metric is then ineligible **by name**: `empty-report` where there IS a diff, and `no-op-diff` where the tree is unchanged too, since the eligibility rule names the diff first. That keeps a failed REPORT write and a failed TERMINAL write two separately named lanes. With **no artifact at all** there is no honest return — `--no-receipt` reads the same path and would refuse the same way — so close the thread with `degrade`.
15
- 7. **`fold`** — appends the integration re-confirmation and CLOSES the thread (`--nonce --verdict <text>`). It runs the **same hidden-path guard** as `return`, and here for a sharper reason: `treeDigestAtFold` is computed from the very payload that is blind to those paths, so a change made behind an `assume-unchanged` / `skip-worktree` bit between the return and the fold leaves the digest **equal** and the fold would accept bytes nobody returned — the digest cannot catch what the payload cannot see. It then computes the **current** tree digest, which the store binds to the folded return's `postTreeDigest`: a tree that moved between the return and the fold refuses in the store's own words, and the recovery it names is a fresh dispatch, never a fold. Staging usually moves the tree too — every change set carrying an untracked path does — so the order is `return` → review (own eyes + the configured review recipe) → a plain gate run → `fold` → `git add`. **The one shape staging does not move** is a tracked-only change passing from the worktree into a **clean** index: the payload concatenates the staged and unstaged diffs, so those bytes are identical either side of `git add` (the uncommitted-state fingerprint's stated blindness to the index↔worktree split). The fold is still honest there — identical payload bytes mean the folded content IS the returned content — so the rule is "**the fold precedes staging**", never "staging refuses the fold by construction".
16
- 8. **`degrade`** — appends the recorded no-fold closure (`--wave --step-class --rationale [--nonce]`), **threaded** (with `--nonce`) or **pre-dispatch** (without). The pre-dispatch form opens no nonce thread, so once one is recorded `aggregate` **REFUSES the whole wave by name** until the wave's counting unit is decided — said here and on the line that writes it, because it is a live cost of the record, not a surprise to discover later.
17
- 9. **`aggregate [--wave <id>]`** reports one wave: the registered thresholds, every observation (context — never part of the acceptance number), and per registered step class the delegated threads with the **D7 inclusion table** applied. A folded **success** with an eligible `wrapper-git` metric contributes its `L`; a folded success whose metric is ineligible or self-reported is EXCLUDED from the mean and from `n` while still counting in the first-pass rate; a failure-terminal thread, a degrade-closed one, and a folded **acceptance-failure** (the §7 fold-fix — the orchestrator paid for the same work twice, so its bytes are not leverage) are each in `n` at `L = 0`. The first-pass rate is per retry CHAIN: its denominator is the `retryIndex`-0 threads, so a retry never reports one piece of work twice. Below the registered minimum acceptance is **NOT computed** (insufficient); at or above it is computed and labeled **PILOT** evidence. The primary statistic is the per-class MEAN of `L`; the byte-weighted aggregate prints as SECONDARY only.
10
+ 2. **`advise --step-class <c>`** the **vehicle-routing advisory**, and the question that comes BEFORE a contract check: *which vehicle carries this step class on THIS host, and what has the ledger already recorded for it.* It prints one block — the advised vehicle with its availability, the solo fallback, the recorded history, and the note below — and **decides nothing**: the advice never gates a dispatch: which vehicle carries a sub-task stays orchestrator judgment, and a divergence from the advice is recorded as a note rather than refused. Exit `0` for every legal step class, `2` only on usage. Its posture, stated exactly: the advisor module itself writes nothing, spawns nothing and opens no second ledger door; the VERB may run read-only git probes — the delegation store path (unless AW_DELEGATION_STORE names it outright) and the repository top-level the cheap vehicles are anchored at — and it never runs a vehicle, a subscription CLI, or anything that writes. Host capability is read from the filesystem — the execute backend from the bridge install (the detector spawns nothing at all), a cheap vehicle from the presence of `.claude/agents/<name>.md` **at the repository top-level**; where that root is not resolved, the lane answers **`unknown`** rather than claiming a vehicle is unplaced, because an absent file under an unanchored directory is not evidence of absence and a present one there is not evidence it is this repository's vehicle. The recorded history is the ledger's OWN thread walk, over the closed state set `folded | failure-terminal | degrade-closed | open` with **`open` counted separately** (a live thread is not evidence about a finished one). An **absent** ledger prints `no recorded history`; an **unreadable** one prints the store's own words and the advice still prints; neither is a refusal. The same block prints as a **footer on a form-valid `check`** (never under a refusal, so it cannot mask one), where the advised vehicle is compared against the contract's `vehicle.selected` and a difference is printed as a divergence NOTE naming `vehicle.requested` when the pair itself differs.
11
+
12
+ The vehicle-routing matrix one row per step class, and this copy is held to the code's registry CELL for cell by a structure check (a reorder, a duplicate, a dropped row, a mis-bound vehicle and a drifted availability or returns cell all leave every token present, so a token check would pass every one of them). The check reads only the anchored surface between the two markers below, so a copy of this table anywhere else in the doc can neither stand in for it nor mask its drift:
13
+
14
+ <!-- advisor-matrix:begin -->
15
+
16
+ | step class | vehicle | availability | returns |
17
+ | --- | --- | --- | --- |
18
+ | `code` | codex-exec | execute backend — readiness read from the bridge install, never from a spawn | a diff plus the wrapper's exec receipt |
19
+ | `extraction` | mechanical-sweep | bundled vehicle — present once placed in .claude/agents/ | an extraction report you verify |
20
+ | `triage` | gate-triage | bundled vehicle — present once placed in .claude/agents/ | a structured gate-failure classification |
21
+ | `draft` | changelog-skeleton | bundled vehicle — present once placed in .claude/agents/ | a factual skeleton |
22
+ | `research` | doc-research | HOST-LOCAL — a per-host web grant, never bundled with the kit | cited findings |
23
+ | `review-opinion` | review-lens | bundled vehicle — present once placed in .claude/agents/ | one additional review opinion |
24
+ | `worktree-stream` | worktrees | ships with the kit — available wherever the kit is deployed | a prepared satellite diff plus its handoff |
25
+ | harness subagent | the host's own | the harness's own subagent lane is ASSUMED/manual — it is not kit-detectable, so it carries no availability verdict here and no acceptance weight | not measured |
26
+
27
+ <!-- advisor-matrix:end -->
28
+
29
+ **SIX of the seven rows are portable, for three different reasons, and exactly ONE is not.** Four are the bundled cheap-agent vehicles — the kit ships them and `/agent-workflow-kit agents` places them, so their availability is the presence of a file. One is the execute backend, portable because the bridge is installable anywhere, so its availability is that install. One is the kit's own worktrees mode, portable because it IS the kit. The single non-portable row is `doc-research`: its web access is a deliberate per-host grant, never bundled, so it renders **HOST-LOCAL** in both states and where the file is absent it names the solo fallback instead. The last row of the table is not a step class at all — it carries no availability verdict and no acceptance weight. The `worktree-stream` row inherits the **one-writer bar** the worktrees mode states: one writing session per worktree is a BAR, not a mechanism, exactly as D10 stands for the exec lane below.
30
+
31
+ 3. **`register`** — appends the wave's **pre-registration** record (`--wave --step-classes --pairing-key --min-per-class --mean-l-threshold --first-pass-num --first-pass-den`). Acceptance is pre-registered so thresholds can never be chosen after the observations that will be judged against them; a registration is **immutable per wave** and a second one refuses. `--pairing-key` accepts only a key the aggregator actually implements (today `stepClass`) — a wave registered under any other key would record a contract the computation never follows, and the aggregator re-checks the registration independently, since a store can be hand-written.
32
+ 4. **`observe`** — appends ONE **observation** (`--wave --step-class --scope --plan --phase --provenance [--denominator-bytes]`). Provenance is `solo-construction` or `self-reported` **only**: delegated accounting is DERIVED from nonce threads and is never hand-appended. The numerator is the post-image bytes of the objects the scope names; a **solo** observation takes that same number as its denominator — `L = 1` by construction, so `--denominator-bytes` is refused — while a self-reported one states its own. One stated exception: a solo scope that measures **zero** bytes has no ratio at all, and is recorded INELIGIBLE by name (`zero-denominator`) rather than as `L = 1` — `0/0` is undefined before it is one, and a named ineligibility is what this family records instead of a silent number. **`--scope` is repeatable**, one repo-relative path per occurrence: no separator is safe inside a POSIX path, so a split list could not express `docs/my file.md` and would silently measure the wrong set wherever its fragments happened to exist. Paths are anchored at the **git top-level**, never at the caller's cwd, and the record carries a **JSON-encoded array of canonical repo-relative paths, carried in the schema's string field** (the record's `scope` is a string by D3, so the array travels encoded). An object's identity IS its canonical path, so two files with identical bytes are two objects and count twice, while one path reached twice (a repeated listing, an in-repo ancestor symlink) is one object and counts once. The run refuses by name outside a git work tree, for an absent or non-regular path, and for a path whose **real** location leaves the repository (an ancestor symlink is not a way out).
33
+ 5. **`open`** — appends the **dispatch** record that opens a thread (`--contract <dispatch-file> --wave --backend --rationale --wrapper-cap-s --kill-grace-s [--retry-of <nonce>]`). Every mint-time field — `nonce`, `stepClass`, `vehicle`, `deadlineS`, `retry.index`, `retry.cap` — is **COPIED from the contract header** and bound by `contractDigest`, so a dispatch can never disagree with the header it claims to carry. It fingerprints the tree (`preTreeDigest`) and probes it for a CLEAN baseline; a **dirty** one is recorded as such, and the eventual return is then metric-INELIGIBLE by the name `dirty-baseline` — the uncommitted-state fingerprint is blind to the index↔worktree split, so a dirty baseline cannot attribute bytes to a dispatch. It **refuses a `deadlineS` below `--wrapper-cap-s` + `--kill-grace-s`** (both explicit operands: the kit never reads another package's default), because a dispatch that cannot be capped inside its own deadline could never honour the terminal-exit rule. Every retry rule — one successor per thread, the cap read off the chain's ORIGIN, wave and class equality, a changed contract after a `contract-refusal` — is the **store's**, and `open` surfaces its refusals verbatim.
34
+ 6. **`await`** — watches for ONE dispatch to **answer**, and writes nothing at all (`--nonce [--timeout <s>]`). Satisfaction is the arrival of the **TERMINAL** exec receipt for the dispatch's own `{backend, nonce}`: a `reserved` artifact means the run holds the nonce and has published nothing about how it ended, so the wait continues — the same artifact that is a **supervision refusal** at `return`, because absorbing it would record an outcome nobody observed. Satisfaction is decided **positively**, by the exec-receipt reader itself, so an artifact of a neighbouring family standing at that path — a review receipt, a delegation ledger line, a finding manifest — **refuses** rather than satisfies: only an exec receipt answers an exec dispatch, which is the D10 rule the review waiter states from its own side. The bound is the **ABSOLUTE** deadline, measured from the DISPATCH record's timestamp rather than from whenever this wait began: `--timeout` defaults to the time **remaining**, and one reaching past the deadline is **refused** rather than silently clamped (a shortened wait would let a caller believe they waited longer than they did). An already-expired dispatch is **answered immediately**, never waited on. **Arrival is read first, before EVERY bound** — the two wait bounds and the `--timeout` admissibility check alike, so a receipt already on disk answers `ARRIVED` even when the supplied `--timeout` is inadmissible, and the check runs only once there genuinely IS a wait to bound. A receipt already on disk is a fact this verb reports; whether a *late* one may be absorbed is the absorb door's question, refused there by name — **lateness has exactly one decision site, and it is `return`**. The review waiter checks its deadline first for the opposite reason: a receipt *line* carries no dispatch-bound timestamp at all, so there the clock is the only evidence there is. Each pass consults the clock over the poll that has already happened, then sleeps and polls again, so a receipt landing during the last sleep is reported rather than lost to a cutoff a moment later. An unanswered wait exits **`3`** — its own status so a caller that **branches** on the code can tell it from a refusal, never a defence against one that discards failure wholesale — names whether the **deadline** or the `--timeout` ended it, and states that **no writer slot was released**: a wait that ended without an answer never authorizes the next dispatch. Every artifact read rides the store's own no-follow reader, so a symlinked, FIFO, unreadable or malformed artifact refuses **by class** instead of standing in for an answer.
35
+ 7. **`return`** — absorbs the wrapper's exec receipt and appends the **return** record (`--nonce [--outcome <o>] [--no-receipt --exit-status <n>]`). The receipt and its report live **beside the ledger**, in the store's own directory. It reads the **TERMINAL** receipt — a `reserved` one is a **supervision question**, not a timeout, and refuses by saying so — then checks the receipt's independently computed `contractDigest` against the dispatch's (a run that executed a *different* contract would otherwise correlate cleanly), its `{backend, nonce}` body against the dispatch it answers (the filename alone is composed by this side), its `capS + killGraceS` against the recorded `deadlineS`, and its timestamp against the **ABSOLUTE** deadline (`dispatch.timestamp + deadlineS`) — a late return refuses by name. It re-verifies the report's digest and length against the receipt, refuses a change set **hidden from the plain `git diff`** the metric is computed over (assume-unchanged, skip-worktree, `diff.ignoreSubmodules` — the numerator and the denominator would both be blind to it), and then enumerates the returned objects and frames the canonical integration bundle, **bracketed by the tree fingerprint**: a tree that moves mid-computation refuses rather than mixing two change sets. `--outcome` records an orchestrator judgment under the **closed override form** — a wrapper outcome either stays itself or moves to `contract-refusal | partial-edit | acceptance-failure | stale-return | store-failure`, and `success` is recordable only from a receipt that already says success; the record vocabulary's own cross-field rules cut the remaining incompatible pairs. **`--no-receipt`** means "no TERMINAL receipt", never "no evidence": it absorbs a **reservation ONLY** — over a `terminal` artifact it refuses, because a hand-stated outcome there would discard an exit status, a session id and a report digest the run actually proved, and skip the report check that guards them. It builds the return from that reservation (`wrapperVersion` and `posture`, never hand-typed), requires `--exit-status` and an `--outcome` from `transport-failure | contract-refusal | store-failure | missing-identity` (so `partial-edit` and `stale-return` are inexpressible there by name), and reads the report **if present** — an absent one records length 0 and the metric is then ineligible **by name**: `empty-report` where there IS a diff, and `no-op-diff` where the tree is unchanged too, since the eligibility rule names the diff first. That keeps a failed REPORT write and a failed TERMINAL write two separately named lanes. With **no artifact at all** there is no honest return — `--no-receipt` reads the same path and would refuse the same way — so close the thread with `degrade`.
36
+ 8. **`fold`** — appends the integration re-confirmation and CLOSES the thread (`--nonce --verdict <text>`). It runs the **same hidden-path guard** as `return`, and here for a sharper reason: `treeDigestAtFold` is computed from the very payload that is blind to those paths, so a change made behind an `assume-unchanged` / `skip-worktree` bit between the return and the fold leaves the digest **equal** and the fold would accept bytes nobody returned — the digest cannot catch what the payload cannot see. It then computes the **current** tree digest, which the store binds to the folded return's `postTreeDigest`: a tree that moved between the return and the fold refuses in the store's own words, and the recovery it names is a fresh dispatch, never a fold. Staging usually moves the tree too — every change set carrying an untracked path does — so the order is `return` → review (own eyes + the configured review recipe) → a plain gate run → `fold` → `git add`. **The one shape staging does not move** is a tracked-only change passing from the worktree into a **clean** index: the payload concatenates the staged and unstaged diffs, so those bytes are identical either side of `git add` (the uncommitted-state fingerprint's stated blindness to the index↔worktree split). The fold is still honest there — identical payload bytes mean the folded content IS the returned content — so the rule is "**the fold precedes staging**", never "staging refuses the fold by construction".
37
+ 9. **`degrade`** — appends the recorded no-fold closure (`--wave --step-class --rationale [--nonce]`), **threaded** (with `--nonce`) or **pre-dispatch** (without). The pre-dispatch form opens no nonce thread, so once one is recorded `aggregate` **REFUSES the whole wave by name** until the wave's counting unit is decided — said here and on the line that writes it, because it is a live cost of the record, not a surprise to discover later.
38
+ 10. **`handoff-return --slug <s> --wave <id> --plan <id> --phase <n>`** — the **worktree-stream return rung**, run FROM MAIN after `land --prepare` (from inside a linked worktree it refuses: the git dir is not the git common dir, and the shared common dir would let it attest the wrong tree). It locates the satellite through the handoff identity (the same locator leaf the worktrees mode binds on), **DELIVERS** every user-owned fragment of the handoff **byte verbatim** — the content before and after `## Provision record` alike, each between boundary lines carrying its byte length, so the delivery is verifiable rather than asserted — and names the MAIN-owned destinations (findings → `docs/plans/queue.md`; decisions and session records → the `docs/ai` records); the fold itself stays orchestrator judgment and the rung claims nothing about whether it happened. It requires **BOTH** `prepared-tree` and `prepared-head` from the record and re-attests them against MAIN — the staged write-tree must equal the recorded tree and HEAD must equal the recorded head, because a clean post-commit index reproduces the committed tree, so a tree comparison alone cannot close the window; a record with no `prepared-head` was written by an earlier kit and refuses by name pointing at `land --prepare`. Then it classifies the prepared change set against the **observation domain** (present regular files only — a regular BINARY file is inside, its bytes read like any other) and appends ONE `self-reported` `worktree-stream` observation through the store's single door — numerator: the change set's **blob bytes, read from the attested tree itself** (`git cat-file` over the diff-tree entries' new OIDs, every answer validated fail-closed) — never from disk, which an unstaged edit after the prepare moves silently; denominator: the handoff byte count. The staged write-tree and HEAD are re-checked once more **immediately before either answer** (the pre-append idiom — it narrows the race window rather than closing it, this family's stated posture). A **deletion**, a **rename's absent old side**, a **symlink**, a **submodule**, a **mode-only change** (no measurable byte change at all), a **path whose name is not valid UTF-8** (the record's string domain cannot carry it; it renders as hex) and every other unrepresentable form end instead with `observation: NOT RECORDED — <form> at <path> is outside the observation domain` at exit `0`, delivery and proof still printed — **no partial scope is ever recorded**: a number that silently omitted the deletions half of a landing would be worse than a named non-record. **Honest limit (double-recorded):** the closed observation key set carries no artifact digest, so the handoff's sha256 and the two OIDs are the rung's **printed proof, never ledger fields** — an ACCEPTED limitation, recorded in the ADR with its follow-up row in the queue. The printed next-step order is the worktrees mode's after-the-fold order: a fold landed after the gates leaves them stale.
39
+ 11. **`aggregate [--wave <id>]`** — reports one wave: the registered thresholds, every observation (context — never part of the acceptance number), and per registered step class the delegated threads with the **D7 inclusion table** applied. A folded **success** with an eligible `wrapper-git` metric contributes its `L`; a folded success whose metric is ineligible or self-reported is EXCLUDED from the mean and from `n` while still counting in the first-pass rate; a failure-terminal thread, a degrade-closed one, and a folded **acceptance-failure** (the fold-fix judgment — the orchestrator paid for the same work twice, so its bytes are not leverage) are each in `n` at `L = 0`. The first-pass rate is per retry CHAIN: its denominator is the `retryIndex`-0 threads, so a retry never reports one piece of work twice. Below the registered minimum acceptance is **NOT computed** (insufficient); at or above it is computed and labeled **PILOT** evidence. The primary statistic is the per-class MEAN of `L`; the byte-weighted aggregate prints as SECONDARY only.
18
40
 
19
41
  **Exit codes:** `0` success (a computed report and an insufficient one both exit 0 — go/no-go is judgment, never an exit code); `1` a refusal (a form violation, an unreadable dispatch file, a supervision question, any store STOP verbatim); `2` usage; `3` an `await` that ended with no terminal receipt — its own status so a caller that **branches** on the exit code can tell an unanswered wait from a refusal; a caller that discards every failure discards this one too.
20
42
 
@@ -26,4 +48,4 @@ Run **`node ${CLAUDE_SKILL_DIR}/tools/dispatch.mjs <check|register|observe|open|
26
48
 
27
49
  **What the fold's binding IS, and its stated residuals.** The binding is **equality of the visible unframed payload** between the return and the fold — never an identity of the tree, and the difference is not academic. The payload is an unframed concatenation, so **two different trees can produce the same bytes**: probed live, a single file containing the line `untracked:two.txt` yields the same fingerprint as a tree of two files, and a symlink's target can imitate the marker that opens the next untracked entry. It also carries **no git-relevant mode**, so `chmod 0644 → 0755` on an untracked file moves neither the fingerprint nor the enumeration — a blind attribute, not a fourth unsupported content class. Regular-file content and symlink targets are deliberately **not** subtracted: they are present in the payload, only ambiguously framed, and refusing them would refuse `new` and `symlink`, which is every delegated change set there is. The real fix — a framed payload carrying mode bits — is queued against that frozen surface, since it revalues every stored fingerprint in the family. A **receipt is forgeable**, exactly like every record in this family, so what the absorb door defends against is a buggy or interrupted producer, not a hostile one; the numerator **over-counts a rename** (a delegate's rename arrives as a deletion plus a creation, and no identity matching is done — that machinery was built and deliberately subtracted), so the metric reads optimistic, except in the rename-then-recreate case, which is indeterminate and named in the producer's own header; and **D10 is a bar, not a mechanism** — at most ONE in-tree exec dispatch runs at a time, and nothing here refuses a second one.
28
50
 
29
- **Invariants:** writer — appends only to the delegation ledger, `<git common dir>/agent-workflow-delegation.jsonl` (`AW_DELEGATION_STORE` overrides, absolute only), through the store's own lock-serialized append; the store's preflight is the single legality door and this mode adds no second validator · every verb that DERIVES anything from the ledger (`return`, `fold`, `degrade`, `aggregate`) replays the store's preflight over the whole file first and refuses at the first record the append path would have refused, naming its physical line — a verb never computes over a "legal prefix" · **never commits**, never runs a subscription CLI, spawns nothing but git READS · the ledger is separate from the review receipts and the flow store, and a line of one family never satisfies a reader of another.
51
+ **Invariants:** writer — appends only to the delegation ledger, `<git common dir>/agent-workflow-delegation.jsonl` (`AW_DELEGATION_STORE` overrides, absolute only), through the store's own lock-serialized append; the store's preflight is the single legality door and this mode adds no second validator · every verb that DERIVES anything from the ledger (`return`, `fold`, `degrade`, `aggregate`) replays the store's preflight over the whole file first and refuses at the first record the append path would have refused, naming its physical line — a verb never computes over a "legal prefix" · **never commits**, never runs a subscription CLI, spawns nothing but git READS — with ONE stated exception: `handoff-return` attests MAIN's index with `git write-tree`, which may write a tree OBJECT into the odb and moves no ref (the same probe `land --prepare` itself uses; the ledger stays the only thing this mode APPENDS to) · the ledger is separate from the review receipts and the flow store, and a line of one family never satisfies a reader of another.
@@ -7,12 +7,15 @@ The **config writer** for `docs/ai/orchestration.json` — the answer to *"set m
7
7
 
8
8
  **Map the user's plain language → explicit ops** (the kit ships no NL parser; it performs no `all`-magic, so you expand scope explicitly, asking when unclear):
9
9
 
10
- | user says (RU/EN) | op | scope |
10
+ The intents are listed in English; a user speaking any other language expresses the same four, and
11
+ mapping their wording onto them is your job, not a shipped phrase list.
12
+
13
+ | user means | op | scope |
11
14
  |---|---|---|
12
- | "оба ревьюят" / "both review" | `--set <activity>.review=council` | **disambiguate**: which activity? If both, pass `--set plan-authoring.review=council --set plan-execution.review=council`. |
13
- | "один ревьюер" / "one reviewer" | `--set <activity>.review=reviewed` | per the named activity, else ask |
14
- | "делегируй исполнение" / "delegate execution" | `--set plan-execution.execute=delegated` | execution only |
15
- | "верни как было / сам" / "revert / do it myself" | `--unset <activity>.<slot>` | the named slot → its computed default |
15
+ | "both review" | `--set <activity>.review=council` | **disambiguate**: which activity? If both, pass `--set plan-authoring.review=council --set plan-execution.review=council`. |
16
+ | "one reviewer" | `--set <activity>.review=reviewed` | per the named activity, else ask |
17
+ | "delegate execution" | `--set plan-execution.execute=delegated` | execution only |
18
+ | "revert / do it myself" | `--unset <activity>.<slot>` | the named slot → its computed default |
16
19
 
17
20
  Run **`node ${CLAUDE_SKILL_DIR}/tools/set-recipe.mjs [--set <activity>.<slot>=<recipe>]… [--unset <activity>.<slot>]… [--write] [--json]`**:
18
21
 
@@ -19,12 +19,12 @@ this as preventing the defect is describing it wrongly.
19
19
  - **The «what I need from you» slot answering "nothing".** A turn that ends is by definition idle, so
20
20
  a resume from the reader IS required; a slot saying otherwise states something untrue. Judged over
21
21
  the slot's **first clause only** — a slot that names a real ask and then, after a `;` or a full
22
- stop, adds «ничего другого не нужно» is honest and passes. A COMMA does not open a new clause, so
23
- the comma-joined version of that sentence is flagged; see the residuals. Hedged forms count: «не
24
- требуется», «ваше присутствие не требуется прямо сейчас», «nothing right now», «n/a».
25
- - **Announce-and-stop.** The «what next» slot promising first-person imminent work («беру…»,
26
- «начинаю…», «I'll start…») while the turn ends. A promise **gated on something named** («после
27
- вашего да беру…», «once CI finishes…») states a dependency instead of a false start, and passes.
22
+ stop, adds «nothing else is needed» is honest and passes. A COMMA does not open a new clause, so
23
+ the comma-joined version of that sentence is flagged; see the residuals. Hedged forms count: «not
24
+ required», «your presence is not required right now», «nothing right now», «n/a».
25
+ - **Announce-and-stop.** The «what next» slot promising first-person imminent work («I take the
26
+ class…», «I'll start…») while the turn ends. A promise **gated on something named** («after your
27
+ yesI publish», «once CI finishes…») states a dependency instead of a false start, and passes.
28
28
 
29
29
  **A message with no recognisable block at all is SILENT by default.** This kit does not mandate the
30
30
  three-part closing block — it is a per-project dialogue contract — so warning whenever a block is
@@ -75,10 +75,13 @@ gets no detection at all**, silently — not a warning on every turn, which is w
75
75
  condition would produce. An EMPTY delivered message is different: it is text, so a turn that ended
76
76
  with no prose is judged as having no block, which `--require-block` will report.
77
77
 
78
- **Language.** The slot labels and both banned sets carry Russian and English twins, because the
79
- contract this enforces was written for a Russian-dialogue deployment. A deployment in another
80
- dialogue language gets no detection until its labels are added that is a real limit, not a
81
- configuration you can set today.
78
+ **Language one vocabulary, English, and no list of others.** The slot labels and every phrase set
79
+ are English. The kit enumerates no second language: a shipped language list is a guess about somebody
80
+ else's dialogue, and it never stops growing. A deployment whose dialogue language is not English
81
+ extends the arrays **in its own placed copy** — the runtime is copied into the project and
82
+ self-contained, so that edit is local and survives an uninstalled kit. Until it is made, such a
83
+ deployment gets **no detection**, silently. That is a real limit, stated rather than hidden, and it is
84
+ not a configuration you can set today.
82
85
 
83
86
  **No writer yet — and why, plainly.** Every other placed thing in this kit arrives through a
84
87
  consent-gated writer. This one does not, deliberately:
@@ -162,28 +165,33 @@ the warning text. It is not a sandbox and not a permission control.
162
165
 
163
166
  - **Detection, not prevention** — restated because it is the one thing that must not blur.
164
167
  - **The judgement is LEXICAL, and that is a layer with limits, not a temporary weakness.** It matches
165
- slot labels and phrase sets on Unicode-aware word boundaries, so an honest «мне нужно ваше
166
- подтверждение» is not read as «не нужно» and an interrogative «не нужно ли…» is not read as a
167
- refusal. It cannot parse a sentence, and it cannot recognise a wording it has never been told
168
- about. Two rules that tried to close an edge here were **deleted rather than tightened a third
169
- time**, because each next version needed a second classifier the residuals below are what
170
- replaced them. A named specimen of the cost: «подтвердите, что ничего не упущено» is a real ask
171
- and is FLAGGED, because the banned word sits inside it. Phrase the ask without the word.
172
- - **A comma-joined qualifier is flagged.** «одно да, ничего другого не нужно» warns; the same
173
- sentence with a `;` or a full stop passes. The rule that tried to exempt the comma form kept
174
- letting a real "nothing" through behind a harmless prefix, so it was removed and the false flag is
175
- accepted instead. It costs one line and names its own fix.
168
+ slot labels and phrase sets on **Unicode-aware** word boundaries the markers are English but the
169
+ message is the project's dialogue language, which need not be ASCII, and JavaScript's `\b` would
170
+ read every non-ASCII letter as a word break and match a marker sitting inside a longer word. On the
171
+ English side the same class is what stops «none» matching inside «nonexistent». It cannot parse a
172
+ sentence, and it cannot recognise a wording it has never been told about. Two rules that tried to
173
+ close an edge here were **deleted rather than tightened a third time**, because each next version
174
+ needed a second classifier the residuals below are what replaced them. A named specimen of the
175
+ cost: «confirm that nothing was missed» is a real ask and is FLAGGED, because the banned word sits
176
+ inside it. Phrase the ask without the word.
177
+ - **A comma-joined qualifier is flagged.** «one yes, nothing else is needed» warns; the same sentence
178
+ with a `;` or a full stop passes. The rule that tried to exempt the comma form kept letting a real
179
+ "nothing" through behind a harmless prefix, so it was removed and the false flag is accepted
180
+ instead. It costs one line and names its own fix.
176
181
  - **A condition is bound to a promise by TOKEN ORDER inside one segment, which is an approximation.**
177
- «после вашего даберу класс» passes; «беру класс, а если тест упадёт, сообщу» is flagged. Two
178
- known misreadings follow from the approximation, both accepted: an honest TRAILING gate («беру,
179
- когда вы скажете») is flagged, and a gate belonging to an earlier comma-clause («если тест упадёт,
180
- сообщу, а сейчас начинаю…») wrongly excuses the promise after it. Leading with the gate avoids the
181
- first; the second is a miss this layer cannot close without parsing.
182
- - **The English side is weaker than the Russian side, structurally.** Russian promises are action
183
- verbs («беру», «начинаю»); English ones are pronoun+modal («I'll», «I will»), which cannot tell
184
- starting from waiting. Waiting is explicitly excluded «I'll wait for your approval» passes,
185
- because waiting is what a turn that ends actually does but the exclusion is a list, and an
186
- unusual way of saying "I am waiting" will be flagged.
182
+ «after your yesI take the class» passes; «I take the class, and if the test fails I'll report»
183
+ is flagged. Two known misreadings follow from the approximation, both accepted: an honest TRAILING
184
+ gate («I take it when you say so») is flagged, and a gate belonging to an earlier comma-clause («if
185
+ the test fails I'll report, and now I start the next class») wrongly excuses the promise after it.
186
+ Leading with the gate avoids the first; the second is a miss this layer cannot close without
187
+ parsing.
188
+ - **The promise markers are a MIXED set, and each half misses differently.** Some are pronoun+modal
189
+ («I'll», «I will», «I'm going to»), which name no verb of their own and so cannot tell starting
190
+ from waiting; waiting is therefore excluded by an explicit list«I'll wait for your approval»
191
+ passes, because waiting is what a turn that ends actually does — and an unusual way of saying "I am
192
+ waiting" is flagged. The others are pronoun+verb («I take», «I start», «I begin», «I move on»),
193
+ which name the action but only in the exact wording listed: a synonym nobody wrote down is missed
194
+ entirely, and a stative use of a listed verb («I take that as settled») is a false flag.
187
195
  - **A host that does not deliver `last_assistant_message` gets no detection**, silently. There is no
188
196
  transcript fallback, on purpose — see above.
189
197
  - **Without `--require-block`, a turn that drops the block entirely is not detected.** That is the
@@ -22,7 +22,16 @@ own verbatim error through the existing Git-error surface.
22
22
  `node_modules` symlink when main has one and the link stays ignored — a SHARED MUTABLE cache:
23
23
  writes through it hit MAIN's node_modules; for isolation RUN the printed isolated-install
24
24
  command (`--install` only prints it), and on `--resume` an existing symlink is kept — run the
25
- printed unlink-first recovery first. Absolute root-pinned gate commands are rebased on UNTRACKED copies only and only
25
+ printed unlink-first recovery first. That recovery is printed ONLY for a link this tool can prove
26
+ it placed — BOTH halves of the ownership rule below: raw target bytes equal to MAIN's
27
+ `node_modules`, AND the ignored lane. Every other state is reported in its own words and advises no
28
+ removal, because deleting a node whose provenance was never established is not a recovery: a
29
+ FOREIGN target (read, and not equal to that path — which says where it points, never who placed
30
+ it), a TRACKED path (matching target, but repository content the landing lane protects), a
31
+ LANE-UNPROVEN path (matching target, but the ignore probe did not settle, carrying the probe's own
32
+ cause), and an UNREADABLE link (the target read itself failed, carrying its errno). The same five
33
+ verdicts render the same way in `provision`'s report and in the `prompt` output, so the two can
34
+ never describe one link differently. Absolute root-pinned gate commands are rebased on UNTRACKED copies only — and only
26
35
  while their bytes still equal the MAIN source (or its rebased form); user-modified copies stay
27
36
  byte-untouched. `--install` only PRINTS the install command — zero spawn, zero write.
28
37
  `--resume <slug>` completes a half-done provision (identity fail-closed; handoff user sections,
@@ -97,6 +106,31 @@ own verbatim error through the existing Git-error surface.
97
106
  symlinked docs/plans (or ancestor), a handoff-named entry that is not a regular file, a
98
107
  vanished worktree dir, or any other read failure renders `handoff: (unreadable)` — never a
99
108
  silent "no".
109
+ - `prompt <slug>` — read-only: re-print the satellite's cold-start prompt, the same text
110
+ `provision` ends its report with. It carries the worktree path and branch, the ONE seeded plan,
111
+ the handoff as the one return channel, MAIN's orientation (the shared series index and the landing)
112
+ and — under its own heading, because it is probed on the SATELLITE and not in MAIN — that
113
+ checkout's install posture. Every value is derived LIVE, never replayed from the provision record:
114
+ the record FREEZES them at provision time, so a moved MAIN or a hand-edited field would otherwise
115
+ put a stale runnable command in front of a satellite. A recorded value that no longer matches is
116
+ NAMED as a divergence beside the live one, with the cause its own source makes likely (a moved MAIN
117
+ or a hand edit for the MAIN-derived pair; a changed dependency declaration for the install
118
+ posture), and a field an earlier kit never wrote is no divergence at all. Every command the prompt
119
+ offers is a marked line naming WHO runs it: `MAIN $ …` for the landing, `HERE $ …` for this
120
+ checkout's own install — including the removal of a provisioned `node_modules` symlink, which is
121
+ runnable even where the install command itself is not derivable. A posture with nothing to run
122
+ stays prose, and the recorded posture string is never printed loose: it IS a command in the
123
+ ordinary case, and an unattributed one reads as an instruction. A `node_modules` symlink is
124
+ claimed to point into MAIN only when its RAW TARGET says so — the same evidence cleanup binds
125
+ ownership on; any other target reads as a link this tool did not place, with nothing claimed about
126
+ it and no removal advised. A control character in any value it would render is a
127
+ typed STOP, not a sanitized string: the prompt is line-oriented, so a newline in a filename would
128
+ otherwise forge a line — including a line imitating a command. Run it from MAIN — a cwd inside a
129
+ linked worktree is the same refusal `provision`/`land`/`cleanup` make. It resolves the satellite
130
+ through the handoff identity (the duplicate-identity, missing-record and slug/branch-mismatch STOPs
131
+ are the same ones `cleanup` binds on), and it writes nothing.
132
+ One writer per worktree is a BAR the prompt states and nothing enforces:
133
+ ONE writer per worktree: this session is the only agent writing in this checkout, and nothing enforces that — a second session writing here interleaves two agents into one tree, which neither this tool nor git can detect or undo. It is a bar you keep, not a lock you hold.
100
134
  - `land <slug> --prepare` — stage the satellite's finished diff onto a CLEAN main; the commit is
101
135
  NEVER run by the tool — it stays a dialogue ask at the primary session. Land takes the transient
102
136
  common-git-dir lock, refuses a dirty main, graph divergence, visible `docs/ai` drift, excluded
@@ -133,7 +167,11 @@ own verbatim error through the existing Git-error surface.
133
167
  **Provision record (`docs/plans/handoff-<slug>.md`, `## Provision record` — tool-owned):** resume
134
168
  IDENTITY (`slug`, `branch`, and the seeded plan name — a mismatch STOPs) · recorded provision FACTS
135
169
  that never authorize a resume (`include`, `node_modules`, `vscode-settings`) · and, after a prepare,
136
- `prepared-tree`, which is a land/cleanup attestation-and-recovery surface, not resume identity
170
+ `prepared-tree` plus `prepared-head` (MAIN's HEAD at prepare time what lets the handoff-return
171
+ rung tell a still-pending prepared set from an already-committed one, since a clean post-commit
172
+ index reproduces the committed tree; a record from an earlier kit simply lacks it and the rung
173
+ refuses by name), a land/cleanup/handoff-return attestation-and-recovery surface, not resume
174
+ identity —
137
175
  PLUS the three facts a fresh satellite session cannot derive from its own checkout:
138
176
 
139
177
  - `shared-queue` — the ABSOLUTE path to MAIN's `docs/plans/queue.md`, followed by the rule the record states verbatim: the series index is SHARED and lives ONLY in main: read it at the absolute path above, and never copy it into this worktree, because docs/plans is git-ignored and machine-local, so a copy silently diverges from what main and every other worktree are writing. This worktree never WRITES that file: reaching outside it is an fs_outside_repo action the autonomy policy denies by default. Put new findings in THIS handoff record instead — it is the channel that survives the landing, and main appends them to the index from here. Provision never seeds a copy: the queue is deliberately absent from the satellite, and the absolute path is the only pointer — `--include` refuses to copy the index (or any directory containing it) into the worktree.
@@ -173,7 +211,13 @@ preview prints the exact `--apply`). Neither is proof of write
173
211
  capability: the create+delete probe above stays the runtime truth, and the ack binds to the
174
212
  resolved probe dir, so the item re-fires only when that resolved dir changes.
175
213
 
176
- **Landing flow:** provision → work → handoff → land → re-attestcommitcleanup. Satellite
214
+ **Landing flow:** provision → work → handoff → land --prepare handoff-returnfoldre-stage
215
+ configured review → run-gates --final → commit-guard --check → commit ask → cleanup. The
216
+ `dispatch handoff-return` rung (see the dispatch mode) delivers the handoff's user-owned content
217
+ byte verbatim, re-attests the prepared pair against MAIN, and records the worktree-stream
218
+ observation where the change set allows one; what follows it is one order, stated exactly:
219
+ a fold landed AFTER the gates leaves those gates STALE — the order is fold → re-stage (git add) → the configured review → run-gates --final (the receipt is minted over the CURRENT post-fold staged tree) → commit-guard --check (the final re-attestation) → the commit ask.
220
+ Satellite
177
221
  commits are outside v1: graph divergence stops land and prints cherry-pick/rebase recovery. A gate
178
222
  failure keeps the prepared main tree and names both recovery lanes. A second prepare is reset-only:
179
223
  the STOP prints the current staged write-tree, compares it with the PREPARED OID recorded in the
@@ -1,6 +1,6 @@
1
1
  // check-docs-size-cli.test.mjs — runCli branch pins the subprocess smokes cannot reach
2
2
  // in-process (Phase-5 coverage fill; the main spec file is parity-frozen, so these ride a
3
- // colocated file): the unknown-argument refusal and the written-empty-index guard.
3
+ // colocated file): the unknown-argument refusal and the pre-write symlink refusal on the index path.
4
4
  import { describe, it } from 'node:test';
5
5
  import assert from 'node:assert/strict';
6
6
  import { mkdtempSync, mkdirSync, writeFileSync, symlinkSync, rmSync } from 'node:fs';
@@ -20,7 +20,7 @@ describe('check-docs-size runCli — refusal branches', () => {
20
20
  assert.match(stderr, /Unknown argument: --bogus/);
21
21
  });
22
22
 
23
- it('--write-index landing on a sink path (index stat size 0) is the loud written-empty refusal', async () => {
23
+ it('--write-index refuses a symlinked index path BEFORE writing, naming the path', async () => {
24
24
  const root = mkdtempSync(join(tmpdir(), 'cds-cli-'));
25
25
  try {
26
26
  mkdirSync(join(root, 'docs', 'ai'), { recursive: true });
@@ -51,8 +51,8 @@ the project's internal `docs/ai` structure version, the stamp filename, or the i
51
51
  vocabulary — that number is inert here and only confuses; it belongs to *Version disclosure* (below).
52
52
  Frame the success itself plainly, in the **user's conversational language** (never hardcode a phrase):
53
53
  - a **zero-diff no-op `upgrade`** (step 4) → **settings already current — no update is required**
54
- (illustrative tone for a Russian-speaking user, an example of the meaning, not a literal string to
55
- embed: *«Настройки уже актуальны — обновление не требуется»*);
54
+ (that is the MEANING to convey, not a literal string to embed say it in the user's conversational
55
+ language, in your own words);
56
56
  - a **fresh `bootstrap`** → its normal "deployed and ready" success, minus the number.
57
57
 
58
58
  **Version block — the installed package versions, fed from `--json`** (the `docs/ai` structure version
@@ -71,6 +71,7 @@ Apply this as part of §2 before any user-facing summary:
71
71
  - **No condescension, no filler.** Own a miss plainly and fix it in the same message.
72
72
  - **Large artifact (≈>100 lines):** deliver a real summary or the key excerpt inline **and** link the file — never flood the reader with a 2000-line paste, never hide the answer behind a bare pointer.
73
73
  - **Live host/session facts are tool-composed only.** Any claim about the current host or session state (prompts fired, sandbox scope, whether a bypass was needed, network reachability, approval counts) must trace to **live tool output** from **this session**; a memory/handover snapshot is **context, never report facts**, and a claim with no live signal is **omitted or explicitly marked unverified** — never asserted from recollection.
74
+ - **The closing state block answers three DIFFERENT questions.** Close a user-facing message with three labelled slots — *now* · *what I need from you* · *what's next*. The slot LABELS stay ENGLISH — an English label is what lets a state-block checker FIND the block and its slots at all; everything written INTO a slot is in the project's dialogue language; when that language is not English, the checker's English phrase sets do not judge those values. **Now** = the state at this instant: what is RUNNING, or what the work is stopped on. It is **never a report of finished work** — what you completed goes in the message BODY, above the block. **From you** = the real unblocker, named; a turn that is ENDING always has one. **Next** = what follows. A *now* slot that opens with what was completed buries the one fact the reader opened the message for, and the three slots collapse into one restatement.
74
75
 
75
76
  ### 2.6. Planning, review & process-fidelity invariants
76
77
  Apply these when authoring a plan, reviewing, folding a finding, or editing code — the layer read **before any code change**. (Full canon: the project's planning / workflow-methodology + orchestration canon. This section is rendered from that canon and refreshed on upgrade; a custom edit is preserved verbatim, but flagged.)
@@ -0,0 +1,165 @@
1
+ // advisor-matrix.mjs — the advisor-matrix STRUCTURE check (delegation Plan 3, Phase 1), as a leaf.
2
+ //
3
+ // A doc-parity BINDING proves a token is somewhere in a file. Correspondence is a different claim,
4
+ // and it is the one this table needs: the dispatch mode doc's routing matrix must carry one row per
5
+ // registry row, in registry order, with every CELL equal. A reorder, a duplicate, a dropped row, a
6
+ // mis-bound vehicle and a drifted availability or returns cell all leave every token present — so a
7
+ // token check passes every one of them, which is exactly why this exists beside the bindings rather
8
+ // than as more of them.
9
+ //
10
+ // Its own module rather than more lines in doc-parity.mjs: the lint's identity is "a closed registry
11
+ // of value bindings plus the runner over them", and a table parser with its own refusal vocabulary is
12
+ // a second thing. Split, each is a file you can hold whole — and the parser gets its own test file.
13
+ //
14
+ // Read-only: never writes, never commits, spawns nothing. Node built-ins plus the advisor registry
15
+ // only. No side effects on import; no CLI (it is reached through doc-parity).
16
+
17
+ import { readFileSync } from 'node:fs';
18
+ import { dirname, resolve } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { ADVISOR_ROWS, ADVISOR_MATRIX_HEADER, ADVISOR_MATRIX_COLUMNS, renderAdvisorMatrix } from './dispatch-advisor.mjs';
21
+
22
+ const KIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
23
+
24
+ export const ADVISOR_MATRIX_DOC = 'references/modes/dispatch.md';
25
+
26
+ // The table is found through an ANCHORED surface, not by "the first header line anywhere". Header
27
+ // search alone is maskable in a way even an exactly-one rule does not close: a faithful copy of the
28
+ // table plus a canonical one whose HEADER drifted leaves exactly one matching header — the decoy's —
29
+ // and the check then reads the decoy and passes. The markers make the checked surface a property of
30
+ // the DOC, so a copy outside them can neither stand in for the table nor hide its drift, and a
31
+ // drifted header INSIDE them leaves the surface with zero headers and fails closed.
32
+ export const ADVISOR_MATRIX_BEGIN = '<!-- advisor-matrix:begin -->';
33
+ export const ADVISOR_MATRIX_END = '<!-- advisor-matrix:end -->';
34
+
35
+ export const readKitDoc = (rel) => readFileSync(resolve(KIT_ROOT, rel), 'utf8');
36
+
37
+ // Both line endings are ordinary here. Splitting on '\n' alone leaves a trailing '\r' on every line
38
+ // of a CRLF-authored doc, and the marker match survives it (it trims) while the exact header match
39
+ // does not — so the check would fail a CORRECT document while naming a drifted header. One split,
40
+ // before anything compares.
41
+ const splitLines = (text) => String(text).split(/\r?\n/);
42
+
43
+ const linesMatching = (lines, marker) => lines.flatMap((line, i) => (line.trim() === marker ? [i] : []));
44
+
45
+ // Only leading and trailing BLANK lines are dropped. Trimming the joined block with String.trim()
46
+ // would also eat significant edge whitespace INSIDE the first and last lines, which is drift the
47
+ // comparison is supposed to see.
48
+ const trimBlankEdges = (lines) => {
49
+ let start = 0;
50
+ let end = lines.length;
51
+ while (start < end && lines[start].trim() === '') start += 1;
52
+ while (end > start && lines[end - 1].trim() === '') end -= 1;
53
+ return lines.slice(start, end);
54
+ };
55
+
56
+ // parseAdvisorMatrix(text) → { ok: true, lines, rows } | { ok: false, reason }. `lines` is the whole
57
+ // anchored block; `rows` are its class rows, parsed for the DIAGNOSIS only — the verdict is the
58
+ // whole-block comparison in checkMatrixStructure, so a deleted alignment rule, a rewritten harness
59
+ // lane and an extra row are all caught, none of which a class-row walk would ever see.
60
+ export const parseAdvisorMatrix = (text) => {
61
+ const lines = splitLines(text);
62
+ const begins = linesMatching(lines, ADVISOR_MATRIX_BEGIN);
63
+ const ends = linesMatching(lines, ADVISOR_MATRIX_END);
64
+ if (begins.length !== 1 || ends.length !== 1) {
65
+ return { ok: false, reason: `the anchored matrix surface is not unique — found ${begins.length} "${ADVISOR_MATRIX_BEGIN}" and ${ends.length} "${ADVISOR_MATRIX_END}" (exactly one of each is required)` };
66
+ }
67
+ if (ends[0] < begins[0]) {
68
+ return { ok: false, reason: 'the matrix end marker precedes its begin marker — the anchored surface is inverted' };
69
+ }
70
+ const surface = trimBlankEdges(lines.slice(begins[0] + 1, ends[0]));
71
+ const headers = surface.filter((line) => line === ADVISOR_MATRIX_HEADER);
72
+ if (headers.length !== 1) {
73
+ return { ok: false, reason: `the anchored matrix surface carries ${headers.length} header line(s) equal to "${ADVISOR_MATRIX_HEADER}" — exactly one is required` };
74
+ }
75
+ const rows = [];
76
+ for (const line of surface.slice(surface.indexOf(ADVISOR_MATRIX_HEADER) + 1)) {
77
+ if (!line.startsWith('|')) continue;
78
+ const cells = line.split('|').slice(1, -1).map((c) => c.trim());
79
+ // Arity refuses OUTRIGHT rather than skipping the row: a row whose cell count disagrees with the
80
+ // header's is malformed whatever it says, and skipping it would report the drift as a MISSING
81
+ // class row — a true verdict reached through a misleading sentence.
82
+ if (cells.length !== ADVISOR_MATRIX_COLUMNS.length) {
83
+ return { ok: false, reason: `a matrix row carries ${cells.length} cell(s), the table has ${ADVISOR_MATRIX_COLUMNS.length} columns: ${line.trim()}` };
84
+ }
85
+ const classCell = /^`(.+)`$/.exec(cells[0]);
86
+ // A row whose first cell is not a backticked class joins no CLASS comparison — the harness lane,
87
+ // or an interloper. Neither escapes: the whole-block equality below sees every line.
88
+ if (classCell === null) continue;
89
+ rows.push(Object.fromEntries(ADVISOR_MATRIX_COLUMNS.map(({ key }, i) => [key, i === 0 ? classCell[1] : cells[i]])));
90
+ }
91
+ return { ok: true, lines: surface, rows };
92
+ };
93
+
94
+ const quoted = (classes) => classes.map((c) => `\`${c}\``).join(', ');
95
+
96
+ // The DIAGNOSIS over the class rows, and the ORDER of its questions is the point. A POSITIONAL walk
97
+ // reads a deleted middle row as a corrupted step-class cell in the row that slid up behind it —
98
+ // technically a difference at that index, and a useless pointer for whoever has to fix the doc. So
99
+ // membership is settled first (duplicated / missing / unregistered), then ORDER, and only over rows
100
+ // that agree on both does a cell comparison run — where the first differing CELL is named, because
101
+ // "row 3 disagrees" leaves the reader to diff four columns by eye.
102
+ const rowDrift = (actual, expected) => {
103
+ const actualClasses = actual.map((r) => r.stepClass);
104
+ const expectedClasses = expected.map((r) => r.stepClass);
105
+
106
+ const duplicated = actualClasses.filter((c, i) => actualClasses.indexOf(c) !== i);
107
+ if (duplicated.length > 0) return `the advisor matrix names ${quoted([...new Set(duplicated)])} more than once — the registry has exactly one row per step class`;
108
+
109
+ const missing = expectedClasses.filter((c) => !actualClasses.includes(c));
110
+ if (missing.length > 0) return `the advisor matrix is missing ${missing.length} registry row(s): ${quoted(missing)}`;
111
+
112
+ const unregistered = actualClasses.filter((c) => !expectedClasses.includes(c));
113
+ if (unregistered.length > 0) return `the advisor matrix names ${unregistered.length} row(s) the registry does not: ${quoted(unregistered)}`;
114
+
115
+ const outOfOrder = actualClasses.findIndex((c, i) => c !== expectedClasses[i]);
116
+ if (outOfOrder !== -1) return `the advisor matrix is out of registry order — row ${outOfOrder + 1} is \`${actualClasses[outOfOrder]}\`, the registry has \`${expectedClasses[outOfOrder]}\``;
117
+
118
+ for (const [i, row] of actual.entries()) {
119
+ const e = expected[i];
120
+ const differing = ADVISOR_MATRIX_COLUMNS.find(({ key }) => row[key] !== e[key]);
121
+ if (differing !== undefined) {
122
+ return `matrix row ${i + 1} (\`${row.stepClass}\`): the ${differing.label} cell reads "${row[differing.key]}", the registry has "${e[differing.key]}"`;
123
+ }
124
+ }
125
+ return null;
126
+ };
127
+
128
+ // blockDrift(actual, expected) → null when the two blocks are IDENTICAL, else the first line that
129
+ // disagrees. It is the verdict and the fallback diagnosis in one: "the blocks are equal" is exactly
130
+ // "no line disagrees", so there is no second comparison to keep in step with this one — and the
131
+ // null return is the path every green run takes, not an unreachable defensive branch.
132
+ const blockDrift = (actual, expected) => {
133
+ for (let i = 0; i < Math.max(actual.length, expected.length); i += 1) {
134
+ if (actual[i] === expected[i]) continue;
135
+ if (actual[i] === undefined) return `the anchored matrix is missing line ${i + 1}, which the canonical table renders as ${JSON.stringify(expected[i])}`;
136
+ if (expected[i] === undefined) return `the anchored matrix carries an extra line ${i + 1}: ${JSON.stringify(actual[i])}`;
137
+ return `matrix line ${i + 1} reads ${JSON.stringify(actual[i])}, the canonical table renders ${JSON.stringify(expected[i])}`;
138
+ }
139
+ return null;
140
+ };
141
+
142
+ // checkMatrixStructure(readText) → the same shape a doc-parity binding result carries, so the report,
143
+ // the --check line and the --json payload all render it through their existing paths. The VERDICT is
144
+ // whole-block equality against the canonical render: every way the doc's table can stop being the
145
+ // registry's table is one comparison, and the enumeration of those ways never has to be maintained.
146
+ export const checkMatrixStructure = (readText = readKitDoc) => {
147
+ const rel = ADVISOR_MATRIX_DOC;
148
+ const expected = ADVISOR_ROWS.map(({ stepClass, vehicle, availabilityNote, returns }) => ({ stepClass, vehicle, availabilityNote, returns }));
149
+ let text;
150
+ try {
151
+ text = readText(rel);
152
+ } catch (err) {
153
+ return { constant: 'advisor-matrix-structure', files: [{ rel, ok: false, reason: `unreadable (${(err && err.code) || (err && err.message) || 'read failed'})` }], ok: false };
154
+ }
155
+ const parsed = parseAdvisorMatrix(text);
156
+ if (parsed.ok === false) {
157
+ return { constant: 'advisor-matrix-structure', files: [{ rel, ok: false, reason: parsed.reason }], ok: false };
158
+ }
159
+ // The VERDICT is the block comparison; the row walk only refines the MESSAGE when it can point at a
160
+ // class row. A block difference the row walk cannot explain (the alignment rule, the harness lane,
161
+ // an interloping row, whitespace) keeps the line-level pointer.
162
+ const drift = blockDrift(parsed.lines, splitLines(renderAdvisorMatrix()));
163
+ const reason = drift === null ? null : (rowDrift(parsed.rows, expected) ?? drift);
164
+ return { constant: 'advisor-matrix-structure', files: [{ rel, ok: reason === null, reason }], ok: reason === null };
165
+ };
@@ -264,7 +264,7 @@ const CATALOG = [
264
264
  invocation: invocationOf('dispatch'),
265
265
  group: 'Orchestrate',
266
266
  kind: WRITER,
267
- oneLine: 'Measure delegation: check a sub-task brief’s contract block (form only — never whether the task is genuinely bounded), pre-register an acceptance wave with its thresholds, record one observation, open a delegated thread from that brief, wait for that one dispatch to answer — a wait that ends without an answer says so and authorizes nothing — absorb the wrapper’s receipt back into the ledger, fold the returned work or close the thread with a recorded degrade, and print the per-class report of how much a delegated sub-task actually bought, derived from what was dispatched, returned and folded. Writes only its own ledger file beside the repo; never commits.',
267
+ oneLine: 'Measure delegation: check a sub-task brief’s contract block (form only — never whether the task is genuinely bounded), ask which vehicle should carry that kind of sub-task on THIS machine and what past threads of that kind actually did — advice you may ignore, never a gate, printed on its own and again under a valid contract check, pre-register an acceptance wave with its thresholds, record one observation, open a delegated thread from that brief, wait for that one dispatch to answer — a wait that ends without an answer says so and authorizes nothing — absorb the wrapper’s receipt back into the ledger, fold the returned work or close the thread with a recorded degrade, and print the per-class report of how much a delegated sub-task actually bought, derived from what was dispatched, returned and folded. After a landing, deliver the handoff verbatim and count only what is fully measurable. Writes only its own ledger file beside the repo; never commits.',
268
268
  },
269
269
  {
270
270
  // NEVER `guarded` — that kind promises dry-run-first, which these writers do not have; the
@@ -273,7 +273,7 @@ const CATALOG = [
273
273
  invocation: invocationOf('worktrees'),
274
274
  group: 'Orchestrate',
275
275
  kind: WRITER,
276
- oneLine: 'Run features in parallel git worktrees: provision an isolated sibling copy, list them, stage a finished one back onto clean main (the commit still asks in dialogue), and remove a live-verified landed one. No preview step; list is read-only; cleanup --abandon destroys unlanded work.',
276
+ oneLine: 'Run features in parallel git worktrees: provision an isolated sibling copy, list them, print the cold-start prompt a fresh session in one of them needs — where it is, what MAIN answers NOW rather than what the record froze, the handoff as the one way back, and the bars nothing enforces — stage a finished one back onto clean main (the commit still asks in dialogue), and remove a live-verified landed one. No preview step; list is read-only and so is prompt; cleanup --abandon destroys unlanded work.',
277
277
  },
278
278
  ];
279
279