@sabaiway/agent-workflow-kit 6.0.0 → 7.1.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 (36) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +1 -0
  3. package/SKILL.md +5 -1
  4. package/bridges/antigravity-cli-bridge/bin/agy-review-await-guard.test.mjs +176 -0
  5. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +61 -14
  6. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +606 -467
  7. package/bridges/antigravity-cli-bridge/references/review-prompt.md +42 -4
  8. package/bridges/codex-cli-bridge/SKILL.md +18 -5
  9. package/bridges/codex-cli-bridge/bin/codex-await-guard.test.mjs +161 -0
  10. package/bridges/codex-cli-bridge/bin/codex-exec.sh +22 -17
  11. package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +356 -363
  12. package/bridges/codex-cli-bridge/bin/codex-review.sh +6 -6
  13. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +275 -286
  14. package/bridges/codex-cli-bridge/capability.json +1 -1
  15. package/bridges/codex-cli-bridge/references/driving-codex.md +4 -2
  16. package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +3 -2
  17. package/bridges/codex-cli-bridge/setup/README.md +3 -1
  18. package/capability.json +1 -1
  19. package/package.json +1 -1
  20. package/references/hooks/gate-approve.mjs +1 -1
  21. package/references/modes/mcp.md +37 -0
  22. package/references/modes/recommendations.md +1 -0
  23. package/references/modes/uninstall.md +2 -1
  24. package/references/templates/agent_rules.md +1 -0
  25. package/tools/commands.mjs +7 -0
  26. package/tools/direct-run.mjs +3 -0
  27. package/tools/doc-parity.mjs +18 -2
  28. package/tools/fold-scope-cli.mjs +93 -0
  29. package/tools/fold-scope.mjs +307 -0
  30. package/tools/mcp-registration.mjs +283 -0
  31. package/tools/mcp-server.mjs +314 -0
  32. package/tools/mcp-stdio.mjs +229 -0
  33. package/tools/mcp.mjs +299 -0
  34. package/tools/procedures.mjs +29 -4
  35. package/tools/recommendations.mjs +90 -1
  36. package/tools/uninstall.mjs +356 -45
package/CHANGELOG.md CHANGED
@@ -4,6 +4,108 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
4
4
  is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
5
5
  every `migrations/<version>-<slug>.md` newer than it, in semver order.
6
6
 
