@sabaiway/agent-workflow-kit 14.33.0 → 14.35.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 +77 -0
- package/README.md +6 -6
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/migrations/1.2.0-agent-attribution.md +10 -8
- package/migrations/3.0.0-hardened-core-loop.md +9 -13
- package/package.json +1 -1
- package/references/advisor-writers.md +51 -0
- package/references/contracts.md +2 -2
- package/references/modes/bootstrap.md +5 -3
- package/references/modes/gates.md +1 -1
- package/references/modes/mcp.md +3 -3
- package/references/modes/recommendations.md +12 -10
- package/references/modes/upgrade.md +6 -6
- package/references/shared/composition-handoff.md +3 -3
- package/references/shared/deploy-tail.md +2 -2
- package/references/templates/vehicles.json +1 -1
- package/tools/apply-danger-check.mjs +37 -12
- package/tools/attribution.mjs +133 -0
- package/tools/cheap-agents-read.mjs +12 -0
- package/tools/commands.mjs +1 -1
- package/tools/commit-guard.mjs +3 -16
- package/tools/ensure-configs.mjs +6 -6
- package/tools/ensure-executor.mjs +13 -30
- package/tools/ensure-vocabulary.mjs +4 -4
- package/tools/git-hooks.mjs +41 -3
- package/tools/hide-footprint.mjs +87 -61
- package/tools/init-project.mjs +1 -1
- package/tools/mcp-registration.mjs +1 -14
- package/tools/mcp.mjs +22 -23
- package/tools/recommendations.mjs +131 -68
- package/tools/upgrade-runlist.mjs +1 -1
- package/tools/write-lanes.mjs +42 -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`
|
|
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 body re-derived from `docs/ai/vehicles.json`, 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,18 +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
|
+
- `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
|
+
- `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.
|
|
41
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.
|
|
42
|
-
- 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
|
|
43
|
-
- `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.
|
|
44
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.
|
|
45
|
-
- `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
|
|
46
|
-
- `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
|
|
47
|
-
- `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.
|
|
48
50
|
|
|
49
51
|
**Sandbox lanes (what to DO with the bridge surfaces, per host class):**
|
|
50
52
|
|
|
@@ -12,7 +12,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
12
12
|
|
|
13
13
|
1. `pointers` — `node ${CLAUDE_SKILL_DIR}/tools/inject-methodology.mjs reconcile <project>/AGENTS.md` → per pointer: added · already present · skipped (reported) · a hard STOP.
|
|
14
14
|
2. `footprint` — `node ${CLAUDE_SKILL_DIR}/tools/hide-footprint.mjs --dir <project> --reconcile --dry-run` → visibility: visible · ambiguous · hidden — consent (conditional): ambiguous → ask which it is BEFORE anything; hidden → the conditional re-run without `--dry-run` (its surfaced paths ask per bootstrap step 9).
|
|
15
|
-
3. `configs` — `node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --cwd <project>` → one line per ensure: `seeded` / `note-refreshed` / `refreshed` / `regenerated` / `
|
|
15
|
+
3. `configs` — `node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --cwd <project>` → one line per ensure: `seeded` / `note-refreshed` / `refreshed` / `regenerated` / `pending` / `already-current` / `customized-preserved` / `malformed-preserved` / `already-present` / `not-placed` / `skipped-no-node-evidence` / `old-adr-layout-migration-instructed` / `failed` — consent (conditional): a specs replace line → ONE yes naming every file the custom lines name and saying their edits are overwritten, asked even on a non-zero exit; on the yes run the printed line, then STOP if that run fails or an op it does not re-run failed; on a no STOP on any failed op.
|
|
16
16
|
4. `gates-migration` — `node ${CLAUDE_SKILL_DIR}/references/scripts/migrate-gates.mjs --kit-tools ${CLAUDE_SKILL_DIR}/tools --cwd <project>` → the preview plan · INERT checker · CUSTOMIZED entries, each named — consent: apply only on an explicit yes, re-run with `--apply`.
|
|
17
17
|
5. `bridges` — `node ${CLAUDE_SKILL_DIR}/tools/setup-backends.mjs --refresh-placed` → per-bridge lines: refreshed · already current · skipped — with its stated reason (not placed / newer than the bundle / unsupported host) · `skipped-readonly` · could not refresh.
|
|
18
18
|
6. `lens` — `node ${CLAUDE_SKILL_DIR}/tools/lens-region.mjs reconcile <project>/docs/ai/agent_rules.md` → per section: refreshed · already current · custom edit preserved · file absent / engine too old — skipped · over the line cap — refused · section absent — noted · a fully absent/invalid engine → hard STOP.
|
|
@@ -31,9 +31,9 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
31
31
|
|
|
32
32
|
**No-Node project:** the fragments live only in the **installed `agent-workflow-engine`** (`references/methodology-slot.md` + `references/orchestration-slot.md` + `references/autonomy-slot.md`, under `~/.claude/skills/agent-workflow-engine` or `$AGENT_WORKFLOW_ENGINE_DIR`) — there is no bundled copy, and a No-Node host cannot run the `npx` engine install. Open `AGENTS.md` and classify **each** pointer by hand: a **filled / customized** pair → leave it verbatim (no engine needed); a **malformed** pair (not exactly one ordered `start → end`) → STOP, do not edit. A pair that needs filling — **absent markers OR a present-but-empty pair** — needs the engine's fragment, so: if the engine is **not installed**, that pointer **cannot be added** — report it plainly (the methodology is already in `docs/ai/agent_rules.md`; the recipes are available via `/agent-workflow-kit recipes`; the autonomy policy via `/agent-workflow-kit set-autonomy`; install the engine to add the pointers). If the engine **is** present, **count the lines first** — if adding/filling would take the file over 100 lines, **skip that pointer and report the skip** (methodology first, then orchestration, then autonomy; each chained pair sits right under the previous pair's end marker). Fill each empty pair from its engine fragment (`methodology-slot.md` / `orchestration-slot.md` / `autonomy-slot.md`) — never inline a copy (that would re-create the retired mirror).
|
|
33
33
|
|
|
34
|
-
**`footprint` — hidden-mode footprint reconcile (D9 / AD-014).** A deployment does not record whether it chose `hidden`, so first **infer visibility** — the dry-run writes **zero bytes** and reports one of — **visible** (the entry point is tracked) → nothing to do; **ambiguous** (untracked but not ignored — could be a fresh uncommitted repo, or a hide that broke) → **ASK** the user which it is, never guess; **hidden** → re-run without `--dry-run` to migrate any older **machine-global** hide to the **project-local** `.git/info/exclude` (one managed block; folds in the legacy `.claude/skills/` line), idempotently (a clean re-run is zero-diff). Handle its surfaced paths exactly as bootstrap step 9 (`${CLAUDE_SKILL_DIR}/references/modes/bootstrap.md`) (already-committed → show `git rm --cached`, ask before `--include`; generic-name present file → ask; **leftover machine-wide ignore block
|
|
34
|
+
**`footprint` — hidden-mode footprint reconcile (D9 / AD-014).** A deployment does not record whether it chose `hidden`, so first **infer visibility** — the dry-run writes **zero bytes** and reports one of — **visible** (the entry point is tracked) → nothing to do; **ambiguous** (untracked but not ignored — could be a fresh uncommitted repo, or a hide that broke) → **ASK** the user which it is, never guess; **hidden** → re-run without `--dry-run` to migrate any older **machine-global** hide to the **project-local** `.git/info/exclude` (one managed block; folds in the legacy `.claude/skills/` line), idempotently (a clean re-run is zero-diff). Handle its surfaced paths exactly as bootstrap step 9 (`${CLAUDE_SKILL_DIR}/references/modes/bootstrap.md`) (already-committed → show `git rm --cached`, ask before `--include`; generic-name present file → ask; **a leftover machine-wide ignore block is kept + reported — the advisor's `footprint-global` item removes it**). This runs on **every** hidden upgrade, like the methodology slot — no lineage-head bump, no migration file.
|
|
35
35
|
|
|
36
|
-
**`configs` — the project-configuration ensures, ONE run.** The ONE command performs **all eight** ensures — orchestration config · gate declaration · autonomy declaration · vehicles declaration · executor
|
|
36
|
+
**`configs` — the project-configuration ensures, ONE run.** The ONE command performs **all eight** ensures — orchestration config · gate declaration · autonomy declaration · vehicles declaration · executor check · enforcement scripts · spec layer · navigator index — in a fixed order, and prints **one outcome line per ensure**: paste those lines into the step 4 / step 8 success report. Every SEED is **create-only** (an existing file is preserved byte-for-byte); a refresh-class op rewrites only what it owns — the outcome tokens by ensure, below, name each arm. One ensure failing **never** skips the others: each reports its own outcome and the run exits non-zero when any of them `failed`. The outcome tokens, by ensure: orchestration → `seeded` / `note-refreshed` / `already-current` / `customized-preserved` / `malformed-preserved`; gates, autonomy and vehicles → `seeded` / `already-present`; executor → `not-placed` / `already-current` / `pending`; scripts → `seeded` / `already-present` / `old-adr-layout-migration-instructed` / `skipped-no-node-evidence`; specs → `seeded` / `refreshed` / `already-present` / `customized-preserved` / `skipped-no-node-evidence`; index → `regenerated` / `already-current` / `customized-preserved`; and any ensure may report `failed`, whose line OPENS with the cause (relay it with that cause — never soften it into a skip; an op that copies file by file also states when it stopped partway). The cause vocabulary is CLOSED — one of `race-unresolved`, `template-unreadable`, `bundle-unreadable`, `adr-layout-unverifiable`, `node-evidence-unverifiable`, `wrong-node-kind`, `vehicle-settings-unreadable`, `write-refused`, `unexpected-error`, `generator-unlaunchable`, `generator-failed`, `index-probe-failed`, `index-stale-after-write` — and every cause that can only arise AFTER the generator ran (`generator-failed`, and `index-probe-failed` / `index-stale-after-write` when they follow a reported regeneration) DISCLOSES in its own line that a write may already have landed. **A non-zero exit STOPs this upgrade — once row 3's replace ask is answered when a specs replace line printed, then as that ask says** — before the equal-head exit, the migrations and the re-stamp. Add `--dry-run` to preview without writing a byte. Like the pointer slots + the footprint reconcile, all eight reach an equal-head deployment **without a lineage-head bump or a migration file** (all inherently outside the docs cap-validator).
|
|
37
37
|
|
|
38
38
|
**What the orchestration-config ensure does.** `docs/ai/orchestration.json` must exist **and its onboarding note must be current**: created from the canonical seed if missing; if it already exists, **every activity/slot the user set is preserved** and ONLY the `_README` note is refreshed, and only when the existing one still matches a known prior canonical — the tested `refreshIfCanonical` / `refreshReadme` in `tools/orchestration-config.mjs` is the source of truth for that decision (it normalizes CRLF/whitespace before comparing; a *customized* `_README` is preserved verbatim → `customized-preserved`; a *malformed* existing config is **preserved untouched + LOUD** → `malformed-preserved`, never clobbered and never silently skipped). The current note points at `/agent-workflow-kit set-recipe`. **Kit-owned:** in the **delegated** path memory only seeds/preserves the file (memory upgrade step 2) and this ensure applies the `_README` refresh; in the **fallback** path it does both. (Memory stays standalone.)
|
|
39
39
|
|
|
@@ -41,13 +41,13 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
41
41
|
|
|
42
42
|
**What the autonomy-declaration ensure does.** `docs/ai/autonomy.json` must exist: created from `${CLAUDE_SKILL_DIR}/references/templates/autonomy.json` if missing (the kit's own template twin — a stale memory never silently loses the seed); **an existing file is preserved byte-for-byte** (a declared policy is authored content). The seed is SPARSE (the onboarding note only) and **defaults-equivalent** — deploying it never changes behavior (the computed defaults stay the policy until the user declares levels with `/agent-workflow-kit set-autonomy` or by hand).
|
|
43
43
|
|
|
44
|
-
**What the executor ensure does.**
|
|
44
|
+
**What the executor ensure does.** It never writes: a placed `.claude/agents/executor.md` whose bytes differ from the body derived from `docs/ai/vehicles.json` is `pending` — the advisor's `executor-vehicle.differing` item re-derives it on the user's yes — and an absent one is `not-placed` (the agents mode places it on consent).
|
|
45
45
|
|
|
46
46
|
**What the enforcement-script ensure does.** A deployment older than the ADR-cascade feature has no `scripts/archive-decisions.mjs`, and an equal-head exit would otherwise never deliver it. The pairs must exist in the project's `scripts/`: `archive-decisions.mjs` + `archive-decisions.test.mjs` and `markdown-blocks.mjs` + `markdown-blocks.test.mjs`, copied from `${CLAUDE_SKILL_DIR}/references/scripts/` if missing; **an existing file is preserved, never overwritten** (drift repair belongs to a lineage migration). Nothing else is seeded — the other tokenizer-era tests red beside OLD archivers. **OLD ADR-store layout — DETECTED FIRST, never auto-migrated (AD-051, Decision 13):** on a `docs/ai/history/decisions-archive*.md` monolith (`old`), or no monolith but a deployed rotator predating the store (`old-unrotated`), the project is on the RETIRED 3-tier cascade, so the ensure writes NOTHING and reports `old-adr-layout-migration-instructed` (the new `archive-decisions.mjs` beside un-migrated monoliths would red their ADR gate). Relay it as the LOUD instruct it is: the fix is the opt-in **`/agent-workflow-kit migrate-adr-store`** (consent-gated; previews first, never commits), and the seed lands on the next upgrade. A layout the ensure cannot READ is `failed`, not a seed — it never writes on an unverifiable tree. The seed applies ONLY to a clean layout (neither signal). **Whether Node runs here is PROVEN, never proxied:** the ensure seeds on a regular `package.json` at the root OR on any kit-seeded `scripts/*.mjs` already deployed (a bootstrap places them into projects that never carry a `package.json`); only when every probe answers absent does it report `skipped-no-node-evidence`, a line naming every probe it checked, while the config ensures still run; a probe that cannot be read is `failed` with the cause `node-evidence-unverifiable`, nothing written. **A `skipped-*` outcome whose stated reason this tool could itself disprove may not exist** — the retired `skipped-no-node` was exactly that, printed beside twenty deployed Node scripts. The deployed pre-commit hook gains the `archive-decisions.mjs --check` line only when the hook itself is next refreshed (the `enforcement` check line and apply, by the consent route, copy nothing and re-run the installer, which refuses a non-marker hook as always); an OLD hook without the line stays consistent-safe — the decisions gate is simply not enforced yet, never a broken hook.
|
|
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
|
|
|
@@ -111,7 +111,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
111
111
|
**The LATE navigator finalizer — the AUTHORITATIVE run, after the last `docs/ai` mutation.** `lens` above rewrites `docs/ai/agent_rules.md`, so the step-3 index ensure is already behind by the time the run-list ends. Re-run that ONE op here, before the step-4 report: `node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --only index --cwd <project>`. Relay **this** line in the report (it supersedes the early one; an untouched tree changes nothing, a failure STOPs the upgrade like any other ensure). On the migrated path the same rung runs again at the END of step 7 — after the migrations, before the step-8 re-stamp.
|
|
112
112
|
4. **Equal-head exit — a real successful-exit report, not a bare stop.** If the stamp **equals** the head, the lineage is up to date — but step 3 (the stamp-independent reconciles) ran first and may have changed things, so this is a proper exit report, not a no-op:
|
|
113
113
|
- **A skip line that contradicts the observed tree is a FINDING, never pasted as neutral.** Every ensure skip names the fact it proved (the probes that answered absent, the layout it read); if the tree you can see disproves that reason — a "no Node" skip beside deployed Node scripts, a "not deployed" skip beside a stamped `docs/ai` — do not relay the line as an outcome: raise it in the report as a defect of the tool, with the contradicting fact named, and stop short of any step that would build on the skip.
|
|
114
|
-
- **Report step 3's outcome in plain language** — for **each** `pointers` slot (workflow-methodology, orchestration-recipes and autonomy-policy) whether it was *added*, was *already present* (nothing changed), or was *skipped* (the soft-skip from step 3, with its reason — over the line limit / engine too old / the autonomy pointer's anchor absent); the **eight project-configuration ensure** (`configs`) lines exactly as the one ensure run composed them (the ensures and their outcome tokens are enumerated in step 3), each rendered in plain language: what was created, what was left exactly as the user wrote it, and — for a `failed` line — what stopped it; for the navigator, relay the **late** `--only index` line, not the early one; the **`gates-migration`** result — *nothing to migrate*, the shown plan *applied* on your explicit yes, or the plan *left unapplied* (consent not given), with any INERT checker or CUSTOMIZED entries named; the **placed-bridge refresh** (`bridges`) outcome — paste the tool's per-bridge lines verbatim (they are already plain: *refreshed* / *already current* / *skipped — not placed* / `skipped-readonly` with its re-scan verdict / *could not refresh* + recovery); the **agent-rules lens** (`lens`) outcome (*refreshed* / *already current* / *custom edit preserved + note* / *file absent* / *engine too old* / *over the line cap*) and the **Communication-section** outcome (its own set: refreshed / already current / custom preserved + note / section absent — noted / over the cap — refused) and the **story-sessions section** outcome (its own set: refreshed / already current / custom edit preserved + note / section absent — noted / over the file's line cap — refused); the **full-flow offer** (`tier`) lines; the **bridge-settings reconcile** (`bridge-settings`) outcome (paste the tool's line(s) verbatim); and, for a hidden deployment, whether the hidden-mode footprint (`footprint`) was *moved to project-local*, was *already project-local* (nothing changed), or needed a question (ambiguous visibility
|
|
114
|
+
- **Report step 3's outcome in plain language** — for **each** `pointers` slot (workflow-methodology, orchestration-recipes and autonomy-policy) whether it was *added*, was *already present* (nothing changed), or was *skipped* (the soft-skip from step 3, with its reason — over the line limit / engine too old / the autonomy pointer's anchor absent); the **eight project-configuration ensure** (`configs`) lines exactly as the one ensure run composed them (the ensures and their outcome tokens are enumerated in step 3), each rendered in plain language: what was created, what was left exactly as the user wrote it, and — for a `failed` line — what stopped it; for the navigator, relay the **late** `--only index` line, not the early one; the **`gates-migration`** result — *nothing to migrate*, the shown plan *applied* on your explicit yes, or the plan *left unapplied* (consent not given), with any INERT checker or CUSTOMIZED entries named; the **placed-bridge refresh** (`bridges`) outcome — paste the tool's per-bridge lines verbatim (they are already plain: *refreshed* / *already current* / *skipped — not placed* / `skipped-readonly` with its re-scan verdict / *could not refresh* + recovery); the **agent-rules lens** (`lens`) outcome (*refreshed* / *already current* / *custom edit preserved + note* / *file absent* / *engine too old* / *over the line cap*) and the **Communication-section** outcome (its own set: refreshed / already current / custom preserved + note / section absent — noted / over the cap — refused) and the **story-sessions section** outcome (its own set: refreshed / already current / custom edit preserved + note / section absent — noted / over the file's line cap — refused); the **full-flow offer** (`tier`) lines; the **bridge-settings reconcile** (`bridge-settings`) outcome (paste the tool's line(s) verbatim); and, for a hidden deployment, whether the hidden-mode footprint (`footprint`) was *moved to project-local*, was *already project-local* (nothing changed), or needed a question (ambiguous visibility). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
|
|
115
115
|
- **Never surface the structure number on this exit.** Whatever step 3 did, do **not** recite the `docs/ai` structure version, the internal versioning vocabulary, or the two-axes note here — the number is inert on an equal-head exit; it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md` (shown at the never-downgrade STOP, the explicit status view, or on an explicit ask). Frame the success itself per the final bullet: if step 3 changed anything, say **what changed** in plain human terms; only a pure zero-diff no-op is *settings already current — no update needed*.
|
|
116
116
|
- **Render the mandatory Recommendations section — on this exit too, BEFORE the footer.** Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and PRESENT its output — from the `## Recommendations (agent-workflow)` header — in the user's conversational language: every fact, count and item from the tool, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; show the raw tool block on request. The section is present-even-when-empty (with everything optimal the body is exactly `no recommendations — flow optimal.`) and VERDICT-FIRST — the composed verdict line renders from the frozen templates `{K} item(s) need attention` / `nothing is broken` / `{N} optional recommendation(s), apply any you want` / `optimality NOT attested — {M} probe check(s) skipped`. Then OFFER the consent route: the user picks items in plain language; surface each picked item's posture note and get the explicit confirm; run its lane's rendered lines — chat: the one-liner; harness: the check line, then on the yes to its result the apply line; user: the user's own step — per the full consent route in `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`. Pinned order on this exit: Recommendations block → optional applies → report footer → the commit ask (the advisor/apply lane never lands after the commit ask).
|
|
117
117
|
- **Recommendations lanes — both exits.** The three lanes (`chat`, `harness`, `user`) and the consent route live in `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`; `init` stays a route the user may take.
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
**Hand-off contract (explicit; tested independent of agent interpretation).**
|
|
4
4
|
- **Delegated** (memory valid): the kit passes the **target project dir** + the **three setup
|
|
5
|
-
answers** (visibility / language / attribution) to `agent-workflow-memory`, which writes
|
|
5
|
+
answers** (visibility / language / attribution — memory is told to skip its step-4 settings write; the `attribution` item of step 11 sets it) to `agent-workflow-memory`, which writes
|
|
6
6
|
`docs/ai/` + `AGENTS.md` (carrying the **three empty pointer pairs** — `workflow:methodology`,
|
|
7
7
|
`workflow:orchestration` and `workflow:autonomy`) + **`.memory-version`**. The kit then **reconciles
|
|
8
8
|
the bounded pointers** (below) and writes the kit-fallback **`.workflow-version`**. → **both stamps** present. In **hidden** mode the kit is the **single
|
|
9
9
|
hide authority**: after the hand-off it runs `tools/hide-footprint.mjs` (bootstrap step 9 — `${CLAUDE_SKILL_DIR}/references/modes/bootstrap.md`), which **absorbs
|
|
10
10
|
memory's project-local footprint lines** into the one canonical `.git/info/exclude` block and adds
|
|
11
|
-
the external footprint — so
|
|
12
|
-
residual global block is
|
|
11
|
+
the external footprint — so the hide makes **no machine-global write** (a stale memory's
|
|
12
|
+
residual global block is the advisor's footprint-global item).
|
|
13
13
|
- **Fallback** (memory absent/invalid): the kit runs the bootstrap procedure (`${CLAUDE_SKILL_DIR}/references/modes/bootstrap.md`) from its own
|
|
14
14
|
bundled assets — whose entry-point template now ships **three empty pointer pairs** (`workflow:methodology`
|
|
15
15
|
+ `workflow:orchestration` + `workflow:autonomy`) the kit reconciles + fills — and writes **`.workflow-version`** only.
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
The non-obvious traps — scan these before bootstrapping or upgrading. Each is also enforced inline in the bootstrap/upgrade procedures (`${CLAUDE_SKILL_DIR}/references/modes/bootstrap.md`, `${CLAUDE_SKILL_DIR}/references/modes/upgrade.md`); this is the consolidated high-signal list.
|
|
4
4
|
|
|
5
5
|
- **Source vs target directory.** Templates and scripts are read from the skill's own dir (`${CLAUDE_SKILL_DIR}/` in Claude Code, the `SKILL.md` folder elsewhere). The **working directory is the target project** — never write kernel files back into the skill.
|
|
6
|
-
- **The `Co-Authored-By` trailer is added by the harness, not by prose.** When attribution is `off`, a doc directive alone won't stop it — for Claude Code
|
|
6
|
+
- **The `Co-Authored-By` trailer is added by the harness, not by prose.** When attribution is `off`, a doc directive alone won't stop it — for Claude Code the advisor's `attribution` item sets the harness setting in the project's `.claude/settings.json`, on the user's yes after its check. Other tools: disable their equivalent co-author/footer setting.
|
|
7
7
|
- **Hidden mode must never touch `package.json`.** Editing it is a *tracked* change and leaks the whole system. Hidden mode wires nothing into `package.json`; the pre-commit hook (untracked in `.git/hooks/`) calls `node scripts/<x>.mjs` directly. After hiding, **verify `git status` shows the artifacts as ignored**.
|
|
8
|
-
- **Hidden mode is project-local, and the hide tool owns the known footprint.** `tools/hide-footprint.mjs` writes **one managed block** in the **project-local `.git/info/exclude`** — never the machine-global `core.excludesFile` (which would silently affect every repo on the host; **AD-014** amends **AD-006
|
|
8
|
+
- **Hidden mode is project-local, and the hide tool owns the known footprint.** `tools/hide-footprint.mjs` writes **one managed block** in the **project-local `.git/info/exclude`** — never the machine-global `core.excludesFile` (which would silently affect every repo on the host; **AD-014** amends **AD-006**; only its `--remove-global` mode, the `footprint-global` item, edits that file). It hides the kit's own artifacts **and** the known external AI/agent footprint (the `KNOWN_FOOTPRINT` table in [contracts](${CLAUDE_SKILL_DIR}/references/contracts.md#visibility-contract)). A **tracked** file is **never silently un-tracked** — the tool prints the `git rm --cached` it will not run. Never leak its internal marker / asks terms to the user; translate every outcome to plain language.
|
|
9
9
|
- **`CLAUDE.md` is a symlink, not a copy.** `ln -s AGENTS.md CLAUDE.md` — single source, no duplication. A copy drifts; a symlink can't.
|
|
10
10
|
- **Never overwrite an existing entry point or hook.** If `AGENTS.md` / `CLAUDE.md` already exist, **stop and ask** the user to merge vs replace — don't clobber. A non-marker hook stays; moving it aside is the user's step.
|
|
11
11
|
- **Unrecognized invocations are read-only.** Only a **known** subcommand reaches its mode; the **bare** invocation bootstraps (and an existing `docs/ai/` makes it ask upgrade-vs-bootstrap first, never overwrite); **any other / ambiguous** token routes to `help` (read-only). A garbage invocation never writes. The mapping is pinned by `tools/commands.mjs` `routeInvocation` (unit-tested) — don't hand-route around it.
|
|
@@ -1,5 +1,5 @@
|
|
|
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 and the upgrade
|
|
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.",
|
|
3
3
|
"executor": {
|
|
4
4
|
"model": "opus",
|
|
5
5
|
"effort": "high",
|
|
@@ -25,6 +25,8 @@ import { SKILL_FILES, claudeDirOf, skillTargets } from './jev-facts.mjs';
|
|
|
25
25
|
import { findOnPath } from './detect-backends.mjs';
|
|
26
26
|
import { consoleArgv } from './write-lanes.mjs';
|
|
27
27
|
import { BUNDLED_SCRIPT_NAMES, INSTALLER_REL, SCRIPTS_REL, formatPlan, planGitHooks } from './git-hooks.mjs';
|
|
28
|
+
import { ATTRIBUTION_HIDDEN, ATTRIBUTION_KEY, writeAttribution } from './attribution.mjs';
|
|
29
|
+
import { globalRemovalOf, planGlobalRemoval } from './hide-footprint.mjs';
|
|
28
30
|
import { isDirectRun } from './direct-run.mjs';
|
|
29
31
|
import { dirCovers as covers } from './declared-paths.mjs';
|
|
30
32
|
|
|
@@ -35,11 +37,13 @@ const RESEED_VARIANTS = ['gate-hook.marker-stale', 'read-lane.stale'];
|
|
|
35
37
|
const HOOK_VARIANTS = ['gate-hook', 'read-lane.missing', ...RESEED_VARIANTS];
|
|
36
38
|
const STOP = 'state-block';
|
|
37
39
|
const MCP_VARIANTS = ['mcp-channel', 'mcp-channel.differing'];
|
|
38
|
-
const AGENT_VARIANTS = ['agents', 'executor-vehicle'];
|
|
40
|
+
const AGENT_VARIANTS = ['agents', 'executor-vehicle', 'executor-vehicle.differing'];
|
|
39
41
|
const GIT_VARIANTS = ['enforcement', 'commit-guard'];
|
|
40
42
|
const WORKTREES = 'worktrees-dir';
|
|
43
|
+
const ATTRIBUTION = 'attribution';
|
|
44
|
+
const FOOTPRINT = 'footprint-global';
|
|
41
45
|
const VARIANTS = [...VELOCITY_VARIANTS, 'autonomy-render', ...HOOK_VARIANTS, STOP, ...MCP_VARIANTS, ...AGENT_VARIANTS,
|
|
42
|
-
...SKILL_VARIANTS, ...GIT_VARIANTS, WORKTREES];
|
|
46
|
+
...SKILL_VARIANTS, ...GIT_VARIANTS, WORKTREES, ATTRIBUTION, FOOTPRINT];
|
|
43
47
|
const HOOK_KEYS = ['hooks.PreToolUse', 'hooks.Stop'];
|
|
44
48
|
const CREDENTIALS = 'sandbox.credentials.envVars';
|
|
45
49
|
const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -69,10 +73,12 @@ export const KEY_TABLE = Object.freeze({
|
|
|
69
73
|
[ENABLED_KEY]: 4,
|
|
70
74
|
[SERVERS_KEY]: 4,
|
|
71
75
|
[CREDENTIALS]: 5,
|
|
76
|
+
[ATTRIBUTION_KEY]: 5,
|
|
72
77
|
});
|
|
73
|
-
const SCALARS = ['permissions.defaultMode', 'sandbox.enabled', 'sandbox.autoAllowBashIfSandboxed'];
|
|
78
|
+
const SCALARS = ['permissions.defaultMode', 'sandbox.enabled', 'sandbox.autoAllowBashIfSandboxed', ATTRIBUTION_KEY];
|
|
74
79
|
const valid = (key, value, element = false) => {
|
|
75
80
|
if (key.startsWith(`${SERVERS_KEY}.`) || key === SERVERS_KEY) return object(value);
|
|
81
|
+
if (key === ATTRIBUTION_KEY) return object(value) || typeof value === 'boolean';
|
|
76
82
|
if (key === 'permissions.defaultMode') return typeof value === 'string';
|
|
77
83
|
if (SCALARS.includes(key)) return typeof value === 'boolean';
|
|
78
84
|
if (!element) return Array.isArray(value) && value.every((entry) => valid(key, entry, true));
|
|
@@ -150,6 +156,7 @@ export const judgeChange = ({ variant, files, scope }) => {
|
|
|
150
156
|
}
|
|
151
157
|
return { entries, refused };
|
|
152
158
|
};
|
|
159
|
+
const footprintPlan = (facts, deps) => (deps.planGlobalRemoval ?? planGlobalRemoval)({ env: facts.env, home: facts.home }, facts.root, { home: facts.home });
|
|
153
160
|
const writerFacts = (facts, deps) => ({
|
|
154
161
|
...deps, ...facts,
|
|
155
162
|
detect: deps.detect ?? facts.detect,
|
|
@@ -183,7 +190,7 @@ export const scopeOf = (variant, facts, deps = {}) => {
|
|
|
183
190
|
set('permissions.ask', RENDER_OWNED_REDLINE_RULES);
|
|
184
191
|
set('permissions.deny', RENDER_OWNED_REDLINE_RULES);
|
|
185
192
|
allowed[CREDENTIALS] = (value) => value.mode === 'deny';
|
|
186
|
-
}
|
|
193
|
+
} else if (variant === ATTRIBUTION) set(ATTRIBUTION_KEY, [ATTRIBUTION_HIDDEN]);
|
|
187
194
|
const grantDir = variant === WORKTREES ? worktreesGrantDir(facts.root, facts) : null;
|
|
188
195
|
if (grantDir !== null && !worktreesGrantHoldsHome(grantDir, facts.home, facts)) {
|
|
189
196
|
set('sandbox.filesystem.allowWrite', [homeRelative(grantDir, facts.home)]);
|
|
@@ -199,7 +206,9 @@ export const scopeOf = (variant, facts, deps = {}) => {
|
|
|
199
206
|
claudeDir: facts.claudeDir ?? claudeDirOf(facts.env, facts.home, facts.platform).dir }) : [];
|
|
200
207
|
const filePaths = HOOK_VARIANTS.includes(variant) ? [join(facts.root, HOOK_FILE_REL)]
|
|
201
208
|
: variant === STOP ? [join(facts.root, STATE_BLOCK_HOOK_FILE_REL)] : [];
|
|
202
|
-
const directories = SKILL_VARIANTS.includes(variant) ? [] : [join(facts.root, CLAUDE_DIR)];
|
|
209
|
+
const directories = SKILL_VARIANTS.includes(variant) || variant === FOOTPRINT ? [] : [join(facts.root, CLAUDE_DIR)];
|
|
210
|
+
const globalFile = variant === FOOTPRINT ? footprintPlan(facts, deps)?.source : undefined;
|
|
211
|
+
if (globalFile) filePaths.push(globalFile);
|
|
203
212
|
if (HOOK_VARIANTS.includes(variant) || variant === STOP) directories.push(join(facts.root, HOOKS_DIR));
|
|
204
213
|
if (AGENT_VARIANTS.includes(variant)) directories.push(join(facts.root, AGENTS_DIR));
|
|
205
214
|
const existingAncestor = grantDir === null ? null : nearestExistingDir(grantDir, facts);
|
|
@@ -222,7 +231,7 @@ export const scopeOf = (variant, facts, deps = {}) => {
|
|
|
222
231
|
: path === (variant === WORKTREES ? SETTINGS_LOCAL_FILE : SETTINGS_FILE);
|
|
223
232
|
const gains = variant === 'bridge-tier' || variant === WORKTREES || HOOK_KEYS.includes(key) || key === ENABLED_KEY || key === CREDENTIALS
|
|
224
233
|
|| (VELOCITY_VARIANTS.includes(variant) && key === 'permissions.ask');
|
|
225
|
-
return correctFile && (!gains || change === 'added') && Boolean(allowed[key]?.(value));
|
|
234
|
+
return correctFile && (!gains || change === 'added') && !(key === ATTRIBUTION_KEY && change === 'removed') && Boolean(allowed[key]?.(value));
|
|
226
235
|
},
|
|
227
236
|
admitsPath: (path, directory = false) => directory ? directories.includes(path)
|
|
228
237
|
: filePaths.includes(path) || (AGENT_VARIANTS.includes(variant) && covers(join(facts.root, AGENTS_DIR), path)),
|
|
@@ -230,10 +239,11 @@ export const scopeOf = (variant, facts, deps = {}) => {
|
|
|
230
239
|
};
|
|
231
240
|
|
|
232
241
|
const jsonAt = (path) => existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : null;
|
|
242
|
+
const regularBytes = (path) => lstatSync(path, { throwIfNoEntry: false })?.isFile() ? readFileSync(path) : undefined;
|
|
233
243
|
const halted = (lines) => Object.assign(new Error(lines.join('\n')), { code: 'CHECK_HALTED', lines });
|
|
234
244
|
const maskedFiles = (variant, facts) => {
|
|
235
245
|
const paths = VELOCITY_VARIANTS.includes(variant) || variant === 'autonomy-render' || HOOK_VARIANTS.includes(variant)
|
|
236
|
-
|| variant === STOP || variant === WORKTREES ? [SETTINGS_FILE, SETTINGS_LOCAL_FILE] : MCP_VARIANTS.includes(variant) ? [MCP_JSON_REL, SETTINGS_FILE] : [];
|
|
246
|
+
|| variant === STOP || variant === WORKTREES || variant === ATTRIBUTION ? [SETTINGS_FILE, SETTINGS_LOCAL_FILE] : MCP_VARIANTS.includes(variant) ? [MCP_JSON_REL, SETTINGS_FILE] : [];
|
|
237
247
|
return paths.flatMap((path) => lstatSync(join(facts.root, path), { throwIfNoEntry: false })?.isCharacterDevice()
|
|
238
248
|
? [`masked: ${path}`] : []);
|
|
239
249
|
};
|
|
@@ -275,13 +285,12 @@ export const plannedChange = (variant, facts, deps = {}) => {
|
|
|
275
285
|
if (plan.wirePlanned) addFile(SETTINGS_FILE, plan.projectSettings.data, mergeHookEntry(plan.projectSettings.data));
|
|
276
286
|
const path = join(facts.root, HOOK_FILE_REL);
|
|
277
287
|
if (plan.placePlanned) writes.push({ path, bytes: plan.bundleContent });
|
|
278
|
-
if (plan.reseedPlanned) writes.push({ path, bytes: plan.bundleContent
|
|
288
|
+
if (plan.reseedPlanned) writes.push({ path, bytes: plan.bundleContent });
|
|
279
289
|
} else if (variant === STOP) {
|
|
280
290
|
const plan = writeStateBlockHook({ cwd: facts.root, dryRun: true }, writerDeps);
|
|
281
291
|
if (plan.mergePlanned) addFile(SETTINGS_FILE, plan.projectSettings.data, mergeStopEntry(plan.projectSettings.data));
|
|
282
292
|
const path = join(facts.root, STATE_BLOCK_HOOK_FILE_REL);
|
|
283
|
-
if (plan.placement
|
|
284
|
-
if (plan.placement === 'replace') writes.push({ path, bytes: plan.bundleContent, replaces: readFileSync(path) });
|
|
293
|
+
if (['create', 'replace'].includes(plan.placement)) writes.push({ path, bytes: plan.bundleContent });
|
|
285
294
|
} else if (MCP_VARIANTS.includes(variant)) {
|
|
286
295
|
const result = writeMcp({ cwd: facts.root, dryRun: true, replace: variant === 'mcp-channel.differing' }, writerDeps);
|
|
287
296
|
const { plan, registration } = result;
|
|
@@ -311,6 +320,17 @@ export const plannedChange = (variant, facts, deps = {}) => {
|
|
|
311
320
|
const before = plan.localSettings.data ?? null;
|
|
312
321
|
addFile(SETTINGS_LOCAL_FILE, before, mergeAllowWrite(before ?? {}, plan.worktreesDirToAdd));
|
|
313
322
|
created.push(...plan.worktreesDirsToCreate);
|
|
323
|
+
} else if (variant === ATTRIBUTION) {
|
|
324
|
+
const plan = writeAttribution({ cwd: facts.root, dryRun: true }, writerDeps);
|
|
325
|
+
if (plan.planned) addFile(SETTINGS_FILE, plan.before, plan.after);
|
|
326
|
+
} else if (variant === FOOTPRINT) {
|
|
327
|
+
const plan = footprintPlan(facts, deps);
|
|
328
|
+
if (plan !== null) {
|
|
329
|
+
const bytes = readFileSync(plan.source, 'latin1');
|
|
330
|
+
const core = globalRemovalOf(bytes);
|
|
331
|
+
const matches = plan.before === bytes && core !== null && equal(plan.removedLines, core.removedLines) && plan.after === core.after;
|
|
332
|
+
writes.push({ path: plan.source, bytes: Buffer.from(plan.after, 'latin1'), replaces: Buffer.from(plan.before, 'latin1'), removes: plan.removedLines, matches });
|
|
333
|
+
}
|
|
314
334
|
} else throw new Error(`unsupported variant: ${variant}`);
|
|
315
335
|
} catch (error) {
|
|
316
336
|
throw error.code === 'CHECK_HALTED' ? error : halted([error.message]);
|
|
@@ -345,8 +365,13 @@ export const checkApply = (variant, facts, deps = {}) => {
|
|
|
345
365
|
reason: `beyond the declared scope of ${variant}` });
|
|
346
366
|
};
|
|
347
367
|
for (const write of plan.writes) {
|
|
348
|
-
const
|
|
368
|
+
const old = write.replaces ?? (write.directory ? undefined : regularBytes(write.path));
|
|
369
|
+
const replaces = old === undefined ? '' : ` — replaces sha256:${sha(old)}`;
|
|
349
370
|
pathLine(write.path, false, `writes: ${display(write.path)} sha256:${sha(write.bytes)}${replaces}`);
|
|
371
|
+
for (const line of write.removes ?? []) lines.push(`removes: ${line}`);
|
|
372
|
+
if (write.matches === false) {
|
|
373
|
+
refused.push({ path: display(write.path), key: display(write.path), reason: 'the plan is not the one the file\'s own bytes give' });
|
|
374
|
+
}
|
|
350
375
|
}
|
|
351
376
|
for (const path of plan.directories) pathLine(path, true, `writes: ${display(path)}/ created`);
|
|
352
377
|
lines.push(...plan.temporaries);
|
|
@@ -405,7 +430,7 @@ export const main = (argv, io = {}) => {
|
|
|
405
430
|
platform: resolved.platform, claudeDir: args['claude-dir'] };
|
|
406
431
|
let result;
|
|
407
432
|
try {
|
|
408
|
-
result = checkApply(args.variant, facts, { detect: resolved.detect, planGitHooks: resolved.planGitHooks });
|
|
433
|
+
result = checkApply(args.variant, facts, { detect: resolved.detect, planGitHooks: resolved.planGitHooks, planGlobalRemoval: resolved.planGlobalRemoval });
|
|
409
434
|
} catch (error) {
|
|
410
435
|
for (const line of error.lines ?? [error.message]) resolved.log(line);
|
|
411
436
|
return 3;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// attribution.mjs — the writer of the advisor's `attribution` item: when the project's AGENTS.md block
|
|
3
|
+
// reads off, it sets Claude Code's `attribution` setting in the project .claude/settings.json so the
|
|
4
|
+
// harness adds no Co-Authored-By trailer, PR text or session link. Preview by default; --apply writes.
|
|
5
|
+
import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
import { CLAUDE_DIR, EXPECTED_WORKFLOW_VERSION, SETTINGS_FILE, preflightVelocityProfile } from './velocity-profile.mjs';
|
|
8
|
+
import { formatJson } from './gate-hook.mjs';
|
|
9
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
10
|
+
|
|
11
|
+
export const ATTRIBUTION_KEY = 'attribution';
|
|
12
|
+
export const LEGACY_ATTRIBUTION_KEY = 'includeCoAuthoredBy';
|
|
13
|
+
// The one form the writer writes: `false` would make a CLI older than v2.1.281 skip the whole file.
|
|
14
|
+
export const ATTRIBUTION_HIDDEN = Object.freeze({ commit: '', pr: '', sessionUrl: false });
|
|
15
|
+
export const ATTRIBUTION_MALFORMED = 'ATTRIBUTION_MALFORMED';
|
|
16
|
+
export const ATTRIBUTION_STAMP = 'ATTRIBUTION_STAMP';
|
|
17
|
+
export const ATTRIBUTION_SHARED_NOTICE = `notice: ${SETTINGS_FILE} is the project-scope settings file — in a visible deployment it is committed and shared with everyone on this project, as the AGENTS.md block is`;
|
|
18
|
+
|
|
19
|
+
const EXIT_OK = 0;
|
|
20
|
+
const EXIT_REFUSED = 1;
|
|
21
|
+
const EXIT_USAGE = 2;
|
|
22
|
+
const USAGE = 'usage: attribution [--cwd <root>] [--apply]';
|
|
23
|
+
const CHOICE_LINE = /^> \*\*Agent attribution: ([^*\s]+)\*\*/mu;
|
|
24
|
+
const CHOICES = ['off', 'on'];
|
|
25
|
+
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
26
|
+
const hasOwn = (object, key) => isObject(object) && Object.hasOwn(object, key);
|
|
27
|
+
const refusal = (code, message) => Object.assign(new Error(`${code}: ${message}`), { code, exitCode: EXIT_REFUSED });
|
|
28
|
+
const usageError = (message) => Object.assign(new Error(message), { exitCode: EXIT_USAGE });
|
|
29
|
+
|
|
30
|
+
// off, on, or null for an absent file, an absent line or another value; any other read failure throws.
|
|
31
|
+
export const readAttributionChoice = (root, deps = {}) => {
|
|
32
|
+
let text;
|
|
33
|
+
try {
|
|
34
|
+
text = String((deps.readFile ?? readFileSync)(join(root, 'AGENTS.md'), 'utf8'));
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error?.code === 'ENOENT') return null;
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
const value = text.match(CHOICE_LINE)?.[1];
|
|
40
|
+
return CHOICES.includes(value) ? value : null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Per key, the local file's value while it carries the key, else the project's; an object is taken whole.
|
|
44
|
+
export const effectiveAttribution = (project, local) => Object.fromEntries([ATTRIBUTION_KEY, LEGACY_ATTRIBUTION_KEY]
|
|
45
|
+
.map((key) => [key, hasOwn(local, key) ? local[key] : project?.[key]])
|
|
46
|
+
.filter(([, value]) => value !== undefined));
|
|
47
|
+
|
|
48
|
+
export const attributionHidesText = (effective) => {
|
|
49
|
+
const value = effective?.[ATTRIBUTION_KEY];
|
|
50
|
+
if (value === false) return true;
|
|
51
|
+
if (isObject(value) && value.commit === '' && value.pr === '') return true;
|
|
52
|
+
const setsText = hasOwn(value, 'commit') || hasOwn(value, 'pr');
|
|
53
|
+
return !setsText && effective?.[LEGACY_ATTRIBUTION_KEY] === false;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// The wrong-typed key of one settings file, or null: attribution is an object or a boolean, the legacy key a boolean.
|
|
57
|
+
export const wrongTypedKey = (data) => {
|
|
58
|
+
if (hasOwn(data, ATTRIBUTION_KEY) && !isObject(data[ATTRIBUTION_KEY]) && typeof data[ATTRIBUTION_KEY] !== 'boolean') {
|
|
59
|
+
return `${ATTRIBUTION_KEY} must be an object or a boolean`;
|
|
60
|
+
}
|
|
61
|
+
if (hasOwn(data, LEGACY_ATTRIBUTION_KEY) && typeof data[LEGACY_ATTRIBUTION_KEY] !== 'boolean') return `${LEGACY_ATTRIBUTION_KEY} must be a boolean`;
|
|
62
|
+
return null;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const writeAttribution = ({ cwd, dryRun = true } = {}, deps = {}) => {
|
|
66
|
+
const root = resolve(cwd ?? deps.cwd ?? process.cwd());
|
|
67
|
+
const preflight = preflightVelocityProfile({ cwd: root }, deps);
|
|
68
|
+
const before = preflight.projectSettings.data ?? null;
|
|
69
|
+
const malformed = wrongTypedKey(before);
|
|
70
|
+
if (malformed) throw refusal(ATTRIBUTION_MALFORMED, `${SETTINGS_FILE}: ${malformed} — nothing written`);
|
|
71
|
+
const planned = !attributionHidesText(effectiveAttribution(before, preflight.localSettings.data));
|
|
72
|
+
const after = planned ? { ...before, [ATTRIBUTION_KEY]: { ...ATTRIBUTION_HIDDEN } } : before;
|
|
73
|
+
const result = { root, planned, wrote: false, before, after, claudeDirAbsent: preflight.claudeDirAbsent };
|
|
74
|
+
if (dryRun || !planned) return result;
|
|
75
|
+
if (!preflight.stampOk) {
|
|
76
|
+
throw refusal(ATTRIBUTION_STAMP, `not a deployed agent-workflow project at lineage ${EXPECTED_WORKFLOW_VERSION} (found ${preflight.stamp ?? 'none'}) — run /agent-workflow-kit upgrade first`);
|
|
77
|
+
}
|
|
78
|
+
if (preflight.claudeDirAbsent) (deps.mkdir ?? mkdirSync)(join(root, CLAUDE_DIR), { recursive: true });
|
|
79
|
+
(deps.writeFile ?? writeFileSync)(join(root, SETTINGS_FILE), formatJson(after, preflight.projectSettings.eol ?? '\n'), 'utf8');
|
|
80
|
+
return { ...result, wrote: true };
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const formatResult = (result, dryRun) => {
|
|
84
|
+
if (!result.planned) return ['nothing to change: the settings already hide the trailer and the PR text'];
|
|
85
|
+
const object = JSON.stringify(ATTRIBUTION_HIDDEN);
|
|
86
|
+
return dryRun ? [`would set ${ATTRIBUTION_KEY} (${SETTINGS_FILE}): ${object}`, ATTRIBUTION_SHARED_NOTICE]
|
|
87
|
+
: [`set ${ATTRIBUTION_KEY} (${SETTINGS_FILE}): ${object}`];
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const parseArgs = (argv) => {
|
|
91
|
+
if (argv.length === 1 && ['--help', '-h'].includes(argv[0])) return { help: true };
|
|
92
|
+
const args = { help: false, apply: false, cwd: undefined };
|
|
93
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
94
|
+
const flag = argv[index];
|
|
95
|
+
if (flag === '--apply') args.apply = true;
|
|
96
|
+
else if (flag === '--cwd') {
|
|
97
|
+
const value = argv[index + 1];
|
|
98
|
+
if (value === undefined || value.startsWith('-')) throw usageError('--cwd takes a value');
|
|
99
|
+
args.cwd = value;
|
|
100
|
+
index += 1;
|
|
101
|
+
} else throw usageError(`unknown argument: ${flag}`);
|
|
102
|
+
}
|
|
103
|
+
return args;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
107
|
+
const log = deps.log ?? console.log;
|
|
108
|
+
const errlog = deps.errlog ?? console.error;
|
|
109
|
+
let args;
|
|
110
|
+
try {
|
|
111
|
+
args = parseArgs(argv);
|
|
112
|
+
if (args.help) {
|
|
113
|
+
log(USAGE);
|
|
114
|
+
return EXIT_OK;
|
|
115
|
+
}
|
|
116
|
+
args.root = resolve(deps.cwd ?? process.cwd(), args.cwd ?? '.');
|
|
117
|
+
if (!statSync(args.root, { throwIfNoEntry: false })?.isDirectory()) throw usageError(`--cwd is not a directory: ${args.root}`);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
errlog(error.message);
|
|
120
|
+
errlog(USAGE);
|
|
121
|
+
return EXIT_USAGE;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const result = writeAttribution({ cwd: args.root, dryRun: !args.apply }, deps);
|
|
125
|
+
for (const line of formatResult(result, !args.apply)) log(line);
|
|
126
|
+
return EXIT_OK;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
errlog(error?.message ?? String(error));
|
|
129
|
+
return EXIT_REFUSED;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (isDirectRun(import.meta.url)) process.exitCode = main(process.argv.slice(2));
|