@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
@@ -141,7 +141,7 @@
141
141
  ],
142
142
  "guardrails": [
143
143
  { "value": "read-only sandbox — codex cannot edit, create or delete a file", "enforcement": "enforced", "source": "bin/codex-review.sh" },
144
- { "value": "runs on the pinned frontier model at max effort", "enforcement": "enforced", "condition": "unless CODEX_PROBE=1 relaxes the guard for a throwaway probe", "source": "bin/codex-review.sh" },
144
+ { "value": "runs on the pinned model at max effort", "enforcement": "enforced", "condition": "unless CODEX_PROBE=1 relaxes the guard for a throwaway probe", "source": "bin/codex-review.sh" },
145
145
  { "value": "a successful review appends one receipt line the review-state gate reads", "enforcement": "enforced", "condition": "a receipt write failure warns and the review still succeeds — the tree then reads un-receipted", "source": "capability.json roles.review.contract.receipt" }
146
146
  ],
147
147
  "customHooks": ["CODEX_PROBE"]
@@ -20,8 +20,10 @@ judgment.
20
20
 
21
21
  ## Quality-first: model & effort are pinned
22
22
 
23
- Delegated codex work ALWAYS runs on the frontier model at max effort: `gpt-5.6-sol` / `xhigh` are **pinned**
24
- and a non-default `CODEX_MODEL`/`CODEX_EFFORT` is **refused** (exit 2). Do not try to "tune down" the
23
+ Delegated codex work ALWAYS runs on the PINNED model at max effort: `gpt-5.6-sol` / `xhigh` are **pinned**
24
+ and a non-default `CODEX_MODEL`/`CODEX_EFFORT` is **refused** (exit 2). That the pinned id is still the
25
+ strongest selectable model is a hand-checked claim with no automated gate and no recorded date — see
26
+ SKILL.md. Do not try to "tune down" the
25
27
  model or effort for a real run — the wrapper will stop you. Quota is metered in **messages** (rolling
26
28
  5h + weekly), so economy comes from removing waste (clean capture, the precomputed review diff, resume),
27
29
  never from a downgrade. The only opt-out is a throwaway, effort-independent probe: `CODEX_PROBE=1`
@@ -64,8 +64,9 @@ failure is proven.
64
64
  ## Quality-first guard (pinned model & effort)
65
65
 
66
66
  The wrappers default `CODEX_MODEL=gpt-5.6-sol` and `CODEX_EFFORT=xhigh` and **refuse** (exit 2, loud) any
67
- non-default — delegated work always uses the frontier model at max effort; quality is never traded for
68
- quota. `CODEX_PROBE=1` relaxes this for a throwaway, effort-independent probe only (echoed loudly), and
67
+ non-default — delegated work always uses the PINNED model at max effort; quality is never traded for
68
+ quota. (Whether the pinned id is still the strongest selectable model is a hand-checked claim with no
69
+ automated gate — see SKILL.md.) `CODEX_PROBE=1` relaxes this for a throwaway, effort-independent probe only (echoed loudly), and
69
70
  a probe still runs on the subscription, in the sandbox, with clean capture.
70
71
 
71
72
  ## Passthrough guard (two tiers, after a literal `--`)
@@ -70,7 +70,9 @@ if it reports a missing git work tree or root `AGENTS.md`, run it from a project
70
70
 
71
71
  - The wrappers are **subscription-only** by design and will not use api-key billing.
72
72
  - `codex-exec` runs a **workspace-write** sandbox with **network OFF**; `codex-review` runs
73
- **read-only**. They also pin the frontier model/effort (refusing a downgrade), enforce a hard
73
+ **read-only**. They also pin the model/effort (refusing ANY non-pinned model or effort, a stronger
74
+ one included; whether the pinned id is still the strongest selectable model is a hand-checked claim
75
+ with no gate — see `../SKILL.md`), enforce a hard
74
76
  timeout, capture only codex's final message, and block codex from writing git via a shim — see
75
77
  [`../references/sandbox-and-flags.md`](../references/sandbox-and-flags.md) and the knob table in