7
+ ## 7.1.0 — `fold-scope`: the fold channel gets a checker that refuses a claim whose reference does not resolve (AD-110)
8
+
9
+ The engine canon now asks every finding to NAME the invariant its fix enforces before the edit
10
+ (engine **3.1.0**). Prose cannot check that a named invariant is really where you say it is.
11
+
12
+ - **`tools/fold-scope.mjs` + `tools/fold-scope-cli.mjs`** — the pure rule and its CLI half:
13
+ `--class in-scope|new-invariant|blocking`, `--claim "<the invariant>"`, `--plan <plan-file>`,
14
+ `--queue <queue-file>`. Exit 0 for every ACCEPT, non-zero for every REFUSE, one case per arm.
15
+ - **`--plan` and `--queue` are REQUIRED and never defaulted.** A project can have two registers that
16
+ both look like "the queue" — the planning lifecycle's `docs/plans/queue.md` and a declared
17
+ `flow.debtQueue` — and a checker that guessed would attest a deferral against the wrong file.
18
+ - **What it refuses**, each naming the lane out of it: an `in-scope` claim matching no acceptance
19
+ bullet; a `new-invariant` whose invariant IS an acceptance bullet (route to fold); a deferral with
20
+ no queue row, with several, whose row title carries `DONE`/`CLOSED`, that repeats a field label,
21
+ that is missing a field (named, including an `origin` that is not a `file:line`), whose claim is
22
+ not inside the row's `invariant` field, or whose residual exposure is declared live (route to
23
+ blocking) or declares neither.
24
+ - **`/agent-workflow-kit procedures plan-execution` renders the populated command** — under solo,
25
+ reviewed AND council, because the rule routes every finding and a Solo project must see the checker
26
+ too — and NAMES which of the two registers it chose for `--queue`. Every PATH operand goes through
27
+ the family's shell quoter (bare when the value is already safe, single-quoted when it is not), and
28
+ the two placeholders are single-quoted literals so their angle brackets and pipe are inert — the
29
+ line is safe to paste as rendered. `--json` gains an additive `foldScope` array.
30
+ - **Markdown is read through `references/scripts/markdown-blocks.mjs`**, the block model the
31
+ archivers already use, so fenced examples, heading levels and CRLF are solved once for the family;
32
+ a document that model refuses is a loud refusal, never a silent empty read.
33
+
34
+ **Advisory by design:** nothing records that the checker ran, so a skipped or late call is
35
+ indistinguishable from a pre-edit declaration. A fingerprint-bound receipt a gate reads is the next
36
+ slice. Additive throughout — no existing command, flag, output field or exit code changed.
37
+
38
+ ## 7.0.0 — `/agent-workflow-kit mcp` registers the typed channel in ONE project, and `uninstall` stops reporting an interrupted teardown as a success (AD-108)
39
+
40
+ > ### ⚠ BREAKING — an `uninstall` run that silently passed can now refuse, and its plan reports more
41
+ >
42
+ > **The outcome flips for the same filesystem situation.** When a mutable surface changed between the
43
+ > preflight and its own removal, `executePlan` passed over it and returned `{ applied: true }` — a
44
+ > teardown that had not finished, reported as one that had. It now raises a typed `UNINSTALL_STOP`
45
+ > whose message opens `the teardown is INCOMPLETE` and lists, separately, what was refused before it
46
+ > was touched, what may be **partially** removed, and what had already been applied. There is no
47
+ > alias and no deprecation window, which is the same shape this package twice sized as MAJOR —
48
+ > **4.0.0** (a receipt that used to be accepted is refused) and **5.0.0** (a `--check` that silently
49
+ > passed can now refuse); **5.6.0** stayed MINOR only because it kept the old field as an alias.
50
+ >
51
+ > **And it is not only the race.** `uninstall` previously read *through* a symlinked `.claude`, a
52
+ > symlinked `settings.json` and a non-regular pre-commit hook without saying so; each is now a
53
+ > REPORT_ONLY item read no further. For a project in any of those states the printed plan contains
54
+ > **more items than before** — and for a CLI the report is public surface.
55
+ >
56
+ > **Upgrading:** nothing to change in a normal teardown, which is unaffected. `uninstall` is complete
57
+ > **only on exit 0** — an `INCOMPLETE` stop is never a finished run. If you script it, branch on the
58
+ > two lists rather than on the word, because they call for opposite actions:
59
+ >
60
+ > - **left untouched** — the removal was refused *before* it touched anything (containment, or a
61
+ > surface that stopped being provably ours). Nothing was changed by it: settle the tree and re-run.
62
+ > - **may be partially removed** — a removal failed *part way* and the tool cannot tell what survived.
63
+ > **Do not re-run blind and do not proceed to a next step.** Inspect the named path by hand, decide
64
+ > what is left, and only then continue. The run stops at the first of these on purpose, so anything
65
+ > after it in the plan was not attempted either.
66
+
67
+ **The server shipped, and no project could see it.** 6.0.0 bundled a stdio MCP server exposing the
68
+ two promptless readers — `path_inventory` and `repo_search` — as typed tools whose arguments are
69
+ named JSON fields rather than a string handed to a shell. Shipping it did nothing on its own: a
70
+ client sees a project-scoped server only once the project **declares** it. This release is that
71
+ declaration.
72
+
73
+ `/agent-workflow-kit mcp` (guarded, preview-first) writes exactly two files: the `agent-workflow`
74
+ stdio entry in **`.mcp.json`** at the project root, and `enabledMcpjsonServers` plus the two allow
75
+ rules **derived from the server's own name and tool list** in `.claude/settings.json`. The exact
76
+ entry is printed **before** consent is asked — registering a server means your client will run that
77
+ command — and `--apply` writes `.mcp.json` first, then settings, merge-don't-clobber with each
78
+ file's EOL kept. Where an OS sandbox masks `.mcp.json` with a device node it writes nothing and
79
+ hands over **the entry alone** to merge, keeping every server it cannot see through the mask; a
80
+ whole-file body would have deleted them. An existing `agent-workflow` entry that **structurally
81
+ differs** (key order ignored) is refused unwritten.
82
+
83
+ **A veto check was built, worked, and then subtracted.** `disabledMcpjsonServers` rejects a server
84
+ in every mode, so a project can hold the entry, the enable and both rules and still have a dark
85
+ channel. Honouring it means reading that key from every settings scope the client merges, and each
86
+ scope has its own masked, symlinked, malformed and unreadable states in which a hidden deny still
87
+ yields a confident answer — three review rounds each closed one such hole and opened the next. **A
88
+ check that is wrong in states it cannot enumerate is worse than a stated limit**, so the limit is
89
+ now stated by name in the mode contract and pinned by tests: `registered` means *what this mode
90
+ writes is in place*, never *the client will load it*.
91
+
92
+ **`uninstall` learned that containment is the whole parent chain.** Guarding one named container is
93
+ a ladder with no top — close `.claude` and the next symlink moves to `.claude/hooks`, then to
94
+ `.git` — so the teardown now walks from the project root before every read and every removal, with
95
+ the project as the containment root. A surface that changes after the preflight is no longer passed
96
+ over under a successful report: a removal **refused before it touched anything** reads *left
97
+ untouched*, one that **failed part way** reads *possibly partially removed* and stops the run, and
98
+ both are fields of one typed INCOMPLETE outcome.
99
+
100
+ Also: the promptless-lane hint the gate hook delivers now names the typed tools before the CLI
101
+ lanes; `uninstall` reports both MCP seams as KEEP with the exact edit; and `doc-parity` binds the
102
+ registration's five public strings into both the mode contract and the uninstall KEEP list.
103
+
104
+ Source-size raises accepted for: the typed channel gains its registration half — the guarded mcp
105
+ writer, its read-only leaf, and the registry rows every drift guard keeps. The uninstall growth is a
106
+ 15-round diff council: whole-chain containment, a typed incomplete-teardown lane, per-cause
107
+ classification.
108
+
7
109
  ## 6.0.0 — `grounding --plan` follows the canon it grounds: the three literal headings (AD-104; ships with engine 3.0.0 / memory 4.5.2)
