@sabaiway/agent-workflow-kit 14.33.0 → 14.34.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 CHANGED
@@ -4,6 +4,45 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
4
4
  is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
5
5
  every `migrations/<version>-<slug>.md` newer than it, in semver order.
6
6
 
7
+ ## 14.34.0 — bootstrap and the upgrade stop making five protected writes mid-session: attribution, the legacy global ignore block, a hand-edited executor and the commit guard are applied on the chat yes (AD-186)
8
+
9
+ **Five writes that bootstrap, the upgrade or a migration note made by themselves mid-session are now advisor items the
10
+ agent applies on the chat yes, after the danger check, or that stay pending.** Before, under the sandbox each one met a
11
+ refused write with no route: bootstrap and note 1.2.0 set `includeCoAuthoredBy` in `.claude/settings.json`; bootstrap
12
+ and the hide removed the legacy machine-global ignore block with `--remove-global` after an ask; note 3.0.0 migrated
13
+ the commit guard; and the configs step rewrote a hand-edited `.claude/agents/executor.md`.
14
+
15
+ - **`attribution`, a new item and its writer.** When `AGENTS.md` reads `Agent attribution: off` and the effective
16
+ project and local settings do not hide the harness's Co-Authored-By trailer and PR text,
17
+ `node <kit>/tools/attribution.mjs --cwd <root>` previews and `--apply` sets `attribution` to
18
+ `{"commit":"","pr":"","sessionUrl":false}` in the project `.claude/settings.json`, in place, keeping every other key.
19
+ An existing `"includeCoAuthoredBy": false` already counts unless an `attribution` object carries a `commit` or `pr`
20
+ field. A wrong-typed key in the project file refuses (`ATTRIBUTION_MALFORMED`); one in the local file is a stated
21
+ skip, and a local-scope override is your own edit. With
22
+ `agent-workflow-memory` installed, bootstrap tells memory to skip its own settings write; this item sets it.
23
+ - **`footprint-global`, a new item.** A legacy kit block in the machine-global ignore file (`core.excludesFile`) is
24
+ removed only by this item: `hide-footprint.mjs --remove-global` is now its own mode, which removes each recognised
25
+ legacy line, keeps every other byte, prints the removed lines as a backup, and needs at least one kit-specific line
26
+ before it removes anything. The hide, `--reconcile` and `--unhide` never write that file; the hide and `--unhide`
27
+ report the block kept, as `--reconcile` does on a hidden deployment.
28
+ `--keep-global` is removed (it was the default). A refused write stops with `cannot write the global excludes …`.
29
+ - **`executor-vehicle.differing`, a new arm.** The configs step no longer rewrites a placed executor whose body differs
30
+ from the one `docs/ai/vehicles.json` derives: it reports `pending` (the tokens `would-re-derive` and `re-derived` are
31
+ gone), and the optional item re-derives it through the agents writer on your yes.
32
+ - **`commit-guard` verifies the arm.** After the project's installer exits 0, the apply reads the pre-commit hook back;
33
+ an installer older than the arm ignores `--commit-guard`, so the apply exits 1 with one line naming the hook (the
34
+ danger check's apply line reports 6) and the item returns at the next run. Refreshing that installer from the kit's
35
+ `references/scripts/` copy is your own step.
36
+ - **The danger check binds what a file write replaces.** Every `writes:` line over an existing regular file now ends with
37
+ `— replaces sha256:<the bytes there now>`, so a file edited after the check moves the digest and needs a new yes.
38
+ `init` runs each check under the same environment it launches the apply with.
39
+ - **Shipped text.** Bootstrap steps 4 and 9, the upgrade's configs and footprint paragraphs, notes 1.2.0 and 3.0.0,
40
+ `deploy-tail.md`, `composition-handoff.md`, `contracts.md` and `recommendations.md` name the items, and the
41
+ `vehicles.json` seed note says the upgrade reports a differing body as pending; none keeps a "No Node" branch for
42
+ these writes.
43
+ - **Stated limits.** `status` and `uninstall` still read only `includeCoAuthoredBy`; a pre-arm installer is refreshed
44
+ by you; a declined `footprint-global` returns at the next run.
45
+
7
46
  ## 14.33.0 — the worktrees grant gets a writer: the agent writes it into this copy's local settings on the chat yes, and the worktrees stops name that route (AD-185)
8
47
 
9
48
  **The `worktrees-dir` upgrade item, which used to hand the user a settings line to paste, is now a harness item the
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: agent-workflow-kit
3
3
  description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow". New to the epic, story and task tier? Run `/agent-workflow-kit tier`. A typed decision with a confidence from Jev? Run `/agent-workflow-kit jev`.
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '14.33.0'
6
+ version: '14.34.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "14.33.0",
6
+ "version": "14.34.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
@@ -29,10 +29,12 @@ keeps it permanently via PR refs — so the safe default is "no mention anywhere
29
29
  Relay the writer's lines; a named refusal on stderr stops the migration and is relayed as worded.
30
30
  For `answer-missing`, ask exactly the missing answers and retry `--apply` once; a second withheld run stops.
31
31
  5. **Whenever the Attribution block reads `off`**, whether just inserted or already present,
32
- keep this hand step outside the writer: for Claude Code, set the harness flag so the automatic
33
- trailer stops (a doc directive cannot stop a harness-added trailer):
34
- - Ensure `.claude/settings.json` exists in the project root; create it as `{}` if absent.
35
- - Set `"includeCoAuthoredBy": false` (merge into the existing JSON; do not clobber other keys).
32
+ the harness setting stays outside the writer: for Claude Code it is the advisor's `attribution`
33
+ item, which the agent applies on the user's yes after its check, so the automatic trailer stops
34
+ (a doc directive cannot stop a harness-added trailer). It sets `attribution` to
35
+ `{"commit":"","pr":"","sessionUrl":false}` in the project's `.claude/settings.json`, every other
36
+ key kept. An existing "includeCoAuthoredBy": false already converges the item unless an
37
+ "attribution" key sets a commit or PR text.
36
38
  - For other tools, disable their equivalent co-author / footer setting if present.
37
39
  6. The writer refuses with `cap` rather than push `AGENTS.md` past its 100-line budget;
38
40
  trim the entry point and re-run the upgrade. Do **not**
@@ -42,12 +44,12 @@ keeps it permanently via PR refs — so the safe default is "no mention anywhere
42
44
 
43
45
  - `AGENTS.md` has exactly one `## ✍️ Attribution` block, with `off` or `on` (no leftover
44
46
  `{{AGENT_ATTRIBUTION}}` placeholder).
45
- - If `off` + Claude Code: `.claude/settings.json` contains `"includeCoAuthoredBy": false`, and
46
- the file is valid JSON (`node -e "JSON.parse(require('fs').readFileSync('.claude/settings.json'))"`).
47
+ - If `off` + Claude Code: the advisor renders no `attribution` item, and `.claude/settings.json`
48
+ is valid JSON (`node -e "JSON.parse(require('fs').readFileSync('.claude/settings.json'))"`).
47
49
  - The docs cap-validator is still green (`node scripts/check-docs-size.mjs` for Node projects).
48
50
 
49
51
  ## Rollback
50
52
 
51
53
  Delete the inserted `## ✍️ Attribution` block (and its trailing `---`) from `AGENTS.md`, and
52
- remove the `includeCoAuthoredBy` key from `.claude/settings.json` if you added it. No other
53
- files were changed.
54
+ remove the `attribution` key from `.claude/settings.json` if the `attribution` item wrote it. No
55
+ other files were changed.
@@ -23,19 +23,15 @@ loudly, never silently.
23
23
  3. **Review receipts:** receipts minted by pre-3.0.0/4.0.0 bridge wrappers carry no `posture`
24
24
  field and STOP satisfying the review-state gate (fail-closed, the probe-marker precedent).
25
25
  Refresh the placed bridges (init/upgrade does this), then re-run the required reviews.
26
- 4. **Commit guard (optional, consented):** a 2.x deployment's project-local
27
- `scripts/install-git-hooks.mjs` PREDATES the arm and the upgrade preserves existing scripts —
28
- refresh it FIRST, classified deterministically:
29
- `cmp -s scripts/install-git-hooks.mjs <installed-kit>/references/scripts/install-git-hooks.mjs`
30
- — identical ⇒ already current; DIFFERING ⇒ the copy is UNKNOWN (older deploy or customized —
31
- a consumer cannot enumerate shipped priors) ⇒ STOP and merge/replace by hand, never
32
- overwrite unasked (an explicit "my copy is an unmodified older deploy" answer is the one
33
- auto-replace lane). Then, once the declaration is final-run-capable, arm it:
34
- `node scripts/install-git-hooks.mjs --commit-guard "<installed-kit>/tools/commit-guard.mjs"`
35
- (the Recommendations advisor surfaces the exact one-liner) — and VERIFY the arm took:
36
- `grep -Fx 'node "<installed-kit>/tools/commit-guard.mjs" --check' "$(git rev-parse --git-path hooks)/pre-commit"`
37
- (the FIXED-STRING resolved line, exactly as the installer writes it) — absent ⇒ the arm did
38
- not take; re-run the installer refresh above before re-arming.
26
+ 4. **Commit guard (optional, consented):** once the declaration is final-run-capable, the
27
+ advisor's `commit-guard` item arms the pre-commit hook: the agent applies it on the user's yes
28
+ after its check, by the consent route of the Recommendations mode, and the apply reads the hook
29
+ back to verify the arm took. A 2.x deployment's project-local `scripts/install-git-hooks.mjs`
30
+ predates the arm, and the upgrade preserves existing scripts: an unarmed hook after the
31
+ installer's exit 0 makes the apply exit 6 with one line naming the hook, and the item returns
32
+ at the next run. A cmp -s against the kit's references/scripts/ copy tells whether the
33
+ project's copy is the current one; a differing copy may be an older deploy or the user's own
34
+ edit, and refreshing it is the user's own step.
39
35
  5. **Re-stamp:** init/upgrade re-stamps `docs/ai/.workflow-version` to 3.0.0.
40
36
 
41
37
  ## Verification
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "14.33.0",
3
+ "version": "14.34.0",
4
4
  "description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -18,7 +18,7 @@ The user chooses at bootstrap whether the AI artifacts are visible in the repo o
18
18
  diverge:
19
19
 
20
20
  - **visible** — artifacts are committed. When a `package.json` exists and the enforcement scripts are in place, wire the project's `package.json` scripts (`docs:check` / `docs:index` / `docs:index:check` / `docs:archive` / `docs:archive:check` / `docs:archive:issues` / `docs:archive:issues:check` / `prepare: node scripts/install-git-hooks.mjs`); add a minimal `.gitignore` (`docs/plans/`, `.claude/settings.local.json`) either way. This is the canonical model.
21
- - **hidden** (in-tree) — same files on disk, but the repo "looks normal": the AI/agent footprint is git-ignored via **one managed block in the project-local `.git/info/exclude`** (resolved with `git rev-parse --git-path info/exclude` — never the machine-global `core.excludesFile`, which would affect every repo on the host; **AD-014** amends **AD-006**). The kit's `tools/hide-footprint.mjs` is the single writer: it covers `KIT_OWN_PATHS ∪ the present subset of KNOWN_FOOTPRINT` (the full footprint table below — the kit's own artifacts **and** every other AI/agent tool's files). Per path: a **tracked** file → **ASK** (an exclude does nothing for it; only `git rm --cached` un-tracks it — the tool prints that command and never runs it); an untracked path already covered by a **tracked `.gitignore`** → dropped (redundant); a **present** file whose name is generic enough to be ambiguous (`falsePositiveRisk`) → **ASK**; everything else → **hidden**. `asks` are excluded from the block unless explicitly opted in. **Verify** treats a path as hidden only when it is **untracked AND ignored by our project-local block** (or a tracked `.gitignore`) — being ignored by the global excludes does **not** count. Re-running re-derives the block wholesale (sorted/deduped) → a clean re-run is a **zero-diff** no-op. On an existing global-excludes deployment the tool **detects + reports the residual legacy machine-global block and keeps it by default** (a harmless double-ignore; the local block wins precedence); removal is the explicit `--remove-global` (it prints the removed lines as a restorable backup), which **the agent only runs after asking** — another of the user's hidden repos on the same host may rely on the same root-anchored global lines. **Do not edit `package.json`** — that is a tracked change and would leak; the pre-commit hook (always untracked in `.git/hooks/`) calls the scripts via `node scripts/<x>.mjs` directly. Windows is supported (text edit, no symlinks; CRLF preserved).
21
+ - **hidden** (in-tree) — same files on disk, but the repo "looks normal": the AI/agent footprint is git-ignored via **one managed block in the project-local `.git/info/exclude`** (resolved with `git rev-parse --git-path info/exclude` — never the machine-global `core.excludesFile`, which would affect every repo on the host; **AD-014** amends **AD-006**). The kit's `tools/hide-footprint.mjs` is the single writer: it covers `KIT_OWN_PATHS ∪ the present subset of KNOWN_FOOTPRINT` (the full footprint table below — the kit's own artifacts **and** every other AI/agent tool's files). Per path: a **tracked** file → **ASK** (an exclude does nothing for it; only `git rm --cached` un-tracks it — the tool prints that command and never runs it); an untracked path already covered by a **tracked `.gitignore`** → dropped (redundant); a **present** file whose name is generic enough to be ambiguous (`falsePositiveRisk`) → **ASK**; everything else → **hidden**. `asks` are excluded from the block unless explicitly opted in. **Verify** treats a path as hidden only when it is **untracked AND ignored by our project-local block** (or a tracked `.gitignore`) — being ignored by the global excludes does **not** count. Re-running re-derives the block wholesale (sorted/deduped) → a clean re-run is a **zero-diff** no-op. On an existing global-excludes deployment the tool **detects + reports the residual legacy machine-global block and keeps it** (a harmless double-ignore; the local block wins precedence); removal is the advisor's `footprint-global` item, which the agent applies on the user's yes after its check — its `--remove-global` mode prints the removed lines as a restorable backup — since another of the user's hidden repos on the same host may rely on the same root-anchored global lines. **Do not edit `package.json`** — that is a tracked change and would leak; the pre-commit hook (always untracked in `.git/hooks/`) calls the scripts via `node scripts/<x>.mjs` directly. Windows is supported (text edit, no symlinks; CRLF preserved).
22
22
 
23
23
  **Visibility changes what git sees — never how the agent works (the load-bearing invariant).** Both modes carry the **same** working memory and run the **same** session protocol: `docs/ai` and the deployed workflow artifacts (`AGENTS.md`, the added `scripts/`) are read at session start and maintained on task completion **identically** whether visible or hidden. (Hidden mode git-ignores a *wider* footprint — other AI/agent tools' files too — but that is only a **visibility** surface; the working memory the agent actually reads and maintains is `docs/ai` plus those artifacts.) Hidden mode only makes the repo *look* normal to git; it is **never** a license to skip, defer, or down-prioritise a doc/state update. The single difference is the destination: in hidden mode those updates live on disk and never enter a commit (the footprint is excluded), so a commit there carries only the project's own tracked files while `docs/ai` is still kept current locally — **git-ignored ≠ optional-to-maintain**. An agent that reads "git-ignored / `git status` clean" and infers the working memory is optional has the model backwards. (This is why bootstrap/upgrade never wire hidden-mode docs into a commit, yet the session protocol still mandates maintaining them.)
24
24
 
@@ -78,7 +78,7 @@ When attribution is **`off`**, no mention of the agent, AI, or the model appears
78
78
 
79
79
  - **No `Co-Authored-By` trailers** and **no "Generated with …" footers** on commits or PRs.
80
80
  - **No AI/agent/model references** in code, comments, commit messages, PR titles/bodies, branch names, or `docs/` prose. The work reads as the human author's.
81
- - **Two enforcement layers** — the *Attribution* block binds everything an agent writes **by hand**; the automatic `Co-Authored-By` trailer is added by the **harness**, not the prose, so for **Claude Code** the kit also sets `"includeCoAuthoredBy": false` in the project's `.claude/settings.json`. Other tools: disable their equivalent co-author/footer setting if present.
81
+ - **Two enforcement layers** — the *Attribution* block binds everything an agent writes **by hand**; the automatic `Co-Authored-By` trailer is added by the **harness**, not the prose, so for **Claude Code** the advisor's `attribution` item sets the harness setting in the project's `.claude/settings.json`, applied on the user's yes after its check. Other tools: disable their equivalent co-author/footer setting if present.
82
82
 
83
83
  When **`on`**, the agent may add its standard trailer / footer per the user's tooling defaults. This
84
84
  block is about *attribution*, not authorship of the actual changes — quality, tests, and the "ask
@@ -1,6 +1,8 @@
1
1
  ### Mode: bootstrap
2
2
 
3
3
  <!-- opt-in-capability: enforcement -->
4
+ <!-- opt-in-capability: attribution -->
5
+ <!-- opt-in-capability: footprint-global -->
4
6
 
5
7
  Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKILL_DIR}/references/shared/composition-handoff.md · ${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md · ${CLAUDE_SKILL_DIR}/references/shared/command-shapes.md
6
8
 
@@ -19,7 +21,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
19
21
  - **First-contact orientation — say this before the first question (read-only).** In 1–2 plain lines, tell the user what this is — a portable memory & workflow system for this repo, so future sessions boot from a small structured memory instead of re-reading everything — and that they can run **`/agent-workflow-kit help`** any time to see every command. This is the "start here"; keep it to a sentence or two, then ask the three setup questions (the step-2 batched prompt).
20
22
  2. **Choose visibility — ask the batched prompt NOW (all three questions, per the preamble above) and wait until every answer is in.** This decides what gets tracked and is hard to reverse after a commit, so never assume the default silently: `visible` (committed — canonical, recommended) or `hidden` (in-tree, git-ignored via the **project-local** `.git/info/exclude` — one managed block covering the full AI/agent footprint, never the machine-global excludes). See [Visibility contract](${CLAUDE_SKILL_DIR}/references/contracts.md#visibility-contract).
21
23
  3. **Choose conversational language — answered in the step-2 batch.** Which language should the agent *talk to them* in — questions, explanations, summaries, status updates? Offer the language they're already writing in as the default. Carry the answer into the `{{COMM_LANGUAGE}}` slot of the *Communication language* block when `AGENTS.md` is created (step 5). See [Communication contract](${CLAUDE_SKILL_DIR}/references/contracts.md#communication-contract). This sets the **dialogue** language only — never the files.
22
- 4. **Choose agent attribution — answered in the step-2 batch.** May the agent attribute work to itself / to AI — `Co-Authored-By` trailers, "Generated with …" footers, "AI"/agent/model mentions in code, comments, commit messages, PR titles/bodies, or docs? **Default to `off`** (no agent/AI mention anywhere) unless they opt in — people are routinely surprised to find an AI listed as a repo contributor. Carry the answer into the `{{AGENT_ATTRIBUTION}}` slot of the *Attribution* block when `AGENTS.md` is created (step 5). **If `off` and the project uses Claude Code**, also set `"includeCoAuthoredBy": false` in the project's `.claude/settings.json` (create it if absent) — the trailer is added by the harness, so a doc directive alone won't stop it. See [Attribution contract](${CLAUDE_SKILL_DIR}/references/contracts.md#attribution-contract).
24
+ 4. **Choose agent attribution — answered in the step-2 batch.** May the agent attribute work to itself / to AI — `Co-Authored-By` trailers, "Generated with …" footers, "AI"/agent/model mentions in code, comments, commit messages, PR titles/bodies, or docs? **Default to `off`** (no agent/AI mention anywhere) unless they opt in — people are routinely surprised to find an AI listed as a repo contributor. Carry the answer into the `{{AGENT_ATTRIBUTION}}` slot of the *Attribution* block when `AGENTS.md` is created (step 5). **If `off`**, the advisor's `attribution` item of step 11 sets the harness setting in the project's `.claude/settings.json`, applied on the user's yes after its check — the trailer is added by the harness, so a doc directive alone won't stop it. See [Attribution contract](${CLAUDE_SKILL_DIR}/references/contracts.md#attribution-contract).
23
25
  5. **Entry-point doc.** If `AGENTS.md` / `CLAUDE.md` already exist (step-1 recon), do **not** overwrite — show the user and ask whether to merge or replace. Otherwise create `AGENTS.md` (the cross-agent standard — Codex / Cursor / Devin Desktop / Copilot read it natively) from `${CLAUDE_SKILL_DIR}/references/templates/AGENTS.md`, and symlink `CLAUDE.md -> AGENTS.md` (`ln -s AGENTS.md CLAUDE.md`) for Claude Code — single source, no duplication. For nested context, add a subdir `AGENTS.md` (+ a `CLAUDE.md` symlink beside it for Claude Code).
24
26
  6. **Deploy `docs/ai/`.** Create every `docs/ai/` file + `pages/` + the seed `adr/` store + the seed `specs/` store from `${CLAUDE_SKILL_DIR}/references/templates/` (the template loop deploys every template except `AGENTS.md`, `adr-record.md` and `SPEC_TEMPLATE.md` — the latter two are skill-home authoring references, never deployed). This deploys the HOT ADR window **`docs/ai/decisions.md`** and the seed navigator **`docs/ai/adr/log.md`** (its `adr/` subdir copies in wholesale), the spec store root **`docs/ai/specs/index.md`** (its `specs/` subdir copies in wholesale), the `.md` docs, **and** the four seeded, user-editable strict-JSON configs: **`docs/ai/orchestration.json`** (the per-project recipe defaults the `procedures` advisor reads), **`docs/ai/gates.json`** (the project's gate declaration — seeded as the no-consent EMPTY list; FILLING it is the consented `gates-init` preview, `${CLAUDE_SKILL_DIR}/references/modes/gates.md`), **`docs/ai/autonomy.json`** (the per-project autonomy policy — seeded SPARSE with the onboarding note only, so it is exactly the computed defaults until the user declares levels via `/agent-workflow-kit set-autonomy` or by hand), and **`docs/ai/vehicles.json`** (the executor vehicle's model, effort and fallback as a setting the kit's readers resolve into one posture; the placed executor's body follows it — seeded once, hand-edited, never clobbered). Keep each `.md` file's frontmatter (`type / lastUpdated / scope / staleAfter / owner / maxLines`); the `.json` seeds carry no frontmatter (the docs cap-validator globs `*.md` only, so they are inherently skipped).
25
27
  7. **Fill templates** per the table below. **Then materialize the navigator, as the last `docs/ai` step:** the entry point declares `docs/ai/index.md` always-loaded and it is a GENERATED artifact with no template, so run `node ${CLAUDE_SKILL_DIR}/references/scripts/check-docs-size.mjs --ensure-index --root=<project>` and relay its one outcome line (`ensure-index: regenerated` / `already-current`, or a `write-refused` / `probe-failed` line naming the offending path — either is a STOP: resolve it before reporting the deploy, since the navigator the entry point declares does not exist yet). The generator runs from **this skill's own directory**, so a project with **no Node runtime** is covered too: the agent host runs it, and step 8's copy is not a precondition.
@@ -29,8 +31,8 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
29
31
  - **hidden** — run the kit's hide writer (one managed block in the **project-local** `.git/info/exclude`, covering the kit's own artifacts **and** every known AI/agent tool's footprint — never the machine-global excludes): `node ${CLAUDE_SKILL_DIR}/tools/hide-footprint.mjs --dir <project> --dry-run` **first** (changes nothing, prints the plan), then the same without `--dry-run`. Handle what it surfaces, in plain language (never the tool's internal terms):
30
32
  - A path it reports as **already committed** can't be hidden by ignoring it and is **never un-tracked silently** — show the user the printed `git rm --cached <path>`, let them decide, then opt it in with `--include=<path>`.
31
33
  - A **present file with a generic name** it flags (e.g. `GEMINI.md`) → ask before `--include=<path>`.
32
- - If it reports a **leftover machine-wide ignore block** from an older deployment, **ask before removing it** — it could affect another of the user's repos that relies on the same machine-wide rules; on a yes, re-run with `--remove-global` (prints a restorable backup). Otherwise it is kept (harmless — the project-local rules win).
33
- - Report the result plainly (what is now hidden). **No Node on the agent host** → write the one managed block into `.git/info/exclude` by hand from the contract's path list, and report the manual step. **Windows is supported.**
34
+ - A **leftover machine-wide ignore block** from an older deployment is kept and reported (harmless — the project-local rules win); the advisor's `footprint-global` item of step 11 removes it on the user's yes after its check, which prints each line it removes — another of the user's repos may rely on the same machine-wide rules.
35
+ - Report the result plainly (what is now hidden). **Windows is supported.**
34
36
  - **Do not edit `package.json`** in hidden mode — a tracked change leaks the whole system.
35
37
  10. **Stamp the deployment lineage.** **First re-run the step-7 navigator finalizer**
36
38
  (`--ensure-index`) whenever a later step touched `docs/ai` — the reconciles that follow the
@@ -22,7 +22,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
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` and `customized` render NOTHING** (a working vehicle is not a recommendation), a config naming no `subagent` slot renders nothing, 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.
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
 
@@ -38,6 +38,8 @@ A variant's posture note, where its key has one, is printed by `init` from this
38
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 **HAND-APPLY** (rename the id, or repoint the cmd at this checker's `--check` form). **`unminted`** stays what it was for both copies: a gate certain to refuse on every run, attention, and NO acknowledgement silences it — only the rendered verb differs (with the gate already declared elsewhere the missing half is the record alone, so the line is `--write-baseline`). Risk profile: `docs/ai/gates.json` is a PRIVILEGED file, so every arm here either records an acknowledgement or hands you an edit — the kit declares nothing on its own.
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
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 **HAND-APPLY** and the two remedies are marking the real producer with `"lcovProducer": true` or dropping the checker. Never a `node --test` prescription over a project that has no such suite, never the fill preview, and never an acknowledgement — a dead pair is broken, not narrow, and removing a producer after an acknowledgement lands right back in this arm. **`coverage-domain-narrow`** — the producer/checker pair IS live and the tree is still dominated by what the domain excludes: nothing is broken, and the honest sentence is that certification covers the assessable minority. Its apply is the consent-gated **ack writer** preview (a NEUTRAL fingerprint into `docs/ai/acks.json` as `coverageDomainAck`, never a security key); after the SAME confirmation you run the `--apply` it prints. The fingerprint binds the FACT — the verdict plus the unsupported extensions present, never the file counts — so an acknowledged project stays quiet as it grows and re-fires when a new unsupported language arrives or the verdict flips. The census reads the TRACKED tree with a read-only `git ls-files`; a tree it cannot read (a non-git deployment) becomes a stated skip, so optimality is withheld rather than assumed. Risk profile: no enforcement change of any kind — one acknowledgement recorded in a family-owned file, and one hand edit that stays the maintainer's.
43
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 a NAMED **HAND-APPLY 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 maintainer wants BEFORE the confirmation; on "seed" run the apply; on "decline" run NOTHING — hand over the preview line and let them run it and its printed `--apply`; never run the seed after a decline was chosen, and never run the decline preview for them. **`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.
@@ -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` / `re-derived` / `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.
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 → ASK before `--remove-global`**, default keep + report). No Node on the agent host / Windows → as bootstrap step 9 (`${CLAUDE_SKILL_DIR}/references/modes/bootstrap.md`). This runs on **every** hidden upgrade, like the methodology slot — no lineage-head bump, no migration file.
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 re-derivation · 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, the executor's re-derivation among them. 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` / `re-derived`; 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).
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,7 +41,7 @@ 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.** A placed `.claude/agents/executor.md` is kept byte-equal to the body derived from `docs/ai/vehicles.json`: a differing regular file is rewritten (`re-derived` — a hand edit is replaced, never preserved), and an absent one is `not-placed` (the agents mode places it on consent; this ensure never 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
 
@@ -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 / a leftover machine-wide block). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
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 there is **no machine-global write** at any step (a stale memory's
12
- residual global block is cleaned via the upgrade reconcile, below).
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 you **must** also set `"includeCoAuthoredBy": false` in the project's `.claude/settings.json` (create it if absent). Other tools: disable their equivalent co-author/footer setting.
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**). 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.
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 run derive it from model and effort, and fallback names the model of the one retry; seeded once and never clobbered, hand-edited as three bare tokens with fallback differing from model.",
2
+ "_README": "The executor vehicle's model, effort and fallback as a project setting, read by the kit's vehicle-settings readers into one posture, which the placed executor's body follows: the agents writer derives it from model and effort, the upgrade reports a differing body as pending, and fallback names the model of the one retry; seeded once and never clobbered, hand-edited as three bare tokens with fallback differing from model.",
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, replaces: readFileSync(path) });
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 === 'create') writes.push({ path, bytes: plan.bundleContent });
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 replaces = write.replaces === undefined ? '' : ` — replaces sha256:${sha(write.replaces)}`;
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;