76
78
  [`../SKILL.md`](../SKILL.md#environment-knobs). No setup is needed to enable these — they are on by
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "6.0.0",
6
+ "version": "7.1.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "6.0.0",
3
+ "version": "7.1.0",
4
4
  "description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -418,7 +418,7 @@ export const isReadLaneCommand = (command) => {
418
418
  // the behaviour that existed before — silence. Nothing is refused, so a hint that is wrong for a
419
419
  // particular command costs the caller a sentence, never their work. That is the property no deny
420
420
  // design could ever claim.
421
- export const HINT_CORE = 'agent-workflow: this read left the promptless lane — a plain read-only command with no redirection, no banner and no quotes stays inside it. If the byte is a decoration you do not need, drop it and re-issue; if it is ordinary text inside an argument, a search belongs in `repo-search.mjs` (`--pattern-file`), and several path questions (exists / size / line count / listing / small-file contents) are ONE `path-inventory.mjs` call. If the byte is a deliberate operator, keep it and confirm by hand.';
421
+ export const HINT_CORE = 'agent-workflow: this read left the promptless lane — a plain read-only command with no redirection, no banner and no quotes stays inside it. If the byte is a decoration you do not need, drop it and re-issue; if it is ordinary text inside an argument, take it out of the command string entirely: where this project registers the kit\'s MCP server (`/agent-workflow-kit mcp`), a search is the `repo_search` tool and several path questions (exists / size / line count / listing / small-file contents) are ONE `path_inventory` call — the pattern and the paths are JSON fields, so no byte of them is ever shell. The same two readers on the command line are `repo-search.mjs` (`--pattern-file`) and ONE `path-inventory.mjs` call. If the byte is a deliberate operator, keep it and confirm by hand.';
422
422
 
423
423
  // The hint must not GUESS which of the two causes produced the residual — the guard cannot tell a
424
424
  // byte that is ordinary text inside an argument from a real shell operator, and pretending otherwise
@@ -0,0 +1,37 @@
1
+ ### Mode: mcp
2
+
3
+ <!-- opt-in-capability: mcp-channel -->
4
+
5
+ **The typed channel, registered in ONE project.** The kit ships a stdio MCP server (`tools/mcp-server.mjs`) exposing the two promptless readers as TYPED tools — `path_inventory` (exists / type / size / line count / listing / a small file's text, many paths in ONE call) and `repo_search` (LITERAL search, the pattern a JSON string). Both take their arguments as named JSON fields rather than as a string handed to a shell, so a pipe, a redirect, a quote or an `||` inside a pattern or a path stays DATA and is never interpreted — they are legal bytes to search for, and there is no shell to read them as operators. Shipping that server does nothing on its own: a client only sees it once the project DECLARES it. This mode is that declaration, and nothing else.
6
+
7
+ It writes exactly two files:
8
+
9
+ - **`.mcp.json`** at the project root — the `"agent-workflow"` entry under `mcpServers`: `"type": "stdio"`, `"command": "node"`, and `args` = the **absolute path of the RUNNING kit's** `tools/mcp-server.mjs` (the literal path, resolved at write time — a moved kit fails visibly rather than resolving to something else).
10
+ - **`.claude/settings.json`** — `enabledMcpjsonServers` gains `"agent-workflow"` (a project-scoped `.mcp.json` server is inert until it is enabled), and `permissions.allow` gains the two rules derived from the server's own name and tool list: `mcp__agent-workflow__path_inventory` and `mcp__agent-workflow__repo_search`. Both rules are DERIVED, never re-typed — a renamed tool cannot ship a rule that grants nothing.
11
+
12
+ **Trust posture (state it plainly when asking consent):** the registered server is a **read-only child of your MCP client** — it answers path facts and literal searches over the project root, and exposes no write and no exec API. It runs **outside the Bash sandbox**, exactly as the client itself does, so the sandbox is not what bounds it; the server's own root containment is. The two allow rules make its tool calls promptless — that is the whole velocity gain, and it applies to those two tools only. Registering a server means your client will RUN the named command, which is why the **exact entry is printed before consent is asked**, never described.
13
+
14
+ Run `node ${CLAUDE_SKILL_DIR}/tools/mcp.mjs [--dry-run | --apply] [--cwd <dir>]`:
15
+
16
+ 1. **`--dry-run` first, always** (the default — changes nothing): prints what each file would gain and the **exact entry it would declare** — the same structured value, re-serialized for the preview rather than copied byte for byte — plus the apply one-liner. The preflight is **READ-ONLY**: an absent `.claude/` is a named state here and is created on `--apply` only — a preview that made a directory would not be a preview.
17
+ 2. **Ask consent** via **`AskUserQuestion` where supported**, the no-change option first: keep the shell lanes, or register the typed channel — presenting the trust posture above in plain language.
18
+ 3. **Only on an explicit yes**, re-run with `--apply`. It writes **`.mcp.json` FIRST, then `.claude/settings.json`** — settings that enable a server whose entry is not yet there is a client error on every startup. Both writes are merge-don't-clobber (foreign servers, foreign keys and existing allow rules preserved; a re-apply adds nothing twice) and keep each file's existing EOL. New MCP servers are picked up when the client next starts — unlike the `hook` mode, this is not a hot reload.
19
+
20
+ **The two arms that are not a write:**
21
+
22
+ - **DIFFERING** — `.mcp.json` already carries an `"agent-workflow"` entry that **structurally differs** from ours (another kit copy, a hand-edited path, an added `env`, a `null`). The comparison is deliberately key-order-independent, so a re-serialized identical entry is the SAME registration, while any real difference in what would be launched is a difference. The run **STOPs unwritten, on both lanes**: silently changing what an MCP server launches is exactly what consent must not slide past. The recovery is named — review that entry, then remove or rename it and re-run. The kit never resolves this for you.
23
+ - **MASKED** — an OS sandbox injects a **character device** where `.mcp.json` would be (this is the normal condition of a sandboxed session, not a fault; see `${CLAUDE_SKILL_DIR}/references/modes/sandbox-masks.md`). Writing through a device node is refused by construction, so the mode **hands over what to merge, writes nothing, and exits 0**. The two halves of that handoff are worded differently, and the difference is load-bearing: `.claude/settings.json` was **observable — and read where present** — so its body is a real merge already carrying every foreign key it had, while the masked `.mcp.json` was not observable at all, so the mode hands over **the entry alone**, to be merged under `mcpServers` keeping every other server. A whole-file body would name only our server, and pasted as instructed it would delete every server the mask hid. Every other never-committable class (block device, FIFO, socket) takes the same arm. The handoff is scoped to `.mcp.json`: a masked `settings.json` is a **refusal**, because this mode can neither write it nor merge into what it cannot read.
24
+
25
+ **Refusals (STOP, exit 1, zero writes) — decided BEFORE the handoff, so a mask can never swallow one:** a symlinked project root · a symlinked or **directory** `.mcp.json` / `settings.json` (the atomic publish renames over the path, which would replace the **link itself** — silently destroying a deliberate indirection, while the file the reader was actually looking at goes untouched). Note what is **not** in this list: a device node, FIFO or socket at `.mcp.json` takes the successful MASKED handoff above, not a refusal — only at `settings.json` is that class a refusal, since no handoff is sanctioned there · a symlinked or non-directory `.claude` · a masked `settings.json` · malformed or unreadable JSON in either target · a **managed key of the wrong type** — `mcpServers`, `enabledMcpjsonServers`, `permissions` or `permissions.allow` present but not the shape this mode writes, which read as empty containers would be merged over and silently destroyed (a foreign key of any shape is data and is never judged) · the DIFFERING entry above.
26
+
27
+ **Reading posture (the residual, stated).** The **container is classified before the file inside it is read**: path resolution follows an *intermediate* symlink (`O_NOFOLLOW` guards only the final component), so a symlinked `.claude` would otherwise deliver a settings file from outside the work tree — into memory, into a verdict, and into a rendered merge body — before anything could refuse it. A foreign container makes its settings target `unreadable` with that reason, and nothing inside it is read. Beyond that, every target is classified by `lstat` **before** any open, so a target decided **foreign** is never followed, never opened and never read: a symlink is refused by class, and a device node or FIFO is named rather than consumed. (A target decided **regular** is of course opened — that is the read.)
28
+
29
+ The **residual is stated rather than implied**: every guard here is a path check followed by a path read, and Node exposes no `openat`. What is closed is the **static** case — a symlinked container or leaf that is already there is never followed and never read. What is **not** closed is a path that **changes underneath** the guard, and the leaf and the container are not the same problem. The **leaf** is protected as far as a path-based reader can go — the shared reader opens `O_NOFOLLOW`, stats the descriptor and reads through it, so a swapped *symlink* can never be followed; what escapes it is substitution by another *regular file*, which needs the open bound to an earlier inode observation inside that shared primitive (four consumers use it, so it is that leaf's decision, not this mode's). The **container** cannot be closed that way at all: nothing about the leaf's descriptor speaks for the directory the path was resolved through, and that needs directory-relative opening (`openat`), which Node does not expose — a platform limit rather than a missing check. **The bar this mode meets is therefore: a path already foreign is never followed or read, and a target decided foreign is never opened** — nothing here claims more.
30
+
31
+ **Invariants:** writes ONLY `.mcp.json` + `.claude/settings.json` · **never `settings.local.json`** · never commits · never edits `docs/ai/gates.json`, `lanes.json` or any other declaration · the server itself gains no writer tool from this mode · user MCP scope and other clients (Codex, Cursor) are out of scope — this registers the project scope for Claude Code.
32
+
33
+ **Out of scope, by name: `disabledMcpjsonServers`.** A server listed there is rejected by the client in **every** mode, so a project can hold the entry, the enable and both rules and still have a dark channel. **This mode does not detect that**, and "registered" therefore means *the three things this mode writes are in place* — never *the client will load it*. The check was built during review and then **subtracted**: honouring a veto means reading it from every scope the client merges, and each scope has its own masked, symlinked, malformed and unreadable states in which a hidden deny still yields a confident answer. Three review rounds each closed one such hole and opened the next. A check that is wrong in states it cannot enumerate is worse than a stated limit — so this is the stated limit, and the queue carries it.
34
+
35
+ **Reporting and removal.** The read half (`tools/mcp-registration.mjs`) is what the `recommendations` advisor's `mcp-channel` item and `uninstall` both ask; it never edits. `uninstall` reports both seams as **KEEP / report-only** with the exact edit to make by hand — the same posture it takes for every `.claude/settings.json` key.
36
+
37
+ **Exit codes:** `0` done / dry-run / the hand-apply masked state; `1` a precondition STOP; `2` bad arguments.
@@ -31,6 +31,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
31
31
 
32
32
  - `source-size` — the practice's arms differ by what the declared gate actually IS. **`adopted-elsewhere`** — the gate runs a DIFFERENT real copy of this checker (a vendored deployment): the practice is running, the advisor simply cannot resolve it as its own sibling, and the realpath anchor deliberately does not widen (it is what stops a lookalike from certifying). So the convergence is the ack writer's preview (`--lane source-size-copy` → `sourceSizeCopyAck`), never `--adopt`, which would mint the record and then be refused by the fill on the id already declared. The fingerprint binds the declared claims AS AUTHORED, sorted — not their resolved paths, which are machine-specific and would churn a committed ack between machines — so a second external copy re-fires it. **`id-squatter`** — a gate carries the `source-size` id and is not this checker under any reading: the id claims the practice while nothing measures module size, so it is **HAND-APPLY** (rename the id, or repoint the cmd at this checker's `--check` form). **`unminted`** stays what it was for both copies: a gate certain to refuse on every run, attention, and NO acknowledgement silences it — only the rendered verb differs (with the gate already declared elsewhere the missing half is the record alone, so the line is `--write-baseline`). Risk profile: `docs/ai/gates.json` is a PRIVILEGED file, so every arm here either records an acknowledgement or hands you an edit — the kit declares nothing on its own.
33
33
  - `gate-hook` — the base arm is the ordinary opt-in wiring offer. The **`marker-stale`** arm is not: the placed hook validates your declaration through its OWN baked copy and goes dark on any key it does not know, so a declaration carrying the `lcovProducer` key under a hook that predates it silently turns auto-approval OFF, and every gate prompts again with no error anywhere. The condition is the key's PRESENCE, not its value — an older hook rejects a key it does not know whatever that key says, so `"lcovProducer": false` darkens it exactly as `true` does. The arm is deliberately marker-scoped — a stale hook is otherwise harmless — and its recovery is the writer's own: `gate-hook --apply` places only an ABSENT target, so converging means deleting the placed hook and re-placing it. That makes it a destructive **HAND-APPLY** (`rm` + `--apply`, absolute path so it can only delete this project's hook), never something the consent flow runs for you. When the read-lane is also enabled, this arm and `read-lane.stale` would report the same file with the same recovery, so exactly one renders — **this one**, because its cause is the true one: a hook that postdates the read-lane and merely predates the marker key reads `lanes.json` perfectly well, and the read-lane arm's wording would be false over it. Risk profile: deleting one placed hook file, then re-placing it from the bundle; the declaration is never touched.
34
+ - `mcp-channel` — the offer is to REGISTER the kit's read-only stdio MCP server in this project, which is a different kind of consent from the other velocity items: a registration is a command your MCP client will RUN, so the apply here is the mode's own **flagless preview**, which prints the exact entry and writes nothing — the `--apply` that follows is a SEPARATE step you run after reading that entry, and the consent flow never carries it. What the registration buys: path questions and literal searches become typed tool calls whose arguments are named JSON fields instead of 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 — they are perfectly legal bytes to search for, there is simply no shell to read them as operators. Posture: the server is a **read-only child of your client** (path/type/size/line facts and literal search over this project root; no write and no exec API), and like the client itself it runs **outside the Bash sandbox** — the sandbox is not what bounds it, the server's own root containment is. The two allow rules it adds make those two tools promptless and nothing else. Two arms are **HAND-APPLY** and never run for you: **`.differing`** — an `agent-workflow` entry already stands in `.mcp.json` and **structurally differs** from what this kit copy would write (another kit copy, a hand-edited path, an added `env`; the comparison ignores key order, so re-serialized identical bytes are the same registration), and silently changing what an MCP server launches is exactly what consent must not slide past, so the remedy is your edit; and the **masked** arm, where an OS sandbox hides `.mcp.json` behind a device node — the kit cannot write there, so it hands you the text to paste from outside the sandbox. When the file is masked but the settings half is already complete, the item **does not render at all**: what is unobservable becomes a stated SKIP, so optimality is withheld rather than a registration you already made being offered again. Stated limit: this item does **not** detect a `disabledMcpjsonServers` veto, so a converged `mcp-channel` means *what the mode writes is in place*, not *the client will load it* — see `${CLAUDE_SKILL_DIR}/references/modes/mcp.md` for why that check was subtracted rather than half-built. Risk profile: a new read-only channel your client will launch; no write or exec exposure, and no existing declaration is touched.
34
35
  - ADDITIONAL `gates-inert` arms (the third outcomes) — two further arms, and they differ in whether anything is BROKEN. **`producer-unrecognized`** — a checker with no producer anywhere in the declaration, on a tracked tree the changed-line coverage domain cannot reach (`.ts`/`.tsx`/`.jsx`/`.mts`/`.cts` strictly outnumber `.mjs`/`.cjs`/`.js`): the dead pair is real, so this arm is **HAND-APPLY** and the two remedies are marking the real producer with `"lcovProducer": true` or dropping the checker. Never a `node --test` prescription over a project that has no such suite, never the fill preview, and never an acknowledgement — a dead pair is broken, not narrow, and removing a producer after an acknowledgement lands right back in this arm. **`coverage-domain-narrow`** — the producer/checker pair IS live and the tree is still dominated by what the domain excludes: nothing is broken, and the honest sentence is that certification covers the assessable minority. Its apply is the consent-gated **ack writer** preview (a NEUTRAL fingerprint into `docs/ai/acks.json` as `coverageDomainAck`, never a security key); after the SAME confirmation you run the `--apply` it prints. The fingerprint binds the FACT — the verdict plus the unsupported extensions present, never the file counts — so an acknowledged project stays quiet as it grows and re-fires when a new unsupported language arrives or the verdict flips. The census reads the TRACKED tree with a read-only `git ls-files`; a tree it cannot read (a non-git deployment) becomes a stated skip, so optimality is withheld rather than assumed. Risk profile: no enforcement change of any kind — one acknowledgement recorded in a family-owned file, and one hand edit that stays the maintainer's.
35
36
  - `adr-store-migration` — other items write project files too; what is unique here is that the crossing **overwrites and deletes files the project already has**: it replaces the deployed enforcement scripts in `scripts/` (the directional subset — only basenames the project already has; a locally-edited copy is snapshotted first, never silently clobbered) and, where a retired archive file exists, DELETES it once conservation has been proven. That is why it is **HAND-APPLY** and why the command shown in the apply slot is a **`--dry-run`** — it writes nothing and prints the whole plan. `--apply` is a SEPARATE step, run only after that plan has been shown and **fresh consent** obtained for it; the consent flow executes only the apply slot, so an item that needs consent AFTER its preview cannot use that lane at all. Every write is idempotent and the run is re-runnable to completion after any interruption, so a re-run repairs rather than double-applies. It never commits. Risk profile: overwrite + delete of existing project files, gated on a preview you have actually read.
36
37
 
@@ -13,8 +13,9 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/uninstall.mjs [<member>] [--dir <project>] [
13
13
 
14
14
  It classifies every surface into four classes and acts accordingly:
15
15
 
16
- - **remove** (safe) — an installed skill dir that is **provably ours** (valid manifest, `name`+`kind` match). A dir present but **not provably ours** (`foreign`/`stub`/`invalid`/unreadable) → **STOP**: left untouched **and reported**, while the teardown still removes the members that ARE ours (a not-ours surface is never clobbered, and never blocks removing the rest — the per-item `setup` posture). **Preflight-then-mutate:** if a mutable surface **changed since the dry-run** (a skill no longer ours, a wrapper turned foreign, a hook that lost our marker, a malformed fence), the run **aborts with zero changes**.
16
+ - **remove** (safe) — an installed skill dir that is **provably ours** (valid manifest, `name`+`kind` match). A dir present but **not provably ours** (`foreign`/`stub`/`invalid`/unreadable) → **STOP**: left untouched **and reported**, while the teardown still removes the members that ARE ours (a not-ours surface is never clobbered, and never blocks removing the rest — the per-item `setup` posture). **Preflight-then-mutate:** the apply run builds its OWN fresh plan and preflights every mutable surface against it; if one has changed by then (a skill no longer ours, a wrapper turned foreign, a hook that lost our marker, is unreadable or is no longer a regular file, a malformed fence, or a path now reachable only through a symlink), the run **aborts with zero changes**. A surface that changes AFTER that preflight — during the mutation itself — cannot be answered that way, and the report distinguishes two outcomes rather than flattening them. A removal **refused before it touched anything** (containment, or a skill directory that stopped being provably ours — re-established immediately before the recursive delete, not only at preflight) is reported as **left untouched**, which is a claim the refusal proves. A removal that **failed part way** proves nothing of the sort: it is reported as **possibly partially removed**, and it STOPS the run, because continuing to delete later surfaces on top of an unknown state is not a teardown. Either way the run ends with a typed **INCOMPLETE** stop listing both categories plus what had already been applied — or stating plainly that nothing had been. That outcome is a real one, not a promise of atomicity.
17
17
  - **reverse** (managed-marker) — a bridge **wrapper symlink that points at our source** (a foreign/non-symlink one → STOP); the hidden-mode **managed fence** (via the existing `--unhide` path — only the fenced lines); a **pre-commit hook carrying our marker** (an unmarked / user hook → left + reported).
18
18
  - **KEEP — never deleted** (report-only) — `docs/ai`, `AGENTS.md`, `CLAUDE.md`, `docs/plans`, and `.claude/settings.json` (the `includeCoAuthoredBy` edit **and** any `permissions.defaultMode`/`permissions.allow` the velocity profile may have seeded). The tool **prints the exact `rm` / `git rm --cached`** for the docs/entry files and an **edit** instruction for `settings.json` (never an `rm` — it may hold your own settings); the **user** runs them. Surface this in plain language; never delete on their behalf.
19
+ - **KEEP — the MCP registration** (report-only, same posture, two seams): the `"agent-workflow"` entry in **`.mcp.json`** at the project root, and in `.claude/settings.json` the `enabledMcpjsonServers` membership plus the two tool rules `mcp__agent-workflow__path_inventory` and `mcp__agent-workflow__repo_search`. Both are reported with the exact **edit** to make by hand — `.mcp.json` may declare servers this kit never placed, and the settings file may hold your own rules, so neither is ever rewritten or removed. A `.mcp.json` that is a symlink or a sandbox device mask is reported **unread**, never followed and never parsed.
19
20
 
20
21
  **Shared globals:** removing `agent-workflow-memory` / `agent-workflow-engine` / a bridge removes a **global** skill that another project on the machine may use — say so before applying. **Windows:** the wrappers are POSIX; the skill-dir + project arms still work, the wrapper arm reports *use WSL*.
@@ -76,6 +76,7 @@ Apply this as part of §2 before any user-facing summary:
76
76
  ### 2.6. Planning, review & process-fidelity invariants
77
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.)
78
78
  - **Fold by code, not prose.** Before folding a code-touching finding into a plan or change, read the cited `file:line` and cite it — a prose fold drifts from the code and seeds the next bug.
79
+ - **Finding scope (plan-execution) — name the invariant BEFORE the edit.** During EXECUTION only — a plan under authoring has no shipped behaviour to call a live defect in, so plan-review carries none of this. Every finding names the invariant its fix would enforce, and where that invariant already lives decides the disposition: already an acceptance criterion of the phase → **fold here**; it would have to be ADDED → ship the **narrow fix** for the found site (red first, then green) and queue ONLY the generalization — a deferral row carries the invariant, the origin `file:line`, the narrow fix, its proof and a residual exposure declared NOT live; no correct narrow fix → **blocking**: the phase does not close, and it is **never queued**. Two bars declared before each round: a finding counts only if it changes a **WRITE/REMOVE decision** or is a false statement in shipped text; a repeat finding in one subarea **routes to SUBTRACTION**, not a fourth patch.
79
80
  - **Right altitude.** Pin intent + invariants + acceptance criteria (named tests); leave fine code-mechanics to Execute, where prose cannot diverge from reality.
80
81
  - **No code-mechanics in the plan.** A ledger row carries its path and anchor, and Verification carries the exact commands (the plan-shape canon) — checked syntax: the plan's own Verification runs them against an explicit expected outcome or gate; the only other syntax a plan may carry is a literal fixture/schema fragment a named test copies or validates. Un-run, logic-bearing syntax — control-flow, a regex, a glob, a grammar, an algorithm body, a mini-DSL — never lives in plan prose, however plausible or shell-verified it looks: a fold or draft that wants one is the trigger to write the test instead.
81
82
  - **Test-as-spec.** Fold a code-touching finding into a red→green TEST, not a prose paragraph — the gate is the only deterministic checker; a paragraph cannot self-check.
@@ -168,6 +168,13 @@ const CATALOG = [
168
168
  kind: GUARDED,
169
169
  oneLine: 'Hide the sandbox’s injected device masks from git status via one managed block in the repo’s local exclude file — probe-derived (never a frozen list), preview first, full-block replace on your yes; reviews already ignore the masks by construction.',
170
170
  },
171
+ {
172
+ key: 'mcp',
173
+ invocation: invocationOf('mcp'),
174
+ group: 'Configure',
175
+ kind: GUARDED,
176
+ oneLine: 'Register the kit’s read-only MCP server in this project, so path questions and literal searches run as typed tool calls instead of shell command strings — previews the exact entry it would declare, writes only on your yes, and where a sandbox hides the file it hands you the text to paste instead.',
177
+ },
171
178
  {
172
179
  key: 'recipes',
173
180
  invocation: invocationOf('recipes'),
@@ -53,6 +53,9 @@ export const LIBRARY_ONLY_MODULES = Object.freeze({
53
53
  // command that APPLIES that decision is the ensure CLI, and the one that edits the config by intent
54
54
  // is set-recipe.
55
55
  'orchestration-config.mjs': '/agent-workflow-kit set-recipe',
56
+ // Named by references/modes/mcp.md as the read half the advisor and uninstall ask. It only ever
57
+ // REPORTS; the command that acts on what it reports is the mode itself.
58
+ 'mcp-registration.mjs': '/agent-workflow-kit mcp',
56
59
  });
57
60
 
58
61
  // The frozen refusal line. One line, names the module, names the command.
@@ -69,6 +69,9 @@ import { COVERAGE_PRODUCER_BODY } from './coverage-producer.mjs';
69
69
  // Imported from the VOCABULARY leaf, never from the ops: a read-only lint must not pull the ensure
70
70
  // implementation — and through it the orchestration writer — into its import graph.
71
71
  import { RELAYED_ENSURE_TOKENS, RELAYED_FAILURE_CAUSES } from './ensure-vocabulary.mjs';
72
+ // The MCP registration's four public strings. Imported from the READ-ONLY leaf, never from the
73
+ // writer: a read-only lint must not pull the atomic-write core into its import graph.
74
+ import { ENABLED_KEY as MCP_ENABLED_KEY, MCP_JSON_REL, SERVER_NAME as MCP_SERVER_NAME, allowRulesFor } from './mcp-registration.mjs';
72
75
 
73
76
  const AUTONOMY_DOCTOR_DOC = 'references/modes/autonomy-doctor.md';
74
77
  const RECOMMENDATIONS_DOC = 'references/modes/recommendations.md';
@@ -81,6 +84,8 @@ const PROCEDURES_DOC = 'references/modes/procedures.md';
81
84
  const SET_FLOW_DOC = 'references/modes/set-flow.md';
82
85
  const RECEIPT_DEADLINE_DOC = 'references/modes/receipt-deadline.md';
83
86
  const GATES_DOC = 'references/modes/gates.md';
87
+ const MCP_DOC = 'references/modes/mcp.md';
88
+ const UNINSTALL_DOC = 'references/modes/uninstall.md';
84
89
  // One literal for the dispatch mode doc: the structure leaf already names it as the file it anchors
85
90
  // its table in, and a second copy here is exactly the drift this lint exists to catch.
86
91
  const DISPATCH_DOC = ADVISOR_MATRIX_DOC;
@@ -222,6 +227,15 @@ export const BINDINGS = Object.freeze([
222
227
  // sees the doc. Bound here, a future move fails a declared gate instead of leaving the contract
223
228
  // doc quietly describing a command the kit no longer emits.
224
229
  valueBinding('coverage-producer-body', COVERAGE_PRODUCER_BODY, COVERAGE_PRODUCER_BODY, [GATES_DOC]),
230
+ // The MCP registration's four public strings (mode: mcp). TWO docs quote each of them: the mode
231
+ // contract, which tells a reader what will be declared, and the uninstall KEEP list, which tells
232
+ // them what to edit to undo it — so a renamed key or tool would otherwise leave one of the two
233
+ // teaching a registration nobody writes. The server name is bound QUOTED: the bare word rides
234
+ // nearly every kit doc, and a token that is trivially present pins nothing.
235
+ valueBinding('mcp-json-rel', MCP_JSON_REL, `\`${MCP_JSON_REL}\``, [MCP_DOC, UNINSTALL_DOC]),
236
+ valueBinding('mcp-enabled-key', MCP_ENABLED_KEY, `\`${MCP_ENABLED_KEY}\``, [MCP_DOC, UNINSTALL_DOC]),
237
+ valueBinding('mcp-server-name', MCP_SERVER_NAME, `\`"${MCP_SERVER_NAME}"\``, [MCP_DOC, UNINSTALL_DOC]),
238
+ ...allowRulesFor().map((rule) => valueBinding(`mcp-allow-rule:${rule}`, rule, `\`${rule}\``, [MCP_DOC, UNINSTALL_DOC])),
225
239
  ].map((b) => Object.freeze(b)));
226
240
 
227
241
  // ── the pure checker (readText is injectable for hermetic tests) ────────────────────────
@@ -290,8 +304,10 @@ lagging-kit sentence, procedures.md), the receipt-deadline arrival contract, the
290
304
  FORM-only + aggregate-refusal contract and the routing advisor's two honesty sentences — the
291
305
  advice never gates, and the harness-subagent lane carries no availability verdict (dispatch.md), the
292
306
  runner's closed coverage= summary
293
- vocabulary (gates.md), and the canonical coverage-producer-body command the same doc prints in full
294
- (gates.md) — to
307
+ vocabulary (gates.md), the canonical coverage-producer-body command the same doc prints in full
308
+ (gates.md), and the MCP registration's five public strings mcp-json-rel, mcp-enabled-key,
309
+ mcp-server-name and the two mcp-allow-rule entries, each quoted by BOTH the mcp mode contract and the
310
+ uninstall KEEP list that undoes it (mcp.md, uninstall.md) — to
295
311
  the exact token its references/modes/*.md contract must carry, and
296
312
  asserts the CURRENT value renders into every bound file. A drifted doc, an unreadable bound file,
297
313
  or an absent token FAILS CLOSED.
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ // The CLI half of the finding-scope checker: argv and fs, no rule (the rule is fold-scope.mjs).
3
+ //
4
+ // `--plan` and `--queue` are REQUIRED and never defaulted. A project has two registers that can both
5
+ // look like "the queue" - the planning lifecycle's docs/plans/queue.md and a declared
6
+ // flow.debtQueue - and a checker that guessed would attest a deferral against the wrong file. The
7
+ // procedures advisor renders this command already populated, and NAMES which register it chose.
8
+ //
9
+ // Read-only: reads the two files it is pointed at, writes nothing, records nothing (advisory).
10
+ // Dependency-free, Node >= 22.
11
+ //
12
+ // Exit codes: 0 ACCEPT; 1 a matrix REFUSE; 2 usage - a missing/unknown flag, an unreadable path, or
13
+ // a refusal about the arguments themselves (an unknown or absent --class, an absent --claim).
14
+
15
+ import { readFileSync } from 'node:fs';
16
+ import { isDirectRun } from './direct-run.mjs';
17
+ import { CLASSES, ROW_FIELDS, decideFoldScope } from './fold-scope.mjs';
18
+
19
+ const HELP = `fold-scope — declare a finding's SCOPE before the edit (agent-workflow family).
20
+
21
+ Usage:
22
+ node fold-scope-cli.mjs --class <${CLASSES.join('|')}> --claim "<the invariant>" \\
23
+ --plan <plan-file> --queue <queue-file>
24
+
25
+ Every finding NAMES the invariant its fix enforces, BEFORE the edit. Where that invariant already
26
+ lives decides the arm, and this checker refuses a claim whose reference does not resolve:
27
+
28
+ in-scope the claim matches WITHIN ONE \`- \` bullet under the plan's ## Verification
29
+ (those bullets ARE the acceptance criteria) -> fold here.
30
+ new-invariant the claim matches NO acceptance bullet AND exactly one queue row carries all five
31
+ fields (${ROW_FIELDS.join(', ')}), its residual exposure
32
+ declared "not live" -> the narrow fix ships now, only the generalization defers.
33
+ blocking no correct narrow fix exists -> the phase does not close. There is no queue arm.
34
+
35
+ --plan and --queue are required and never defaulted: guessing the register would attest a deferral
36
+ against the wrong file. The check is advisory — nothing records that it ran, so a skipped or late
37
+ call is indistinguishable from a pre-edit declaration.
38
+
39
+ Exit codes: 0 ACCEPT; 1 REFUSE; 2 usage (missing/unknown flag, unreadable path, bad --class/--claim).`;
40
+
41
+ const FLAGS = ['class', 'claim', 'plan', 'queue'];
42
+
43
+ const parseArgs = (argv) => {
44
+ const opts = {};
45
+ for (let i = 0; i < argv.length; i += 1) {
46
+ const arg = argv[i];
47
+ const eq = arg.indexOf('=');
48
+ const name = arg.startsWith('--') ? arg.slice(2, eq === -1 ? undefined : eq) : null;
49
+ if (!name || !FLAGS.includes(name)) throw new Error(`unexpected argument "${arg}" (flags: ${FLAGS.map((f) => `--${f}`).join(', ')})`);
50
+ if (eq !== -1) {
51
+ opts[name] = arg.slice(eq + 1);
52
+ continue;
53
+ }
54
+ const value = argv[i + 1];
55
+ if (value === undefined || value.startsWith('--')) throw new Error(`--${name} requires a value`);
56
+ opts[name] = value;
57
+ i += 1;
58
+ }
59
+ for (const name of ['plan', 'queue']) {
60
+ if (!opts[name]) throw new Error(`--${name} is required and is never defaulted — name the file this claim is checked against`);
61
+ }
62
+ return opts;
63
+ };
64
+
65
+ // main(argv, deps) -> { code, stdout, stderr }. Never calls process.exit itself (the direct-run
66
+ // guard does), and never reads anything the caller did not point it at.
67
+ export const main = (argv, deps = {}) => {
68
+ const read = deps.readFileSync ?? readFileSync;
69
+ if (argv.includes('--help') || argv.includes('-h')) return { code: 0, stdout: HELP, stderr: '' };
70
+ let opts;
71
+ try {
72
+ opts = parseArgs(argv);
73
+ } catch (err) {
74
+ return { code: 2, stdout: '', stderr: `fold-scope: ${err.message}` };
75
+ }
76
+ const text = {};
77
+ for (const name of ['plan', 'queue']) {
78
+ try {
79
+ text[name] = read(opts[name], 'utf8');
80
+ } catch (err) {
81
+ return { code: 2, stdout: '', stderr: `fold-scope: --${name} "${opts[name]}" is unreadable — ${(err && err.message) || err}` };
82
+ }
83
+ }
84
+ const decided = decideFoldScope({ cls: opts.class, claim: opts.claim, planText: text.plan, queueText: text.queue });
85
+ return { code: decided.exit, stdout: decided.lines.join('\n'), stderr: '' };
86
+ };
87
+
88
+ if (isDirectRun(import.meta.url)) {
89
+ const result = main(process.argv.slice(2));
90
+ if (result.stdout) console.log(result.stdout);
91
+ if (result.stderr) console.error(result.stderr);
92
+ process.exit(result.code);
93
+ }