8
110
 
9
111
  **The tool required a heading the canon had deleted.** `grounding --plan` demanded `## Approach` —
package/README.md CHANGED
@@ -237,6 +237,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
237
237
  | `/agent-workflow-kit set-recipe` | any time | **config writer** for `docs/ai/orchestration.json`: tell the agent your preference in plain language and it maps it to explicit `--set <activity>.<slot>=<recipe>` / `--unset` ops; the kit validates, **previews by default**, and writes only on `--write` (deployment-gated, atomic, symlink/TOCTOU-safe), resolving the effective recipe vs live readiness. Writes **only** that file — **never runs a backend, never commits**; hand-editing stays fully supported. |
238
238
  | `/agent-workflow-kit review-state` | any time | **read-only review-receipt checker** — makes "reviewed ≠ shipped" detectable: the bridge review wrappers append a receipt per successful review (into a file inside the git dir — never committable); this checks that every backend your configured `plan-execution.review` recipe names holds a **fresh, grounded receipt for the current uncommitted tree** (any later edit moves the fingerprint and stales the receipt; a review continuation never re-attests a folded tree). `--check` gives a gate exit code to declare in `docs/ai/gates.json` **by hand or via the consent-gated seeder** (preview → your explicit yes; never without consent). `--await [--timeout <s>]` (AD-049) BLOCKS until every recipe-named backend has receipted the current tree — the durable completion signal is the receipt, never a process event — so you wait for the bridges instead of hand-polling. Never writes, never commits, never runs a subscription CLI; it spawns read-only `git` queries to fingerprint the tree — and `git commit --no-verify` stays possible (discipline, not a sandbox). |
239
239
  | `/agent-workflow-kit sandbox-masks` | any time | **cosmetic exclude lane for sandbox device masks** — an OS sandbox (Claude Code) injects character-device masks into the work tree as untracked `git status` noise; the review domain already ignores them **by construction** (never-committable untracked classes — char/block devices, FIFOs, sockets — are excluded from the fingerprint, the assembled review payload, and the clean checks). This mode hides them from `git status` too: flagless = read-only probe (derives the CURRENT mask set from the unfiltered walk + lstat — never a frozen list — and revalidates fenced entries, loudly flagging one that became a real path); `--apply` = consent-gated FULL-BLOCK replace of its own fenced block in `git rev-parse --git-path info/exclude` (stale masks drop by construction; `--clear` always means REMOVE the block — it takes precedence over the derivation). Writes ONLY its fence — never `.gitignore`, never global config; symlinked/non-regular exclude paths and malformed fences fail closed. Watch note: a real file at an excluded path is silently skipped by bulk staging (`git add -A`/`git add .`) — delete the stale line first; the probe flags exactly this case. |
240
+ | `/agent-workflow-kit mcp` | Claude Code · opt-in | **typed-channel registration** — the kit ships a read-only stdio **MCP server** exposing its two promptless readers as TYPED tools (`path_inventory`: exists / type / size / line count / listing / a small file's text, many paths in ONE call; `repo_search`: literal search, the pattern a JSON string). Their arguments are named JSON fields rather than a string handed to a shell, so a pipe, a redirect or a quote inside a pattern or a path stays DATA and is never interpreted — legal bytes to search for, with no shell to read them as operators. Shipping the server does nothing on its own — a client sees it only once the project declares it, and this mode is that declaration: the `agent-workflow` entry in **`.mcp.json`** (command `node`, args = the absolute path of the RUNNING kit's server) plus `enabledMcpjsonServers` and the two derived tool allow rules in **`.claude/settings.json`**. Preview by default and the **exact entry is printed before consent** (registering a server means your client will run that command); `--apply` writes `.mcp.json` FIRST, then settings, merge-don't-clobber with each file's EOL kept, and a re-apply adds nothing twice. An existing `agent-workflow` entry that **structurally differs** from what this kit copy would write is refused unwritten (the comparison ignores key order, so re-serialized identical bytes are the same registration) — silently changing what an MCP server launches is what consent must not slide past. Where an OS sandbox masks `.mcp.json` with a device node, it writes nothing, hands you both paste-ready texts and exits 0. The server is a read-only child of your client (no write, no exec API) running outside the Bash sandbox as the client does. Never writes `settings.local.json`; never commits. |
240
241
  | `/agent-workflow-kit grounding` | any time | **grounded-review facts assembler** — mechanizes populating `agy-review --facts @f`: slices your entry-point's **Hard Constraints** section verbatim (exactly one match, else a loud stop) and/or a plan's three canon sections (`## Goal and boundary` + `## Module ledger` + `## Verification`, each required; duplicates stop), under the same byte budget the agy wrapper enforces (minus `--reserve-bytes` for the artifact share), with a loud tail-trim on overflow. `--autonomy` (AD-044) appends the COMPUTED effective autonomy policy from the git-top `docs/ai/autonomy.json` (every red-line + per-activity level, stated source line; absent file → the computed defaults ARE the policy, exit 0; a malformed policy fails CLOSED, exit 1). `--extra <text|@file>` (repeatable) appends your own facts **byte-verbatim** after the mechanical sections, so the merge is a tool input rather than a shell append — an `@file` must sit inside the proven git work tree (never the git dir) or the system temp surface, and is read through a no-follow descriptor (a FIFO cannot block it, a symlink leaf cannot substitute its target). Prints to stdout; `--out` writes **one scratch file only** — system-temp outside the repo ($TMPDIR / /tmp, rewritable) or a **fresh** gitignored in-repo path (create-only, exclusive write; an existing in-repo file, even gitignored, is refused — the `.env` clobber class); tracked, not-ignored-in-repo, other outside-repo, and symlink/non-regular destinations are all refused. Never commits, never runs a subscription CLI. |
