@sabaiway/agent-workflow-kit 14.34.0 → 14.36.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.
- package/CHANGELOG.md +64 -0
- package/README.md +7 -7
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/advisor-writers.md +51 -0
- package/references/agents/review-lens.md +1 -1
- package/references/modes/agents.md +4 -4
- package/references/modes/bootstrap.md +1 -1
- package/references/modes/gates.md +1 -1
- package/references/modes/mcp.md +3 -3
- package/references/modes/recommendations.md +10 -10
- package/references/modes/upgrade.md +1 -1
- package/references/templates/vehicles.json +5 -1
- package/tools/cheap-agents-read.mjs +25 -5
- package/tools/cheap-agents.mjs +28 -17
- package/tools/commands.mjs +1 -1
- package/tools/mcp-registration.mjs +1 -14
- package/tools/mcp.mjs +22 -23
- package/tools/recommendations.mjs +58 -49
- package/tools/reference-profile.mjs +2 -2
- package/tools/vehicle-settings.mjs +41 -19
- package/tools/write-lanes.mjs +35 -7
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md
|
|
6
6
|
|
|
7
|
-
The **read-only deployment advisor** — the deterministic section every `upgrade` run ends with, also invocable on its own. It computes what in THIS deployment is configured sub-optimally (allowlist not seeded, autonomy render drifted, sandbox unavailable, gates undeclared, a declared gate matrix that verifies nothing, bridge friction, sandbox-mask clutter, a configured `subagent` carrier whose executor vehicle cannot carry it, a difference from the reference profile the project has not declined, Jev not connected on this host, the Jev skill not installed for every agent) and renders **verdict-first**: one composed verdict line, then each item as **{severity · what is sub-optimal · the benefit in ONE plain line · `lane: chat`, `lane: harness` or `lane: user` · an optional `recipe:` line (the `bridge-tier` missing-entries line
|
|
7
|
+
The **read-only deployment advisor** — the deterministic section every `upgrade` run ends with, also invocable on its own. It computes what in THIS deployment is configured sub-optimally (allowlist not seeded, autonomy render drifted, sandbox unavailable, gates undeclared, a declared gate matrix that verifies nothing, bridge friction, sandbox-mask clutter, a configured `subagent` carrier whose executor vehicle cannot carry it, a difference from the reference profile the project has not declined, Jev not connected on this host, the Jev skill not installed for every agent) and renders **verdict-first**: one composed verdict line, then each item as **{severity · what is sub-optimal · the benefit in ONE plain line · `lane: chat`, `lane: harness` or `lane: user` · an optional `recipe:` line (an item's own facts or its alternative, such as the `bridge-tier` missing-entries line or the `worktrees-dir` ack fallback) · a harness item's `check:` line before its apply slot · the apply slot: a chat one-liner, a harness item's apply line or a user item's own step}**. The tool computes deterministic English DATA; **you PRESENT the section in the user's conversational language** — every fact, count and item from the tool, nothing added or dropped; commands, paths, hosts and rule strings stay **byte-exact**; show the raw tool block on request (the AD-032 report-contract lane — the tool cannot know the dialogue language, so the language rendering is your presentation layer).
|
|
8
8
|
|
|
9
9
|
**Live host/session facts are tool-composed only.** Every fact this section states about the current
|
|
10
10
|
host or session — prompts fired, sandbox scope, whether a bypass was needed, network reachability,
|
|
@@ -17,12 +17,12 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
|
|
|
17
17
|
|
|
18
18
|
1. **`--cwd` is REQUIRED** — the target project is explicit, never inferred from the shell's current directory (a subdirectory invocation still advises on the named root).
|
|
19
19
|
2. **Verdict first (present-even-when-empty, opening at the `## Recommendations (agent-workflow)` header).** With everything optimal the body is exactly `no recommendations — flow optimal.` — the empty-state line ALONE is the verdict, zero added lines. Every other state opens the body with ONE verdict line composed from the frozen templates: `{K} item(s) need attention` leads when any item's severity is `attention` (a CONFIGURED declaration that is broken, drifted, degrading or invalid — the deployment needs review); `{N} optional recommendation(s), apply any you want` covers the `optional` class (offers to enable an unconfigured capability), led by `nothing is broken` ONLY when no item needs attention AND no probe check was skipped (a skipped probe could hide an attention-class problem — the claim never overreaches); `optimality NOT attested — {M} probe check(s) skipped` is appended last whenever probe checks were skipped. (The `(s)` invariant form IS the pinned pluralization — no singular/plural branching.) Items render attention-first, each tagged `needs attention:` / `optional:`. A failed probe renders as a stated `⚠ skipped item …` line — never a crash, never a fabricated item.
|
|
20
|
-
3. **The apply-through-agent lane — an explicit informed-consent checkpoint:** the agent applies chat and harness items on the chat yes, harness items by the consent route below. A chat item is never retried outside the sandbox and every failure of a chat step is a STOP. A user item
|
|
20
|
+
3. **The apply-through-agent lane — an explicit informed-consent checkpoint:** the agent applies chat and harness items on the chat yes, harness items by the consent route below. A chat item is never retried outside the sandbox and every failure of a chat step is a STOP. A user item is the user's own step, handed over as worded with its posture note (e.g. `source-size.id-squatter`). After presenting, OFFER to apply; the user selects items in plain language; for each selected item you **surface its posture note inline** where one exists (the per-item notes below cover exactly the risk-marked keys). For a chat item: the user **explicitly confirms**, and **only then** — **no command runs before confirmation** — do you run the rendered command. Do NOT infer safety from the presence or absence of an `--apply` flag: most chat items' rendered command IS the mutation and completes on that one run, **including a no-`--apply` mutation** such as `spec-adoption`'s `ensure-configs --reconcile --only specs` (behind an edited file it prints a replace line that takes its own yes). Some items instead render a **dry-run preview** that changes nothing and prints an explicit follow-up `--apply`/mutating command to run NEXT (e.g. the `gates-declaration` seeder): after the SAME confirmation you run that printed follow-up command — no second ask, no improvisation (each writer keeps its own consent semantics: previews, `--apply` flags, refusals) — except profile-gap and adr-store-migration, whose apply line runs with --apply (appended, or in place of --dry-run) only on a second yes over the relayed preview. For `worktrees-dir` you apply the grant on the chat yes after its check, like every harness item, with no hand step to wait for; its `recipe:` line's `ack-write` preview is the ack fallback, run only on the user's word that the host ignores the settings key, instead of the grant, and against a trusted host NO no ack renders.
|
|
21
21
|
4. Registry strings (benefits + item texts) are frozen tool data, fact-true, ONE line under the pinned shape cap — posture/risk detail lives in the notes below at the consent moment, never inline in the overview. The dual velocity+security wording (`safer — blast radius bounded by the OS sandbox, not human attention`) rides ONLY the items with a real security delta (the autonomy render, the sandbox provisioning); the bridge-wrappers item claims **velocity only**.
|
|
22
22
|
|
|
23
23
|
The **consent route** for a harness item: run its `check:` line in the sandbox and show its result. On the check's `masked:` line, say so and run that same check line once more outside the sandbox, so it re-judges in the apply's own environment; the yes is taken on the outside result. Take the user's yes on that result and run its `apply:` line with that result's digest once, outside the sandbox at once, so the harness asks at the point of action (with the apply ask rule seeded, its own dialog); never another line, never unseen. When velocity-core, kit-tools-tier or bridge-tier is among the user's picks (velocity-core preferred), check, confirm and apply it, or leave it pending, before any other pick's check runs, so its apply seeds the ask rule before the other harness applies. When the harness's auto mode refuses the check or the apply, say so and give the one plain step: leave auto mode for this one action (Shift+Tab in the terminal, the mode indicator in an editor) and say yes, or run `npx @sabaiway/agent-workflow-kit@latest init` in a terminal of your own. On the user's word, run the same line once more when they left auto mode, never another, never unseen; when they ran `init` instead, run the advisor again and offer only what is still pending, each through this route from its check (after a restart `init` asked for, the next session's upgrade does it). When that refusal hits the velocity-core, kit-tools-tier or bridge-tier pick taken first, run no other pick's check until the user answers the plain step. A check that refuses (exit 1) leaves the item unapplied and pending, its result shown; a changed digest (exit 5) applies nothing, shows the new result and asks a new yes. A no, a harness refusal and any other failure of a harness apply leave the item pending with its output shown, and the agent continues. `init`, run in a console from the project's folder, is the route the user may take instead.
|
|
24
24
|
|
|
25
|
-
**The `executor-vehicle` item (the carrier the project already CONFIGURED — beside the `agents` offer, never the same item).** It fires on ONE pair of facts: a slot in `docs/ai/orchestration.json` names `subagent` while the executor vehicle (`.claude/agents/executor.md`) is `missing` or `unusable` — so every such slot silently degrades to solo. That is a configured declaration that is broken, hence **`attention`**, not an offer; its text names the state, its reason and how many configured slots are affected across the six activities (`plan-authoring`, `plan-execution`, `routine`, `feedback-triage`, `epic`, `task`), covering the subagent-capable slots `plan-authoring.author`, `plan-authoring.fold`, `plan-execution.execute`, `routine.carrier`, `epic.author`, `task.author` and `task.execute`. **`placed` renders NOTHING** (a working vehicle is not a recommendation); a differing executor body is the optional `executor-vehicle.differing` arm, re-derived on the chat yes after the check, a config naming no `subagent` slot renders no base item, and a config the validated reader refuses is a stated **skip**, never an item computed over a shape nothing accepted. It is deliberately DISTINCT from its two neighbours, neither of which can observe this state: the `agents` offer converges the moment nothing is left to PLACE — which a present-but-unusable executor also satisfies — and the review-recipe probe skips a slot configured `subagent` by construction. The agent applies it on the chat yes after the check; `init` previews the agents writer's own `cheap-agents.mjs` and applies it with `--apply` on the terminal yes as a route the user may take instead. Its `recipe:` line carries each precondition the advisor observes beside the apply slot, followed by the hidden-mode reconcile follow-up (`hide-footprint --reconcile`), for a hidden deployment only. Honest limit on convergence: the apply places an ABSENT vehicle, so it clears `missing`; it STOPs on a symlink, so on `unusable` the `recipe:` line names each refusal as a precondition; an executor whose frontmatter is invalid is re-derived by the apply itself. Risk profile: writes under `.claude/agents/` only — the executor's
|
|
25
|
+
**The `executor-vehicle` item (the carrier the project already CONFIGURED — beside the `agents` offer, never the same item).** It fires on ONE pair of facts: a slot in `docs/ai/orchestration.json` names `subagent` while the executor vehicle (`.claude/agents/executor.md`) is `missing` or `unusable` — so every such slot silently degrades to solo. That is a configured declaration that is broken, hence **`attention`**, not an offer; its text names the state, its reason and how many configured slots are affected across the six activities (`plan-authoring`, `plan-execution`, `routine`, `feedback-triage`, `epic`, `task`), covering the subagent-capable slots `plan-authoring.author`, `plan-authoring.fold`, `plan-execution.execute`, `routine.carrier`, `epic.author`, `task.author` and `task.execute`. **`placed` renders NOTHING** (a working vehicle is not a recommendation); a differing executor body is the optional `executor-vehicle.differing` arm, re-derived on the chat yes after the check, a config naming no `subagent` slot renders no base item, and a config the validated reader refuses is a stated **skip**, never an item computed over a shape nothing accepted. It is deliberately DISTINCT from its two neighbours, neither of which can observe this state: the `agents` offer converges the moment nothing is left to PLACE and the review lens matches its derived body — which a present-but-unusable executor also satisfies — and the review-recipe probe skips a slot configured `subagent` by construction. The agent applies it on the chat yes after the check; `init` previews the agents writer's own `cheap-agents.mjs` and applies it with `--apply` on the terminal yes as a route the user may take instead. Its `recipe:` line carries each precondition the advisor observes beside the apply slot, followed by the hidden-mode reconcile follow-up (`hide-footprint --reconcile`), for a hidden deployment only. Honest limit on convergence: the apply places an ABSENT vehicle, so it clears `missing`; it STOPs on a symlink, so on `unusable` the `recipe:` line names each refusal as a precondition; an executor whose frontmatter is invalid is re-derived by the apply itself. Risk profile: writes under `.claude/agents/` only — the executor's and the review lens's bodies re-derived from `docs/ai/vehicles.json` (the lens opus/high by default), every other vehicle create-only — never `settings.json`, never a commit.
|
|
26
26
|
|
|
27
27
|
**Per-item posture notes (the consent moment — surface BEFORE running or handing over the apply):**
|
|
28
28
|
|
|
@@ -33,20 +33,20 @@ A variant's posture note, where its key has one, is printed by `init` from this
|
|
|
33
33
|
- `read-lane` — enabling the opt-in read-only compound lane auto-approves *compounds* (and singles) of the seeded read-only core that carry ZERO shell metaprogramming: an UNATTENDED trust extension, bounded by the audited read-only core (never a command outside it; prompt-bypass only, never a sandbox bypass) and applied regardless of which of those core commands you seeded as individual settings rules. It is a PROJECT-PERSISTENT declaration in `docs/ai/lanes.json` — every future session, subagents' Bash too where the host fires hooks on subagent Bash, and (committed) every checkout. The apply depends on state: when the lane is OFF, it is the `gate-hook --read-lane` preview (whose own currency check refuses a stale hook — a pre-1.48 hook never reads `lanes.json`); when the placed hook is MISSING, the item instead surfaces the re-place offer; when it is STALE (an enabled lane over an old hook), the **stale** arm is an attention item on the harness lane: its writer is `gate-hook --apply --reseed`, which replaces the placed hook with the bundled bytes in place, and the agent applies it on the chat yes after the check, whose result names the replaced file with both digests; `init` applies it on the terminal y as the route the user may take instead. Risk profile: a bounded read-only trust-posture extension — no write/exec exposure beyond the audited core.
|
|
34
34
|
- `worktrees-dir` — the agent applies the item on the chat yes after the check: one `sandbox.filesystem.allowWrite` entry naming the worktrees parent itself, written to this copy's `.claude/settings.local.json` only, never the committed project settings, the parent created when missing. `init` applies it on the batch terminal yes as the route the user may take instead. Where the host honors the key, the entry widens the OS-sandbox WRITE surface to the whole parent dir: every sibling path under it (other repositories included) becomes agent-writable, for every later session and every worktree provisioned afterwards. Narrow it BEFORE the first apply: point `docs/ai/worktrees.json` `parentDir` at a dedicated dir — no apply removes an entry, so an earlier one stays until the user deletes it. A parent that is the home or holds it renders `worktrees-dir.home` instead, the user's own step. **Convergence** is two-path: a declared `sandbox.filesystem.allowWrite` entry covering the probed dir (either settings scope — `~` and `~/…` resolve against home, and coverage is path-segment-aware, so a grant on a sibling or on a child never counts), or the dir-bound acknowledgement `worktreesDirAck` in the family-owned `docs/ai/acks.json`, recorded by the `ack-write --lane worktrees-dir` line the `recipe:` line carries — the ack fallback for a host that ignores the key, run only on the user's word, instead of the grant; against a trusted host NO no ack renders. Neither is proof of write CAPABILITY: the provision preflight's real create+delete probe stays the runtime truth, and the ack is bound to the **resolved probe dir**, so the item re-fires only when that resolved dir changes. Risk profile: a real write-surface widening where honored — scope it deliberately.
|
|
35
35
|
|
|
36
|
-
- `gates-inert` — the gate matrix is DECLARED but verifies nothing, and the two causes have different remedies, so the item renders a different apply for each. **Cause A** — a canonical `coverage-check` gate with no producer gate declared BEFORE it (none at all, or one declared after it, which writes the lcov too late): the run certifies no coverage of its own — `coverage=not-run` when nothing wrote an lcov, or a verdict over STALE bytes an earlier run left in the git dir, which is worse because it reads as `coverage=certified`. This item is what surfaces that state at upgrade instead of leaving it to be noticed. The remedy needs the producer to run BEFORE the checker — declared or MOVED there — and what the fill can do about that is exactly what the apply says. When the checker is the LAST declared gate and the project's own scripts yield an offerable producer, the fill PLACES that producer before it, so the apply is the ordinary `gates-init` dry-run preview. Otherwise — no offerable producer, or a checker that is not last —
|
|
36
|
+
- `gates-inert` — the gate matrix is DECLARED but verifies nothing, and the two causes have different remedies, so the item renders a different apply for each. **Cause A** — a canonical `coverage-check` gate with no producer gate declared BEFORE it (none at all, or one declared after it, which writes the lcov too late): the run certifies no coverage of its own — `coverage=not-run` when nothing wrote an lcov, or a verdict over STALE bytes an earlier run left in the git dir, which is worse because it reads as `coverage=certified`. This item is what surfaces that state at upgrade instead of leaving it to be noticed. The remedy needs the producer to run BEFORE the checker — declared or MOVED there — and what the fill can do about that is exactly what the apply says. When the checker is the LAST declared gate and the project's own scripts yield an offerable producer, the fill PLACES that producer before it, so the apply is the ordinary `gates-init` dry-run preview. Otherwise — no offerable producer, or a checker that is not last — it is the user item `gates-inert.unfillable`: the user's own step is to edit `docs/ai/gates.json` (the exact producer form lives in `${CLAUDE_SKILL_DIR}/references/modes/gates.md`), because the fill never reorders entries it did not write. **Cause B** — every declared gate is one of the kit's own canonical checkers, so the matrix runs no project-verification command at all: the apply is the `gates-init` dry-run preview, the same consent-gated seeder the `gates-declaration` item renders, and after the SAME confirmation you run the `--apply` line it prints. Neither cause changes any gate result, exit code or receipt — the item adds no enforcement, only the offer. Risk profile: `docs/ai/gates.json` is a PRIVILEGED file (a declared gate is auto-approvable by the wired hook), so both causes land in the same place — where the fill can help, it declares a project-controlled command on your explicit yes; where it cannot, the edit is the user's own step.
|
|
37
37
|
|
|
38
|
-
- `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
|
|
38
|
+
- `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 a user item, the user's own step (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.
|
|
39
39
|
- `gate-hook` — the base arm is the ordinary opt-in wiring offer on the harness lane: the agent applies it on the chat yes after the check; `init` previews and applies it on the terminal yes as the route the user may take instead. 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 it is a harness item: its writer is `gate-hook --apply --reseed`, which replaces the placed hook with the bundled bytes in place (and wires the entry only while it is unwired), so the agent applies it on the chat yes after the check, whose result names the replaced file with the old and the new digest; `init` applies it on the terminal y as the route the user may take instead. 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: one placed hook file replaced by the bundled runtime; the declaration is never touched.
|
|
40
40
|
- `enforcement` — copies the bundled `references/scripts/*.mjs` runnable scripts and their tests, then runs the project's own hook installer, which installs the pre-commit hook. The lane is harness: the agent applies the item on the chat yes after the check, whose result shows each script the kit places with its bytes and the installer it runs with its bytes; `init` applies it on the batch terminal yes as the route the user may take instead. The copy is create-only: an existing file stands and nothing is written through a link, and the offer renders only when `scripts/` is absent or a plain directory contains no bundled basename. The hook invokes `node` directly, so a committer without it on PATH gets a loud failure. The installer refuses to overwrite a pre-commit hook it did not write: an install it refuses removes the scripts this apply copied, and the item is offered again with the installer's output shown; over a pre-commit hook the installer did not write the item is not offered — its skip names the user's own step. Risk profile: local tooling and a Git hook are added on the chat yes after the check, or on init's y.
|
|
41
41
|
- `attribution` — the agent applies the item on the chat yes after the check, setting one key in the project `.claude/settings.json`; `init` applies it on the terminal y as the route the user may take instead. An existing "includeCoAuthoredBy": false already converges the item unless an "attribution" key sets a commit or PR text; a local-scope override is the user's own edit. Risk profile: one settings key, nothing widened.
|
|
42
42
|
- `footprint-global` — the agent applies the item on the chat yes after the check, which prints each line it removes; `init` applies it on the terminal y as the route the user may take instead. Another of the user's repositories may rely on the same lines: a no keeps the block, and the item returns at the next run. The apply prints the removed lines as a backup. Risk profile: lines removed from the machine-global excludes file.
|
|
43
43
|
- `mcp-channel` — the offer is to REGISTER the kit's read-only stdio MCP server in this project on the harness lane: a registration is a command your MCP client will RUN. The agent applies it on the chat yes after the check; `init` previews the exact entry and applies it on the terminal y as the route the user may take instead. 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. The **`.differing`** arm is also a harness item: 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). The check shows the entry standing in `.mcp.json` and the entry it would write; the chat yes confirms the replacement. `init` previews `mcp --replace` and replaces only on the terminal y. 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; a differing registration is replaced only on the checked chat yes or the terminal y and every other declaration is kept.
|
|
44
|
-
- 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
|
|
45
|
-
- `spec-adoption` — the feature-spec layer's ADOPTION STATE, judged from `docs/ai/specs/` alone through the same census `spec-check --all` runs, so a store the checker would refuse to observe is never counted here either. Both arms are OFFERS (`optional`) — the layer is opt-in, and `attention` stays reserved for a configured declaration that is broken. **`not-adopted`** (no store): every plan then cites zero governing specs by default and nothing says adoption never started; its apply is the spec-layer ensure one-liner (`ensure-configs --reconcile --only specs`, the same seed `upgrade` runs — it seeds the reader/checker pairs and the store root, and skips with a stated reason where no Node evidence exists), and its `recipe:` line carries
|
|
44
|
+
- 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 a user item, the user's own step, 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 edit that is the user's own step.
|
|
45
|
+
- `spec-adoption` — the feature-spec layer's ADOPTION STATE, judged from `docs/ai/specs/` alone through the same census `spec-check --all` runs, so a store the checker would refuse to observe is never counted here either. Both arms are OFFERS (`optional`) — the layer is opt-in, and `attention` stays reserved for a configured declaration that is broken. **`not-adopted`** (no store): every plan then cites zero governing specs by default and nothing says adoption never started; its apply is the spec-layer ensure one-liner (`ensure-configs --reconcile --only specs`, the same seed `upgrade` runs — it seeds the reader/checker pairs and the store root, and skips with a stated reason where no Node evidence exists), and its `recipe:` line carries the decline alternative — the DECLINE preview (`ack-write --lane spec-adoption`, a dry-run that prints the exact `--apply`), which records `specAdoptionAck` in the family-owned `docs/ai/acks.json`. **The two are exclusive, and the order is part of the contract:** ask which the user wants, apart, BEFORE the confirmation; on "seed" run the apply; on "decline" the agent records it — the preview line, then on that same answer its printed `--apply`; never run the seed after a decline, nor the decline after the seed. **`adopting`** (a store with no live contract): an optional item naming the draft count; its apply is that same decline preview (a dry-run; after the SAME confirmation you run the `--apply` it prints), and the honest remedy is authoring a contract from the skill's `SPEC_TEMPLATE.md`. A recorded decline silences BOTH arms and shows on `status` as ` — declined`; one live contract makes the decline moot. An unreadable store is a stated skip, never `flow optimal`. Risk profile: the seed writes create-only files under `scripts/` and `docs/ai/specs/` and refreshes a script body a release shipped; behind an edited file it prints a replace line that takes its own yes; the decline writes one key into the ack store; nothing here authors a spec or blocks a plan.
|
|
46
46
|
- `profile-gap` — each item's apply is a PREVIEW that writes nothing, and consent takes two steps: the confirmed pick runs the preview and you relay every line it prints (the environment-resolved slot values, each checker `entry:` line with its trust-chain disclosure, a Communication region the insert plans in the same run); the same line with ` --apply` appended runs only on a SECOND yes over those lines, never carried by the first. Items whose apply lines are byte-equal are one writer and take one answer: the tier preview's `--apply` writes the absent slots, the store seed and the queue seed together; the checker verb writes only its own entries; the insert writes every region it plans, all or nothing, so a `communication-absent` skip beside a declined `story-sessions-section` gets its own ask after the insert's full preview. A no records nothing and the item stays on the screen. A DECLINE is asked apart from later, before anything runs, and runs `node ${CLAUDE_SKILL_DIR}/tools/tier-preview.mjs --decline --cwd <project-root>` only when the user refuses EVERY item the tier preview then reports offered: it records each of them at the shipped lineage, and a declined item leaves the screen until a lineage step changes the profile. An undecidable entry is a stated skip naming its reason, the remedy by reason: a prior → the upgrade's `lens` reconcile; an absent region → the insert; a customized region → a hand edit only (the lens preserves it), so that skip stands and withholds `flow optimal`. Risk profile: the tier preview's `--apply` rewrites `docs/ai/orchestration.json` in canonical form with the `_README` note and creates `docs/ai/epics/.gitkeep` and `docs/plans/queue.md` create-only; the insert adds regions to `docs/ai/agent_rules.md`; the checker verb adds entries to the PRIVILEGED `docs/ai/gates.json`; the decline writes `docs/ai/profile-declines.json`; nothing here commits.
|
|
47
|
-
- `jev-connect` — the lane is user and env-dependent: the connect command is the user's own step in a terminal of their own, where it asks for the key; the key never reaches the agent or the chat (the command refuses without a terminal, and a terminal is not consent). When the recipe line opens with `run the apply in <place>`, that place is the terminal to run it in; `init`, run there, offers the command on its own consent line. The command reads the key with no echo, makes one network call to api.typesafe.ai with the vendor's sample text only on the user's own run, and writes the key in plain text — as managed export lines in the shell's startup files (bash: ~/.bashrc and the login file, created as ~/.bash_profile when none exists; zsh: ~/.zshrc), as the kit's own file ~/.config/fish/conf.d/typesafe-api-key.fish for fish, or on Windows as the user environment variable TYPESAFE_API_KEY, stored in the Windows user environment; a process already running at the save can keep its old environment and hand it to what it starts — the guide's key mark confirms the agent sees it (a file is readable by any process running as that user, and by other local accounts when one is world-readable in a traversable home); the vendor keeps data under its processing agreement and offers zero retention only to enterprise accounts. The item reads the environment the agent's commands run in: a host setting that filters it shows the item while the key is set in the user's shell. The decline
|
|
48
|
-
- `jev-skill` — the lane is harness; both `jev-skill` and `jev-skill.earlier` are env-dependent: the agent applies the item on the chat yes after the check, showing the files it would write; it changes no harness setting. `init` applies it on the batch terminal yes as the route the user may take instead. When the recipe line opens with `run the apply in <place>`, that place is where a user who prefers their own terminal runs it. The skill is third-party text — the vendor's, MIT — that the kit ships pinned and installs as a verified copy into three agent skill roots: `~/.agents/skills`, the Claude Code skills dir (`CLAUDE_CONFIG_DIR` or `~/.claude`) and `~/.gemini/config/skills`. It is never updated silently: a copy at an earlier pin is replaced only by that command, and a foreign copy is left untouched and named on the recipe line. A copy the vendor's `skills` CLI wrote with the same bytes reads current, and a later pin move replaces it like a kit copy; a vendor plugin install is invisible to the file check, so the Claude Code root may read absent while the plugin serves it. Only when the user asks to see the plan does the agent run the command without `--apply` (a read-only dry run). The decline
|
|
49
|
-
- `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
|
|
47
|
+
- `jev-connect` — the lane is user and env-dependent: the connect command is the user's own step in a terminal of their own, where it asks for the key; the key never reaches the agent or the chat (the command refuses without a terminal, and a terminal is not consent). When the recipe line opens with `run the apply in <place>`, that place is the terminal to run it in; `init`, run there, offers the command on its own consent line. The command reads the key with no echo, makes one network call to api.typesafe.ai with the vendor's sample text only on the user's own run, and writes the key in plain text — as managed export lines in the shell's startup files (bash: ~/.bashrc and the login file, created as ~/.bash_profile when none exists; zsh: ~/.zshrc), as the kit's own file ~/.config/fish/conf.d/typesafe-api-key.fish for fish, or on Windows as the user environment variable TYPESAFE_API_KEY, stored in the Windows user environment; a process already running at the save can keep its old environment and hand it to what it starts — the guide's key mark confirms the agent sees it (a file is readable by any process running as that user, and by other local accounts when one is world-readable in a traversable home); the vendor keeps data under its processing agreement and offers zero retention only to enterprise accounts. The item reads the environment the agent's commands run in: a host setting that filters it shows the item while the key is set in the user's shell. The decline alternative rides the recipe line, asked apart BEFORE the confirmation (the `spec-adoption` order): on "connect" hand over the apply line; on "decline" the agent records it — the recipe line's preview, then its printed `--apply` — per project as `jevConnectAck` in `docs/ai/acks.json`. Risk profile: a plain-text key in the user's startup files or the Windows user environment, written by a command the user runs; one network call from that run.
|
|
48
|
+
- `jev-skill` — the lane is harness; both `jev-skill` and `jev-skill.earlier` are env-dependent: the agent applies the item on the chat yes after the check, showing the files it would write; it changes no harness setting. `init` applies it on the batch terminal yes as the route the user may take instead. When the recipe line opens with `run the apply in <place>`, that place is where a user who prefers their own terminal runs it. The skill is third-party text — the vendor's, MIT — that the kit ships pinned and installs as a verified copy into three agent skill roots: `~/.agents/skills`, the Claude Code skills dir (`CLAUDE_CONFIG_DIR` or `~/.claude`) and `~/.gemini/config/skills`. It is never updated silently: a copy at an earlier pin is replaced only by that command, and a foreign copy is left untouched and named on the recipe line. A copy the vendor's `skills` CLI wrote with the same bytes reads current, and a later pin move replaces it like a kit copy; a vendor plugin install is invisible to the file check, so the Claude Code root may read absent while the plugin serves it. Only when the user asks to see the plan does the agent run the command without `--apply` (a read-only dry run). The decline alternative rides the recipe line, asked apart BEFORE the confirmation: on "install" run the check line and, on the yes given on its result, the apply line by the consent route; on "decline" the agent records it — the recipe line's preview, then its printed `--apply` — per project as `jevSkillAck` in `docs/ai/acks.json`. Risk profile: third-party text written into the user's agent skill roots by the kit's skill command; no network.
|
|
49
|
+
- `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 the apply slot is a **`--dry-run`** — it writes nothing and prints the whole plan — and consent takes two steps: the first yes runs the dry run and you relay its whole plan (every line, the closing `--apply` line included); the same line with `--apply` in place of `--dry-run` runs only on a SECOND yes given over that plan. 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.
|
|
50
50
|
|
|
51
51
|
**Sandbox lanes (what to DO with the bridge surfaces, per host class):**
|
|
52
52
|
|
|
@@ -47,7 +47,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
47
47
|
|
|
48
48
|
**What the navigator ensure does.** `docs/ai/index.md` is the always-loaded navigator the entry point declares, and it is GENERATED — no template ships it, so a deployment that never ran the generator boots from a broken entry point (and, on a Node project, carries a pre-commit hook that fails its own index check). The ensure runs the bundled generator's finalizer and reports `regenerated` (it was missing or stale — it was written) or `already-current` (nothing written); under a hook module that is not the bundled one, a write the project's own checker rejects is put back → `customized-preserved`. It never skips a No-Node project: the generator runs from `${CLAUDE_SKILL_DIR}/references/scripts/` on the agent host, not from the project's `scripts/`. **Its position in the run-list is EARLY and therefore NOT authoritative** — `lens` (and, on the migrated path, steps 6–7) still change `docs/ai` afterwards — so the authoritative run is the LATE `--only index` rung documented at both exits; the early one is idempotent: an untouched tree changes nothing.
|
|
49
49
|
|
|
50
|
-
**What the spec-layer ensure does.** A deployment older than the feature-spec layer lacks `scripts/spec-schema.mjs` and `docs/ai/specs/index.md`, and its deployed `check-docs-size.mjs` predates the store collapse. In ONE order: the reader pair, then the checker pair — each seeded when absent and REFRESHED only while a file's bytes are a body a release shipped (an append-only digest catalog → `refreshed`; an edited body is preserved verbatim and withholds the writes that depend on it, the checker lane waiting on a byte-current reader pair); the store root is seeded from the bundled template, date rendered, ONLY behind a checker pair current after the run — an older or edited checker renders the store row by row and reds the hook's `--check-index`; edited files are replaced only on row 3's yes. One token by precedence: `seeded` > `refreshed` > `customized-preserved` (an edited pair, and this run wrote nothing) > `already-present`; `skipped-no-node-evidence` only when neither a root `package.json` nor any kit-seeded `scripts/*.mjs` is present (the same Node-evidence probe the enforcement-script ensure uses; an unreadable probe is `failed` / `node-evidence-unverifiable`); every line states what this run did, and a write that stops partway names what landed. The legacy-ADR instruct never withholds it. **The adoption state this seed leaves behind is reported, never assumed:** the Recommendations section (step 4 / 8) carries the `spec-adoption` item — `not adopted` (the store is absent: the seed offered, the decline
|
|
50
|
+
**What the spec-layer ensure does.** A deployment older than the feature-spec layer lacks `scripts/spec-schema.mjs` and `docs/ai/specs/index.md`, and its deployed `check-docs-size.mjs` predates the store collapse. In ONE order: the reader pair, then the checker pair — each seeded when absent and REFRESHED only while a file's bytes are a body a release shipped (an append-only digest catalog → `refreshed`; an edited body is preserved verbatim and withholds the writes that depend on it, the checker lane waiting on a byte-current reader pair); the store root is seeded from the bundled template, date rendered, ONLY behind a checker pair current after the run — an older or edited checker renders the store row by row and reds the hook's `--check-index`; edited files are replaced only on row 3's yes. One token by precedence: `seeded` > `refreshed` > `customized-preserved` (an edited pair, and this run wrote nothing) > `already-present`; `skipped-no-node-evidence` only when neither a root `package.json` nor any kit-seeded `scripts/*.mjs` is present (the same Node-evidence probe the enforcement-script ensure uses; an unreadable probe is `failed` / `node-evidence-unverifiable`); every line states what this run did, and a write that stops partway names what landed. The legacy-ADR instruct never withholds it. **The adoption state this seed leaves behind is reported, never assumed:** the Recommendations section (step 4 / 8) carries the `spec-adoption` item — `not adopted` (the store is absent: the seed offered, the decline the agent records), `adopting` (a store with no live contract: the decline offered), silenced by a recorded decline (`ack-write --lane spec-adoption`), and a store the probe cannot read is a stated skip — and `status` prints the same state on its own line.
|
|
51
51
|
|
|
52
52
|
**`gates-migration` — legacy gates.json migration (consented preview — D8).** An EXISTING declaration may still carry the retired review-ledger / fold-completeness checks. Run the preview (dry-run — writes NOTHING), show the user the exact plan, and only on an explicit yes re-run it with `--apply`: canonical legacy entries (matched by their documented single-invocation cmd forms) are REMOVED, the canonical `unit-tests` cmd gains the built-in lcov reporters, and the coverage-check gate is ADDED last — atomic and COMPLETE, so the migrated declaration satisfies `run-gates --final`. **The checker rides a PRODUCER or is not declared at all** (`${CLAUDE_SKILL_DIR}/references/modes/gates.md`): with no gate producing the lcov it reads, the migration does NOT add it, an already-declared one is reported INERT, the result is not called final-run-capable, and the preview prints the paste-ready suite cmd to declare by hand — nothing is ever removed for you. CUSTOMIZED entries are NEVER auto-touched: the preview names each with a paste-ready recovery, and the commit guard must NOT be installed until they are resolved. Of step 3's run-list, only this migration and `tools/checker-gates.mjs` under the `tier` ask change an existing gates.json (the FILL preview runs at init).
|
|
53
53
|
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_README": "The executor vehicle's model, effort and fallback as a project setting, read by the kit's vehicle-settings readers into one posture, which the placed executor's body follows: the agents writer derives it from model and effort, the upgrade reports a differing body as pending, and fallback names the model of the one retry; seeded once and never clobbered, hand-edited as three bare tokens with fallback differing from model.",
|
|
2
|
+
"_README": "The executor vehicle's model, effort and fallback as a project setting, read by the kit's vehicle-settings readers into one posture, which the placed executor's body follows: the agents writer derives it from model and effort, the upgrade reports a differing body as pending, and fallback names the model of the one retry; seeded once and never clobbered, hand-edited as three bare tokens with fallback differing from model. The optional review-lens section holds the bundled review lens's model and effort, opus and high by default: the agents writer derives the placed review-lens.md from them and re-derives a differing copy.",
|
|
3
3
|
"executor": {
|
|
4
4
|
"model": "opus",
|
|
5
5
|
"effort": "high",
|
|
6
6
|
"fallback": "sonnet"
|
|
7
|
+
},
|
|
8
|
+
"review-lens": {
|
|
9
|
+
"model": "opus",
|
|
10
|
+
"effort": "high"
|
|
7
11
|
}
|
|
8
12
|
}
|
|
@@ -8,7 +8,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { refuseDirectRun } from './direct-run.mjs';
|
|
10
10
|
import { deriveLensTemplate } from './review-roster-resolve.mjs';
|
|
11
|
-
import { EXECUTOR_DEFAULTS, VEHICLES_REL, readVehicles, resolveExecutor } from './vehicle-settings.mjs';
|
|
11
|
+
import { EXECUTOR_DEFAULTS, VEHICLES_REL, readVehicles, resolveExecutor, resolveReviewLens } from './vehicle-settings.mjs';
|
|
12
12
|
|
|
13
13
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
|
|
@@ -70,6 +70,12 @@ export const readBundledAgents = (deps = {}) => {
|
|
|
70
70
|
return templates;
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
const bundledTemplate = (name, deps) => {
|
|
74
|
+
const bundled = readBundledAgents(deps).find((item) => item.name === name);
|
|
75
|
+
if (!bundled) throw makeCheapAgentsError(CHEAP_AGENTS_BUNDLE, `${name} is missing from the bundle`);
|
|
76
|
+
return bundled;
|
|
77
|
+
};
|
|
78
|
+
|
|
73
79
|
// ── the preflight reads (velocity discipline: symlink-safe, stamp read, no writes) ────
|
|
74
80
|
|
|
75
81
|
export const readStamp = (absPath, fs) => {
|
|
@@ -130,11 +136,26 @@ export const deriveExecutorBody = (bundledContent, posture) =>
|
|
|
130
136
|
deriveLensTemplate(bundledContent, executorVehicleSpec(posture));
|
|
131
137
|
|
|
132
138
|
export const executorTemplate = (posture, deps = {}) => {
|
|
133
|
-
const bundled =
|
|
134
|
-
if (!bundled) throw makeCheapAgentsError(CHEAP_AGENTS_BUNDLE, `${EXECUTOR_VEHICLE} is missing from the bundle`);
|
|
139
|
+
const bundled = bundledTemplate(EXECUTOR_VEHICLE, deps);
|
|
135
140
|
return { name: EXECUTOR_VEHICLE, content: deriveExecutorBody(bundled.content, posture), rederive: true };
|
|
136
141
|
};
|
|
137
142
|
|
|
143
|
+
// ── the bundled review lens, derived from its posture like the executor ───────────────
|
|
144
|
+
|
|
145
|
+
export const REVIEW_LENS_VEHICLE = 'review-lens.md';
|
|
146
|
+
export const REVIEW_LENS_VEHICLE_REL = `${AGENTS_DIR}/${REVIEW_LENS_VEHICLE}`;
|
|
147
|
+
|
|
148
|
+
export const readReviewLensPosture = (projectDir, deps = {}) => {
|
|
149
|
+
const fs = readFsDeps(deps);
|
|
150
|
+
return resolveReviewLens(readVehicles(projectDir, fs.readFile, fs.lstat));
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export const reviewLensTemplate = (posture, deps = {}) => {
|
|
154
|
+
const bundled = bundledTemplate(REVIEW_LENS_VEHICLE, deps);
|
|
155
|
+
const spec = { stem: 'review-lens', model: posture.model, effort: posture.effort, derived: true };
|
|
156
|
+
return { name: REVIEW_LENS_VEHICLE, content: deriveLensTemplate(bundled.content, spec), rederive: true };
|
|
157
|
+
};
|
|
158
|
+
|
|
138
159
|
// The executor ensure's cell 6: a regular placed executor whose bytes differ from the body derived from
|
|
139
160
|
// the settings; the placed file's read failure throws.
|
|
140
161
|
export const executorBodyDiffers = (projectDir, deps = {}) => {
|
|
@@ -220,8 +241,7 @@ const lensFrontmatter = (frontmatter, stem) => {
|
|
|
220
241
|
|
|
221
242
|
const templateFor = (spec, deps) => {
|
|
222
243
|
if (spec.template == null) return null;
|
|
223
|
-
const bundled =
|
|
224
|
-
if (!bundled) throw makeCheapAgentsError(CHEAP_AGENTS_BUNDLE, `${spec.template}.md is missing from the bundle`);
|
|
244
|
+
const bundled = bundledTemplate(`${spec.template}.md`, deps);
|
|
225
245
|
return {
|
|
226
246
|
name: `${spec.stem}.md`,
|
|
227
247
|
content: spec.derived ? deriveLensTemplate(bundled.content, spec) : bundled.content,
|
package/tools/cheap-agents.mjs
CHANGED
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
// STOPs both, since the derived lenses it names cannot be known);
|
|
15
15
|
// • symlink-safe — a symlinked project root, `.claude`, `.claude/agents` or target file is a
|
|
16
16
|
// STOP, in a dry-run too, never a write-through;
|
|
17
|
-
// • preserves differing .claude/agents/ files (`customized — preserved`), except the executor
|
|
18
|
-
// on apply
|
|
17
|
+
// • preserves differing .claude/agents/ files (`customized — preserved`), except the executor
|
|
18
|
+
// and the bundled review lens: on apply each body is re-derived from docs/ai/vehicles.json
|
|
19
|
+
// (the lens from its review-lens section, opus and high by default), replacing the earlier body;
|
|
19
20
|
// an identical file is `already current` (idempotent re-run);
|
|
20
21
|
// • writes ONLY under .claude/agents/ — never settings.json / settings.local.json, never
|
|
21
22
|
// commits.
|
|
@@ -38,7 +39,7 @@ import { shellQuoteArg } from './repo-lex.mjs';
|
|
|
38
39
|
import { loadConfig } from './orchestration-config.mjs';
|
|
39
40
|
import { lensMembersOf } from './review-roster.mjs';
|
|
40
41
|
import { deriveLensTemplate, lensVehicleSpec } from './review-roster-resolve.mjs';
|
|
41
|
-
import { loadVehicles, resolveExecutor } from './vehicle-settings.mjs';
|
|
42
|
+
import { loadVehicles, resolveExecutor, resolveReviewLens } from './vehicle-settings.mjs';
|
|
42
43
|
import { safeLine } from './carriers.mjs';
|
|
43
44
|
import { writeContainedFileAtomic } from './atomic-write.mjs';
|
|
44
45
|
// The READ core, never a second copy: the bundle, the placement plan and the executor survey live
|
|
@@ -55,7 +56,9 @@ import {
|
|
|
55
56
|
CHEAP_AGENTS_CONFIG,
|
|
56
57
|
CHEAP_AGENTS_VEHICLES,
|
|
57
58
|
EXECUTOR_VEHICLE,
|
|
59
|
+
REVIEW_LENS_VEHICLE,
|
|
58
60
|
executorTemplate,
|
|
61
|
+
reviewLensTemplate,
|
|
59
62
|
makeCheapAgentsError,
|
|
60
63
|
readFsDeps,
|
|
61
64
|
readBundledAgents,
|
|
@@ -100,15 +103,16 @@ const USAGE = `usage: cheap-agents [--dry-run | --apply] [--cwd <dir>] [--help]
|
|
|
100
103
|
|
|
101
104
|
Places the bundled subagent definitions into the project's ${AGENTS_DIR}/. Four vehicles grant NO
|
|
102
105
|
shell: three ride a cheap model (haiku/low) for mechanical work — extraction sweeps, changelog
|
|
103
|
-
fact-skeletons, gate triage — and review-lens is a read-only REVIEW vehicle
|
|
104
|
-
|
|
106
|
+
fact-skeletons, gate triage — and review-lens is a read-only REVIEW vehicle, opus/high unless
|
|
107
|
+
docs/ai/vehicles.json sets another posture. The fifth, executor, is the ONE full-tool vehicle: dispatched only for a bounded execution,
|
|
105
108
|
authoring, or write-capable routine slice the orchestrator verifies, never for read-only work, and
|
|
106
109
|
it never commits.
|
|
107
110
|
Default is --dry-run (a preview; writes nothing). --apply writes.
|
|
108
111
|
Configured derived review lenses are planned and placed beside the bundled vehicles.
|
|
109
|
-
An existing file with DIFFERENT content is preserved and reported, never overwritten, except the executor
|
|
110
|
-
The executor's model and effort come from docs/ai/vehicles.json,
|
|
111
|
-
from
|
|
112
|
+
An existing file with DIFFERENT content is preserved and reported, never overwritten, except the executor
|
|
113
|
+
and the review lens. The executor's model and effort come from docs/ai/vehicles.json, the review lens's
|
|
114
|
+
from its review-lens section there (opus and high by default), and a placed executor or review-lens.md
|
|
115
|
+
whose bytes differ from its derived body is re-derived on --apply (the earlier body is replaced, never preserved).`;
|
|
112
116
|
|
|
113
117
|
export const fail = (exitCode, message) => Object.assign(new Error(message), { exitCode });
|
|
114
118
|
|
|
@@ -151,11 +155,12 @@ const loadConfigOrStop = (cwd, deps) => {
|
|
|
151
155
|
}
|
|
152
156
|
};
|
|
153
157
|
|
|
154
|
-
const
|
|
158
|
+
const loadPosturesOrStop = (cwd, deps) => {
|
|
155
159
|
try {
|
|
156
|
-
|
|
160
|
+
const answer = loadVehicles(cwd, deps.readFile, deps.lstat);
|
|
161
|
+
return { posture: resolveExecutor(answer).posture, lensPosture: resolveReviewLens(answer).posture };
|
|
157
162
|
} catch (err) {
|
|
158
|
-
throw makeCheapAgentsError(CHEAP_AGENTS_VEHICLES, `${err?.message ?? err} — the agents writer cannot derive the executor
|
|
163
|
+
throw makeCheapAgentsError(CHEAP_AGENTS_VEHICLES, `${err?.message ?? err} — the agents writer cannot derive the executor and review-lens vehicles — nothing is placed`);
|
|
159
164
|
}
|
|
160
165
|
};
|
|
161
166
|
|
|
@@ -182,10 +187,12 @@ export const writeCheapAgents = ({ cwd, dryRun = true } = {}, deps = {}) => {
|
|
|
182
187
|
const fs = writeFsDeps(deps);
|
|
183
188
|
const projectDir = cwd ?? process.cwd();
|
|
184
189
|
refuseSymlinkedRoot(projectDir, deps);
|
|
185
|
-
const posture =
|
|
186
|
-
const derived = [
|
|
190
|
+
const { posture, lensPosture } = loadPosturesOrStop(projectDir, deps);
|
|
191
|
+
const derived = [
|
|
192
|
+
executorTemplate(posture, deps), reviewLensTemplate(lensPosture, deps), ...configuredDerivedTemplates(projectDir, deps),
|
|
193
|
+
];
|
|
187
194
|
const preflight = preflightCheapAgents({ cwd: projectDir, derived }, deps);
|
|
188
|
-
if (dryRun) return { wrote: false, dryRun: true, posture, ...preflight };
|
|
195
|
+
if (dryRun) return { wrote: false, dryRun: true, posture, lensPosture, ...preflight };
|
|
189
196
|
|
|
190
197
|
if (!preflight.stampOk) {
|
|
191
198
|
throw makeCheapAgentsError(
|
|
@@ -202,7 +209,7 @@ export const writeCheapAgents = ({ cwd, dryRun = true } = {}, deps = {}) => {
|
|
|
202
209
|
});
|
|
203
210
|
} else fs.writeFile(item.abs, item.content, UTF8);
|
|
204
211
|
}
|
|
205
|
-
return { wrote: toWrite.length > 0, dryRun: false, posture, ...preflight };
|
|
212
|
+
return { wrote: toWrite.length > 0, dryRun: false, posture, lensPosture, ...preflight };
|
|
206
213
|
};
|
|
207
214
|
|
|
208
215
|
// ── report ────────────────────────────────────────────────────────────────────────────
|
|
@@ -213,9 +220,12 @@ const ACTION_LABEL = {
|
|
|
213
220
|
're-derive': 're-derived — a hand edit was replaced; set the model in docs/ai/vehicles.json',
|
|
214
221
|
'customized-preserved': 'customized — preserved (delete the file to reseed from the bundle)',
|
|
215
222
|
};
|
|
223
|
+
const LENS_REDERIVE_LABEL = 're-derived — the earlier body was replaced; set its model and effort in docs/ai/vehicles.json';
|
|
224
|
+
const labelOf = (item) =>
|
|
225
|
+
(item.action === 're-derive' && item.name === REVIEW_LENS_VEHICLE ? LENS_REDERIVE_LABEL : ACTION_LABEL[item.action]);
|
|
216
226
|
|
|
217
227
|
export const formatResult = (result) => {
|
|
218
|
-
const { posture } = result;
|
|
228
|
+
const { posture, lensPosture } = result;
|
|
219
229
|
const lines = [
|
|
220
230
|
result.dryRun
|
|
221
231
|
? 'agent-workflow subagent vehicles — DRY RUN (no changes)'
|
|
@@ -223,7 +233,7 @@ export const formatResult = (result) => {
|
|
|
223
233
|
];
|
|
224
234
|
for (const item of result.plan) {
|
|
225
235
|
const verb = result.dryRun && item.action === 'place' ? 'would place'
|
|
226
|
-
: result.dryRun && item.action === 're-derive' ? 'would re-derive' :
|
|
236
|
+
: result.dryRun && item.action === 're-derive' ? 'would re-derive' : labelOf(item);
|
|
227
237
|
lines.push(` - ${item.rel}: ${verb}`);
|
|
228
238
|
}
|
|
229
239
|
if (!result.stampOk) {
|
|
@@ -235,6 +245,7 @@ export const formatResult = (result) => {
|
|
|
235
245
|
`three of those ride the cheap lane (model: haiku, effort: low) for mechanical work; ${FALLBACK_LENS_ADDITIONAL_ONLY}`,
|
|
236
246
|
'executor is the one FULL-TOOL vehicle: dispatched only for a bounded execution, authoring, or write-capable routine slice the orchestrator verifies, never for read-only work, and it never commits.',
|
|
237
247
|
`executor posture: model=${safeLine(posture.model)} effort=${safeLine(posture.effort)} fallback=${safeLine(posture.fallback)} (source: ${safeLine(posture.source)}) — set in docs/ai/vehicles.json`,
|
|
248
|
+
`review-lens posture: model=${safeLine(lensPosture.model)} effort=${safeLine(lensPosture.effort)} (source: ${safeLine(lensPosture.source)}) — set in docs/ai/vehicles.json`,
|
|
238
249
|
);
|
|
239
250
|
// A preview must print the EXACT command that applies it. The advisor renders this dry-run as an
|
|
240
251
|
// item's one-liner, and that flow's contract is "run the printed command, no improvisation" — a
|
package/tools/commands.mjs
CHANGED
|
@@ -194,7 +194,7 @@ const CATALOG = [
|
|
|
194
194
|
invocation: invocationOf('mcp'),
|
|
195
195
|
group: 'Configure',
|
|
196
196
|
kind: GUARDED,
|
|
197
|
-
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
|
|
197
|
+
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 writes nothing and names the advisor’s mcp-channel item, which applies it by the consent route.',
|
|
198
198
|
},
|
|
199
199
|
{
|
|
200
200
|
key: 'recipes',
|
|
@@ -311,7 +311,7 @@ const classifyDir = (abs, io) => {
|
|
|
311
311
|
return { state: STATE.FOREIGN, className: describeNonRegular(st) };
|
|
312
312
|
};
|
|
313
313
|
|
|
314
|
-
// ── the merges (ONE definition, shared by the writer's bodies
|
|
314
|
+
// ── the merges (ONE definition, shared by the writer's bodies) ──
|
|
315
315
|
// Merge-don't-clobber in both: every foreign server, key and allow rule is carried over untouched,
|
|
316
316
|
// and our own entry/rules are added idempotently.
|
|
317
317
|
|
|
@@ -335,19 +335,6 @@ export const mergeSettings = (registration) => {
|
|
|
335
335
|
|
|
336
336
|
export const formatJson = (data, eol) => `${JSON.stringify(data, null, JSON_INDENT).replaceAll(LF, eol)}${eol}`;
|
|
337
337
|
|
|
338
|
-
// The two hand-apply bodies — the whole output of the MASKED arm.
|
|
339
|
-
//
|
|
340
|
-
// They are deliberately DIFFERENT shapes, because what the kit knows about each file differs. The
|
|
341
|
-
// settings file was OBSERVABLE — and read where present — so its body is a real merge: it already
|
|
342
|
-
// carries every foreign key it had. The masked `.mcp.json` was not observable at all, so a
|
|
343
|
-
// whole-file body would name only our server and, pasted as
|
|
344
|
-
// instructed, would delete every foreign server the mask hid. `mcpEntry` is therefore the ENTRY
|
|
345
|
-
// ALONE, to be merged under `mcpServers` by a human who can see what is actually in that file.
|
|
346
|
-
export const renderFragments = (registration) => ({
|
|
347
|
-
mcpEntry: formatJson(registration.entry, registration.mcpJson.eol),
|
|
348
|
-
settings: formatJson(mergeSettings(registration), registration.settings.eol),
|
|
349
|
-
});
|
|
350
|
-
|
|
351
338
|
// A LIBRARY module the mode doc names by path — so someone can try to run it. A no-op on import;
|
|
352
339
|
// on a direct run it points at the command that acts on what this module only reports.
|
|
353
340
|
refuseDirectRun(import.meta.url);
|
package/tools/mcp.mjs
CHANGED
|
@@ -22,21 +22,21 @@
|
|
|
22
22
|
// • the preflight is READ-ONLY: an absent `.claude/` is a NAMED state, and the dir is created on
|
|
23
23
|
// `--apply` only (assertCreatableDirSafe mkdirs, so it may not run in a preview);
|
|
24
24
|
// • a MASKED target (an OS sandbox injects a character device where `.mcp.json` would be — this
|
|
25
|
-
// repo is exactly that case) is not a failure: the kit
|
|
26
|
-
//
|
|
25
|
+
// repo is exactly that case) is not a failure: the kit writes nothing, exits 0 and names the
|
|
26
|
+
// advisor's mcp-channel item, whose check line re-judges outside the sandbox;
|
|
27
27
|
// • never `settings.local.json`; never commits.
|
|
28
28
|
//
|
|
29
29
|
// The read half lives in mcp-registration.mjs, which the advisor and `uninstall` use — so what is
|
|
30
30
|
// there and what would be written are computed by one module, never by two that can disagree.
|
|
31
31
|
//
|
|
32
|
-
// Exit codes: 0 done / dry-run (incl. the
|
|
32
|
+
// Exit codes: 0 done / dry-run (incl. the masked state); 1 precondition STOP; 2 usage.
|
|
33
33
|
// Dependency-free beyond the kit's own exports, Node >= 22. No side effects on import.
|
|
34
34
|
|
|
35
35
|
import {
|
|
36
36
|
lstatSync, openSync, closeSync, fstatSync, writeFileSync, ftruncateSync, fsyncSync,
|
|
37
37
|
renameSync, constants,
|
|
38
38
|
} from 'node:fs';
|
|
39
|
-
import { join, resolve } from 'node:path';
|
|
39
|
+
import { dirname, join, resolve } from 'node:path';
|
|
40
40
|
import { fileURLToPath } from 'node:url';
|
|
41
41
|
import { assertCreatableDirSafe, writeContainedFileAtomic } from './atomic-write.mjs';
|
|
42
42
|
import { isDirectRun } from './direct-run.mjs';
|
|
@@ -46,7 +46,6 @@ import {
|
|
|
46
46
|
CLAUDE_DIR_REL,
|
|
47
47
|
ENABLED_KEY,
|
|
48
48
|
MCP_JSON_REL,
|
|
49
|
-
SERVERS_KEY,
|
|
50
49
|
SERVER_NAME,
|
|
51
50
|
SETTINGS_REL,
|
|
52
51
|
STATE,
|
|
@@ -54,9 +53,9 @@ import {
|
|
|
54
53
|
mergeMcpJson,
|
|
55
54
|
mergeSettings,
|
|
56
55
|
readRegistration,
|
|
57
|
-
renderFragments,
|
|
58
56
|
} from './mcp-registration.mjs';
|
|
59
57
|
import { shellQuoteArg } from './review-state.mjs';
|
|
58
|
+
import { applySlot } from './write-lanes.mjs';
|
|
60
59
|
|
|
61
60
|
const q = shellQuoteArg;
|
|
62
61
|
|
|
@@ -83,7 +82,8 @@ Default is --dry-run (a preview that prints the exact entry and writes nothing).
|
|
|
83
82
|
|
|
84
83
|
Without --replace, an existing "${SERVER_NAME}" entry that STRUCTURALLY DIFFERS is refused unwritten (key
|
|
85
84
|
order is ignored). Where ${MCP_JSON_REL} is a device node, FIFO or socket — an OS sandbox
|
|
86
|
-
mask is the usual cause —
|
|
85
|
+
mask is the usual cause — nothing is written, and the report names the advisor's mcp-channel
|
|
86
|
+
item, which applies the registration by the consent route.
|
|
87
87
|
Never writes settings.local.json; never commits.`;
|
|
88
88
|
|
|
89
89
|
export const fail = (exitCode, message) => Object.assign(new Error(message), { exitCode });
|
|
@@ -105,9 +105,9 @@ const lstatNoFollow = (path, lstat = lstatSync) => {
|
|
|
105
105
|
// A target we could not read or parse is never overwritten: a merge over a file whose current
|
|
106
106
|
// content is unknown is a clobber wearing a merge's name.
|
|
107
107
|
//
|
|
108
|
-
// `maskedAllowed` is TRUE for `.mcp.json` alone. That file has a sanctioned
|
|
109
|
-
//
|
|
110
|
-
// mask there is an ordinary refusal rather than a second, unplanned success path.
|
|
108
|
+
// `maskedAllowed` is TRUE for `.mcp.json` alone. That file has a sanctioned route — the advisor's
|
|
109
|
+
// mcp-channel item, whose check line re-judges outside the sandbox — and `settings.json` has none, so
|
|
110
|
+
// a mask there is an ordinary refusal rather than a second, unplanned success path.
|
|
111
111
|
const assertTargetUsable = (target, { maskedAllowed = false } = {}) => {
|
|
112
112
|
if (target.state === STATE.MASKED) {
|
|
113
113
|
if (maskedAllowed) return;
|
|
@@ -168,7 +168,7 @@ export const preflightMcp = ({ cwd, replace = false }, deps = {}) => {
|
|
|
168
168
|
};
|
|
169
169
|
|
|
170
170
|
// The plan is pure over the registration: what is missing, and the exact body each file would get.
|
|
171
|
-
// Both bodies are built even when nothing is written — they ARE the preview
|
|
171
|
+
// Both bodies are built even when nothing is written — they ARE the preview.
|
|
172
172
|
export const planMcp = (registration) => ({
|
|
173
173
|
writeMcpJson: !registration.mcpJson.matches,
|
|
174
174
|
writeSettings: !registration.settings.complete,
|
|
@@ -225,9 +225,8 @@ const writeTarget = (root, target, body, deps, stop) => {
|
|
|
225
225
|
|
|
226
226
|
export const writeMcp = ({ cwd, dryRun = true, replace = false } = {}, deps = {}) => {
|
|
227
227
|
const preflight = preflightMcp({ cwd: cwd ?? deps.cwd ?? process.cwd(), replace }, deps);
|
|
228
|
-
const base = { ...preflight, dryRun, wrote: false };
|
|
229
|
-
if (preflight.masked) return
|
|
230
|
-
if (dryRun) return base;
|
|
228
|
+
const base = { ...preflight, dryRun, replace, wrote: false };
|
|
229
|
+
if (preflight.masked || dryRun) return base;
|
|
231
230
|
|
|
232
231
|
const { root, registration, plan } = preflight;
|
|
233
232
|
const stop = (message) => makeMcpError(MCP_SYMLINK, message);
|
|
@@ -286,19 +285,19 @@ const settingsLine = (result) => {
|
|
|
286
285
|
return ` - ${SETTINGS_REL}: ${dryRun ? 'would set' : 'set'} ${parts.join(' · ')}`;
|
|
287
286
|
};
|
|
288
287
|
|
|
289
|
-
// The
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
// whole-file body pasted as instructed would delete every server it could not see.
|
|
288
|
+
// The masked report names the item that applies the registration by the consent route: its check
|
|
289
|
+
// line, run once more outside the sandbox, reads the real file this mode cannot see through the mask.
|
|
290
|
+
// A run under --replace routes through the differing arm, whose check and apply carry the replace.
|
|
293
291
|
const maskedReport = (result) => {
|
|
294
292
|
const target = result.registration.mcpJson;
|
|
293
|
+
const variant = result.replace ? 'mcp-channel.differing' : 'mcp-channel';
|
|
294
|
+
const { check, apply } = applySlot(variant, { toolsDir: dirname(MCP_TOOL), root: result.root });
|
|
295
295
|
return [
|
|
296
296
|
// The CLASS is what was observed; the sandbox mask is the usual CAUSE but is not established here.
|
|
297
|
-
`agent-workflow MCP registration —
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
`
|
|
301
|
-
indented(result.fragments.settings),
|
|
297
|
+
`agent-workflow MCP registration — nothing was written: ${MCP_JSON_REL} is a ${target.className} (an OS sandbox device mask is the usual cause).`,
|
|
298
|
+
" the advisor's mcp-channel item applies the registration by the consent route — its check line runs once more outside the sandbox and the apply line runs on the yes given on that result:",
|
|
299
|
+
` check: ${check}`,
|
|
300
|
+
` apply: ${apply}`,
|
|
302
301
|
POSTURE_LINE,
|
|
303
302
|
HIDDEN_MODE_LINE,
|
|
304
303
|
].join(LF);
|