241
242
  | `/agent-workflow-kit core-evidence` | any time | **the ONE loop-evidence writer** (strip-the-kit) — every core evidence record lands in a single append-only JSONL store inside the git dir (never committable; versioned schema, latest-per-key supersession, byte-identical duplicates refused, malformed lines fail every reader closed). `red-proof "<file>#<pattern>"` declares an observed-red **BEFORE a bugfix** (N/N red runs + content custody + base + the pre-fix fingerprint; green/mixed/timeout are DISTINGUISHED refusals — nothing written); `degrade --backend --reason` is the ONLY escape for an unavailable review backend (per-tree, never all backends); `summary` renders the whole loop state statelessly (gate result · per-backend verdicts · red-proofs · degrades) — no ledger, no rounds, nothing remembered. Honest residual: records are forgeable — self-discipline, not a security boundary. Never commits, never runs a subscription CLI. |
242
243
  | `/agent-workflow-kit coverage-check` | any time | **the final-run checker** (D3(c)+(d)) — **certifies coverage ONLY inside the `--final` run that owns the lcov** (ownership is exclusive by CONVENTION over the fixed path, not enforced — a concurrent writer to it is a stated residual, queued as LCOV-EXCLUSIVE-OWNERSHIP): an artifact on disk proves nothing about the tree it came from, so a standalone run prints its findings and states `attested=no` / `NO VERDICT` rather than a PASS (an lcov that predates an edit would otherwise certify a line the suite never executed). `attested=` claims a verdict was ISSUED, pass or fail — a run over uncovered lines still reads `attested=yes` and still exits 1, and a run whose coverage arm never executed (no lcov) reads `attested=no` even inside `--final`, because nothing was read and nothing is certified. The runner passes a nonce whose one-way commitment over `{nonce, fingerprint, base}` is the `final-start.attempt` it recorded; a context describing another tree, or matching no recorded attempt, is a REFUSAL, never a verdict. Findings are unchanged — reads the lcov the declared `unit-tests` gate produced at the FIXED git-dir path and fails on any uncovered CHANGED executable Node line (listed `file:line`; a changed file absent from the map is a file-level red; out-of-domain/unsupported files are LISTED — the claim narrowed honestly); VERIFIES every current-base red-proof declaration (bound test exists · custody hash unchanged · green N/N now · pre-fix fingerprint differs); prints `lcov-sha256=<hex|none>` of the exact bytes it consumed — the sha the `--final` receipt binds and re-hashes. An absent lcov is a LOUD `skipped-no-lcov`; a symlinked path is a refusal. `--check` is the gate exit code — declare it as the LAST gate (`run-gates --final` refuses otherwise). Read-only. |
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: agent-workflow-kit
3
3
  description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow".
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '6.0.0'
6
+ version: '7.1.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
@@ -147,6 +147,10 @@ guarded — read `${CLAUDE_SKILL_DIR}/references/modes/autonomy-doctor.md` befor
147
147
 
148
148
  guarded — read `${CLAUDE_SKILL_DIR}/references/modes/sandbox-masks.md` before acting.
149
149
 
150
+ ### Mode: mcp
151
+
152
+ guarded — read `${CLAUDE_SKILL_DIR}/references/modes/mcp.md` before acting.
153
+
150
154
  ### Mode: uninstall
151
155
 
152
156
  guarded — read `${CLAUDE_SKILL_DIR}/references/modes/uninstall.md` before acting.
@@ -0,0 +1,176 @@
1
+ // agy-review-await-guard.test.mjs — the source-level guard that makes the agy-review suite's
2
+ // asynchronous dispatch lane self-checking.
3
+ //
4
+ // Why this exists, and why it is a SEPARATE FILE.
5
+ //
6
+ // `agy-review.test.mjs` drives a bash wrapper. Every dispatch used to be a blocking `spawnSync`,
7
+ // which pinned the whole file to one core: 208 test points in one serial chain, 91.4s solo at
8
+ // 103% CPU while the rest of the machine idled. The dispatch helpers are asynchronous now, so the
9
+ // file's describes overlap — and that turns a MISSING `await` into a real hazard.
10
+ //
11
+ // The tempting claim is that a missing `await` announces itself: an assertion against a Promise
12
+ // never matches a status. That claim is FALSE here, and one site proves it — a dispatch whose
13
+ // RESULT IS DISCARDED (`run(sb, {...});` as a bare statement, the receipt read on the next line).
14
+ // Un-awaited, that test reads the fixture before the wrapper has written it and tears the sandbox
15
+ // down underneath a live child. Nothing in the suite can catch that; only the source can.
16
+ //
17
+ // It is a separate FILE rather than a describe inside the suite because the repo's survivor-corpus
18
+ // checker (`scripts/suite-parity.mjs`) binds each SURVIVING file's assert-call-site COUNT, and
19
+ // `--accept-rewrites` exempts the assertion-expression hash only, never the count. A guard added
20
+ // to `agy-review.test.mjs` itself would fail the very acceptance it exists to serve; a NEW file is
21
+ // counted separately and is never a parity failure.
22
+ //
23
+ // Dependency-free, Node >= 22. No side effects on import.
24
+
25
+ import { describe, it } from 'node:test';
26
+ import assert from 'node:assert/strict';
27
+ import { readFileSync } from 'node:fs';
28
+ import { dirname, join } from 'node:path';
29
+ import { fileURLToPath } from 'node:url';
30
+
31
+ const HERE = dirname(fileURLToPath(import.meta.url));
32
+ const SUITE_REL = 'agy-review.test.mjs';
33
+ const SUITE_PATH = join(HERE, SUITE_REL);
34
+
35
+ // A CALL, not a definition and not prose: the callee name, `(`, then an identifier start. A
36
+ // definition reads `const run = (`, and a comment's `run()` carries no argument, so neither
37
+ // matches. Keying on the ARGUMENT NAME instead would be a trap — the suite's sandboxes are called
38
+ // `sb`, `single`, `fedSb`, `utf8`, and a pattern pinned to `sb` silently skips the rest (it did,
39
+ // and the suite failed on exactly those sites).
40
+ const DISPATCH_CALL = /\b(?:run|fedRun|runAsync)\(\s*[A-Za-z_${]/g;
41
+
42
+ // A call site is DISCHARGED when the code immediately before it hands the promise on. Two rules,
43
+ // and they are NOT the same rule — reading the raw prefix for either keyword is a false green in
44
+ // the one guard that exists to prevent false greens:
45
+ // • `return` counts ONLY on the call's own line. ASI ends a bare `return` at the newline, so
46
+ // `return\n run(...)` returns undefined and leaves the dispatch unheld.
47
+ // • `await` and an arrow head may sit on an earlier line, so those are read across newlines —
48
+ // but only after TRAILING PROSE is dropped, because a comment line ending in the word "await"
49
+ // or "return" looks exactly like the keyword to a suffix test.
50
+ // The keyword must come from CODE. A line-oriented prose filter is not enough — `previous(); //
51
+ // await` is a code line whose tail is a comment — so comment and string bodies are masked to
52
+ // spaces in one pass first. Newlines survive the mask so the per-line `return` rule still sees
53
+ // them. Not a parser: it tracks the states a JS prefix can be in, and every ambiguity it cannot
54
+ // resolve leaves MORE text masked, which can only make the guard stricter.
55
+ // Mask ONE line: a comment tail and every string body become spaces, so no keyword can come from
56
+ // prose. Per LINE on purpose — a whole-file scan accumulates state, and one quote inside a regex
57
+ // leaves it "in a string" for the rest of the file (measured: 21 real call sites reported as
58
+ // un-awaited). A `'`/`"` string cannot span a newline and neither can a `//` comment, so a
59
+ // line-scoped mask resynchronises at every newline and cannot desync at all.
60
+ // Stated residual: a line INSIDE a multi-line template literal or block comment is read as code.
61
+ export const maskLine = (line) => {
62
+ if (/^\s*\*/.test(line)) return ' '.repeat(line.length);
63
+ const out = [...line];
64
+ let quote = null;
65
+ for (let i = 0; i < line.length; i += 1) {
66
+ const ch = line[i];
67
+ if (quote === null) {
68
+ if (ch === '/' && line[i + 1] === '/') { for (let j = i; j < line.length; j += 1) out[j] = ' '; break; }
69
+ if (ch === "'" || ch === '"' || ch === '`') quote = ch;
70
+ continue;
71
+ }
72
+ if (ch === '\\') { out[i] = ' '; if (i + 1 < line.length) out[i + 1] = ' '; i += 1; continue; }
73
+ if (ch === quote) { quote = null; continue; }
74
+ out[i] = ' ';
75
+ }
76
+ return out.join('');
77
+ };
78
+
79
+ // Two rules, and they are NOT the same rule:
80
+ // • `return` counts ONLY on the call's own line — ASI ends a bare `return` at the newline, so
81
+ // `return\n run(...)` returns undefined and leaves the dispatch unheld;
82
+ // • `await` and an arrow head may sit on an earlier line, so the walk skips blank and
83
+ // fully-masked lines to find the nearest line that carries code.
84
+ // Neither keyword counts when it is part of a longer name or a member access (`x.await`).
85
+ const RETURNED_HERE = /(?<![.$\w])return[ \t]*$/;
86
+ const HANDED_ON = /(?:(?<![.$\w])await|=>)\s*$/;
87
+
88
+ export const isDischarged = (rawPrefix) => {
89
+ const lines = rawPrefix.split('\n').map(maskLine);
90
+ if (RETURNED_HERE.test(lines[lines.length - 1])) return true;
91
+ let at = lines.length - 1;
92
+ while (at >= 0 && /^\s*$/.test(lines[at])) at -= 1;
93
+ return at >= 0 && HANDED_ON.test(lines[at]);
94
+ };
95
+
96
+ const sourceOf = () => readFileSync(SUITE_PATH, 'utf8');
97
+
98
+ export const dispatchSites = (source) => {
99
+ const sites = [];
100
+ for (const match of source.matchAll(DISPATCH_CALL)) {
101
+ const before = source.slice(0, match.index);
102
+ sites.push({
103
+ line: before.split('\n').length,
104
+ call: match[0],
105
+ discharged: isDischarged(before),
106
+ });
107
+ }
108
+ return sites;
109
+ };
110
+
111
+ describe('agy-review suite — every wrapper dispatch is awaited (concurrency guard)', () => {
112
+ // A guard that matches nothing passes for the wrong reason. The suite carried 188 dispatch call
113
+ // sites when this guard was written; the floor is deliberately below that so ordinary editing
114
+ // does not trip it, and far above zero so a broken pattern cannot read as "all clear".
115
+ it('the guard really finds the suite dispatch sites (never a vacuous pass)', () => {
116
+ const sites = dispatchSites(sourceOf());
117
+ assert.ok(sites.length >= 150, `only ${sites.length} dispatch call site(s) matched — the pattern stopped seeing the suite`);
118
+ });
119
+
120
+ it('no dispatch call site is left un-awaited', () => {
121
+ const loose = dispatchSites(sourceOf()).filter((site) => !site.discharged);
122
+ assert.deepEqual(
123
+ loose.map((site) => `${SUITE_REL}:${site.line} ${site.call}`),
124
+ [],
125
+ 'each listed site starts a dispatch whose promise nothing holds — prefix it with await',
126
+ );
127
+ });
128
+
129
+ // The predicate must be able to REPORT a violation, or the green above proves nothing. Two
130
+ // fixtures, one per arm, run through the same exported function the assertions above use.
131
+ it('the predicate reports a bare dispatch and accepts a discharged one', () => {
132
+ assert.deepEqual(
133
+ dispatchSites(' run(sb, { args: [] });\n').map((site) => site.discharged),
134
+ [false],
135
+ 'a bare statement dispatch must be reported',
136
+ );
137
+ assert.deepEqual(
138
+ dispatchSites(' const r = await run(sb, {});\n return fedRun(sb);\n const f = (sb) =>\n runAsync(sb, {});\n')
139
+ .map((site) => site.discharged),
140
+ [true, true, true],
141
+ 'await, return and an arrow body all discharge the promise',
142
+ );
143
+ // The three shapes that LOOK discharged to a prefix test and are not. Each fixture really ends
144
+ // in the keyword — a fixture whose comment trails off into another word tests nothing.
145
+ assert.deepEqual(
146
+ dispatchSites(' // do not await\n run(sb, {});\n').map((site) => site.discharged),
147
+ [false],
148
+ 'a comment ending in await is prose, not a discharge',
149
+ );
150
+ assert.deepEqual(
151
+ dispatchSites(' // early return\n run(sb, {});\n').map((site) => site.discharged),
152
+ [false],
153
+ 'a comment ending in return is prose, not a discharge',
154
+ );
155
+ assert.deepEqual(
156
+ dispatchSites(' return\n run(sb, {});\n').map((site) => site.discharged),
157
+ [false],
158
+ 'ASI ends a bare return at the newline, so the next call is NOT returned',
159
+ );
160
+ assert.deepEqual(
161
+ dispatchSites(' previous(); // await\n run(sb, {});\n').map((site) => site.discharged),
162
+ [false],
163
+ 'a CODE line whose tail is a comment ending in await is still prose',
164
+ );
165
+ assert.deepEqual(
166
+ dispatchSites(' const x = thing.await\n run(sb, {});\n').map((site) => site.discharged),
167
+ [false],
168
+ 'a member access named await is not the operator',
169
+ );
170
+ assert.deepEqual(
171
+ dispatchSites(" const s = 'await ';\n run(sb, {});\n").map((site) => site.discharged),
172
+ [false],
173
+ 'a string whose body ends in await is not the operator',
174
+ );
175
+ });
176
+ });
@@ -1160,16 +1160,28 @@ plan_fed_parts() { # $1 = file, $2 = first-part budget, $3 = later-part budget,
1160
1160
  ' "$1"
1161
1161
  }
1162
1162
 
1163
- # ORDERED delivery-proof candidates for ONE part (D1), nearest that part's middle first: an INTERIOR
1163
+ # ORDERED delivery-proof candidates for ONE part (D1), from that part's head inward: an INTERIOR
1164
1164
  # line whose trimmed text sits in the echo window and is unique as a WHOLE line across the change
1165
1165
  # set. This is only the cheap prefilter — the exact fixed-string checks (exactly one occurrence
1166
1166
  # across the bodies, ZERO anywhere the wrapper's own envelope text can reveal it) run in bash over
1167
1167
  # this list, because a candidate that is merely a unique LINE can still occur as a SUBSTRING of
1168
1168
  # another body's line, and a model without that part could then copy it.
1169
- # The order is produced by WALKING OUTWARD from the part's middle, never by sorting: a comparison
1170
- # sort here was quadratic, and the caller may walk the whole list, so the cost of merely ORDERING
1171
- # candidates must not depend on how many there are. There is no cap capping made the wrapper claim
1172
- # "no usable candidate" while a usable one sat at position 26.
1169
+ # HEAD-first, because the model is told to count the address BY READING and is denied every tool: an
1170
+ # earlier walk outward from the part's MIDDLE asked for line 847..1024 on a 701464-byte change set,
1171
+ # and a model that had received the part perfectly answered with the wrong line three times running
1172
+ # a false refusal on exactly the payload size this lane exists for.
1173
+ # The banner reject is the other half of that move. assemble_code_diff emits its section banners for
1174
+ # EVERY change set, so echoing one proves no delivery at all. It never mattered while the walk started
1175
+ # mid-part. It rejects the assembler's own VOCABULARY, not the "=== … ===" shape: a change set's own
1176
+ # "=== Deployment configuration notes ===" must stay admissible, or a part where nothing else
1177
+ # qualifies earns the false refusal this move exists to remove.
1178
+ # It does NOT duplicate $nonbody. That file now carries the banners it can SEE — up to the first
1179
+ # untracked section, where the scan stops by design (emit_derivable_from_artifact). Everything after
1180
+ # it is untracked file content, and the per-path banners interleaved with that content are exactly
1181
+ # what nothing upstream can enumerate: they are built from paths, not fixed strings, and a model
1182
+ # holding part 1 rebuilds them from the git-status block there. This reject is what covers them.
1183
+ # There is no cap — capping made the wrapper claim "no usable candidate" while a usable one sat at
1184
+ # position 26.
1173
1185
  list_echo_candidates() { # $1 = part file, $2 = unique-trimmed-lines file → "<line number><TAB><text>"
1174
1186
  LC_ALL=C awk -v minlen="$FED_ECHO_MIN_BYTES" -v maxlen="$FED_ECHO_MAX_BYTES" '
1175
1187
  NR == FNR { uniq[$0] = 1; next }
@@ -1178,16 +1190,12 @@ list_echo_candidates() { # $1 = part file, $2 = unique-trimmed-lines file → "
1178
1190
  if (i < 2 || i >= FNR) return 0
1179
1191
  t = lines[i]
1180
1192
  gsub(/^[ \t]+|[ \t]+$/, "", t)
1193
+ if (t ~ /^=== (repo file map|git status|staged diff|unstaged diff|untracked)/ && t ~ / ===$/) return 0
1181
1194
  if (length(t) < minlen || length(lines[i]) > maxlen) return 0
1182
1195
  return (t in uniq)
1183
1196
  }
1184
1197
  END {
1185
- mid = int((FNR + 1) / 2)
1186
- if (usable(mid)) printf "%s\t%s\n", mid, lines[mid]
1187
- for (d = 1; d <= FNR; d++) {
1188
- if (usable(mid - d)) printf "%s\t%s\n", mid - d, lines[mid - d]
1189
- if (usable(mid + d)) printf "%s\t%s\n", mid + d, lines[mid + d]
1190
- }
1198
+ for (i = 1; i <= FNR; i++) if (usable(i)) printf "%s\t%s\n", i, lines[i]
1191
1199
  }' "$2" "$1"
1192
1200
  }
1193
1201
 
@@ -1457,6 +1465,39 @@ run_fed_review() {
1457
1465
  # payload is byte-identical with and without a device mask). Symlinks are shown as their target
1458
1466
  # (never followed — no out-of-repo leak); directories/vanished paths are noted, never read (a `cat`
1459
1467
  # on a FIFO would hang BEFORE the hard timeout applies — that class never reaches this loop).
1468
+ # Everything in the ASSEMBLED artifact that a model holding part 1 could reproduce for a part it
1469
+ # never received. Sliced from the artifact's own bytes rather than recomputed: recomputing raced the
1470
+ # assembly (an index or worktree change between the two reads leaves a line the REAL part 1 revealed
1471
+ # outside the filter) and could only ever re-derive the header shapes it thought of — a rename's
1472
+ # `diff --git a/old b/new`, its `rename from`/`rename to` pair, and git's quoting of an awkward name
1473
+ # were all missing. The artifact carries the true bytes, so there is nothing to reproduce.
1474
+ # - the repo-map and status blocks entire: they travel in part 1 and name every path;
1475
+ # - inside the two diff sections, every line that is NOT a content line. A unified diff's payload
1476
+ # lines begin with '+', '-' or ' '; everything else is metadata git derives from those paths, so
1477
+ # one positive rule covers `diff --git`, `@@`, `index`, mode/similarity/rename/copy and
1478
+ # `Binary files` without enumerating any of them. The `--- a/P` / `+++ b/P` file headers are the
1479
+ # exception the rule cannot see: they open with '-' and '+' and so READ as payload. A regression
1480
+ # caught one being chosen as proof, so they are named. The cost is that a removed line whose own
1481
+ # text starts with '-- ' is skipped too — the walk simply continues, which only ever narrows what
1482
+ # may be proven with.
1483
+ # Untracked file CONTENTS are deliberately absent: nothing in part 1 reveals them, so they remain the
1484
+ # honest proof material this lane depends on. Reaching the untracked section is therefore TERMINAL —
1485
+ # a file whose own content happens to carry a `=== git status … ===` line would otherwise flip the
1486
+ # scan back into metadata mode and sweep every line after it into the set, starving later parts of
1487
+ # candidates and earning the false refusal this whole move exists to remove.
1488
+ emit_derivable_from_artifact() { # $1 = the assembled artifact
1489
+ LC_ALL=C awk '
1490
+ sec == "body" { next }
1491
+ /^=== repo file map/ { sec = "meta"; print; next }
1492
+ /^=== git status/ { sec = "meta"; print; next }
1493
+ /^=== staged diff/ { sec = "diff"; print; next }
1494
+ /^=== unstaged diff/ { sec = "diff"; print; next }
1495
+ /^=== untracked/ { sec = "body"; print; next }
1496
+ sec == "meta" { print; next }
1497
+ sec == "diff" { if ($0 !~ /^[-+ ]/ || $0 ~ /^(--- |\+\+\+ )/) print; next }
1498
+ ' "$1"
1499
+ }
1500
+
1460
1501
  assemble_code_diff() {
1461
1502
  echo "=== repo file map (git ls-files) ==="
1462
1503
  emit_repo_file_map
@@ -1853,9 +1894,14 @@ else
1853
1894
  echo " because an unprovable delivery is exactly what this lane exists to prevent." >&2
1854
1895
  exit 2
1855
1896
  fi
1856
- # Everything the wrapper itself will SEND that is not a BODY. A candidate occurring anywhere in
1857
- # here would be revealed to a model that never received its part, so the proof would prove
1858
- # nothing. Rendered with the REAL part indices, because the frames carry them.
1897
+ # Everything a model could hold WITHOUT the part a candidate belongs to. Two families, and the
1898
+ # second is why this is not just "what the wrapper sends":
1899
+ # 1. the envelope the wrapper sends outside every body, rendered with the REAL part indices;
1900
+ # 2. every line DERIVABLE from the repo map and status blocks — which travel in part 1, so a
1901
+ # model holding part 1 can reproduce them for any LATER part it never received. Sliced from
1902
+ # the assembled artifact's own bytes (see emit_derivable_from_artifact), never recomputed.
1903
+ # Enumerating families was already the wrong shape once (a banner blacklist); this instead lets
1904
+ # the existing fixed-string filter do the work against what the artifact itself already shows.
1859
1905
  ( umask 077; {
1860
1906
  cat "$staging/grounding"
1861
1907
  for (( k = 1; k <= n; k++ )); do
@@ -1865,6 +1911,7 @@ else
1865
1911
  done
1866
1912
  emit_fed_final_head
1867
1913
  emit_shape_fed "(addresses are appended below)"
1914
+ emit_derivable_from_artifact "$artifact_file"
1868
1915
  } > "$nonbody" )
1869
1916
 
1870
1917
  # ONE pass, no re-picking. The address list cannot reveal a candidate BY CONSTRUCTION (see