From 643f7ffcf567c64b9abf28d4e32d8e10b899254a Mon Sep 17 00:00:00 2001 From: Justin Forth Date: Wed, 30 Sep 2026 08:57:26 -0600 Subject: [PATCH 001/135] =?UTF-8?q?docs:=20harvest=205=20design=20spec=20a?= =?UTF-8?q?nd=20trace=20=E2=80=94=20v1.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The design for landing the sixteen open issues (#58's residue, #60–#74) and the whole-kit review as the kit's first tagged release: decisions D41–D62, ten clusters in commit order, six execution-time probes, and a 371-row trace of every ask by issue and comment id. Co-Authored-By: Claude Opus 5.5 --- ...2026-09-30-cascade-kit-harvest-5-design.md | 593 ++++++++++++++++++ .../specs/2026-09-30-harvest-5-trace.md | 394 ++++++++++++ 2 files changed, 987 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-30-cascade-kit-harvest-5-design.md create mode 100644 docs/superpowers/specs/2026-09-30-harvest-5-trace.md diff --git a/docs/superpowers/specs/2026-09-30-cascade-kit-harvest-5-design.md b/docs/superpowers/specs/2026-09-30-cascade-kit-harvest-5-design.md new file mode 100644 index 0000000..b0955f8 --- /dev/null +++ b/docs/superpowers/specs/2026-09-30-cascade-kit-harvest-5-design.md @@ -0,0 +1,593 @@ +# Harvest 5 design — v1.0.0: what three targets running ahead of the kit settled + +**Date:** 2026-09-30 +**Status:** Draft for operator review +**Type:** Design spec for evolving `context-builder-kit` itself, and its first tagged release +**Prior:** harvest 1 (`2026-07-04-…`, PR #4) · harvest 2 (`2026-08-09-cascade-kit-harvest-2-design.md`, PR #5) · axis parity (`2026-08-09-axis-parity-pass-design.md`, PR #7) · harvest 3 (`2026-09-06-cascade-kit-harvest-3-design.md`, PRs #54–#57) · harvest 4 (`2026-09-21-cascade-kit-harvest-4-design.md`, PR #59) +**Trace:** `2026-09-30-harvest-5-trace.md` — one row per ask, with the ask's issue and comment id + +## What this is + +Harvest 4 landed on 2026-09-22 as `74edf84`. Within a week all three targets had synced it, and then +they ran ahead of the kit: + +- `j4th/you-are-hear` PR #90 recalibrated for Opus 5.5, generalised `finish-ab` to two to four arms + with a headless runner, and measured subagent-driven `/finish` (not adopted). +- `j4th/echosphere` #55 and #56 hardened a path guard against ten bypass spellings and rebuilt the + review bot's "posted" assertion. +- A third, private target's harvest-5 PR (100 commits, merged 2026-09-30) absorbed both and added + its own final check. It is the latest exercised form of nearly every item. + +The findings sit in sixteen open issues: #58 (reopened 2026-09-30 with a residue audit, rows R1–R14 +and B) and #60–#74. + +This pass lands all of them, and releases the kit as **v1.0.0**. It is the first tag. The five +earlier harvests are back-tagged, and a `CHANGELOG.md` records each release with the sync notes a +target needs. The install and upgrade path now speaks in versions. + +### The pre-spec pass + +A 20-agent workflow (`wf_d6e0ae85-8be`, 2026-09-30) ran two branches. + +**Map.** Eight cluster readers (Sonnet 5.5 at `high`) produced **281 items**. Each item records: +- where the change lands in the kit; +- whether the defect is still live at `74edf84`; +- the exercised fix it ports from; +- what to sanitize, and any merge hazard. + +Two checks followed: +- A completeness critic found **22 asks** no item carried, and **6 misreads**. All are folded into + the trace. +- A refute-by-default verifier re-tried the 15 "not live" claims. One was refuted: the ADR guard is + also bypassed through a linked worktree today. + +**Review.** Five lenses reviewed the whole kit at the operator's request: consistency, Claude Code +best practices against the live docs, security and shell safety, release readiness, and portability +with dated facts. They produced 80 raw findings, which file-and-line dedup cut to **68 findings**. +- 38 verified by one refute-by-default Opus verifier per lens. None refuted. +- 30 over the per-lens bound of 8, returned unverified. + +The main loop re-checked the five highest-impact verified findings against the raw doc pages. All +five hold: + +| Finding | Source | +|---|---| +| Commands use `$1`, which is the **second** argument; `/finish 42` leaves `$1` as literal text and works only through the appended `ARGUMENTS:` line | skills page: "`$0` for the first argument or `$1` for the second"; "An indexed placeholder with no argument at its position stays as literal text" | +| `enabledPlugins` ships bare names; keys are `@` | plugin-marketplaces page, § Entry name | +| Hook commands are shell form with an unquoted `${CLAUDE_PROJECT_DIR}`; a path with a space makes every guard fail open | hooks page: "Set `args` whenever the hook references a path placeholder" | +| `tooling.md` (always loaded) says a repeated list in local settings "silently replaces" the committed one | settings page heading: "Lists merge instead of overriding" | +| `.mcp.json.example` launches two npm packages that return 404 and one that is deprecated; its hosted entries lack `type` | `npm view` (2026-09-30); mcp page | + +## Governing constraints — five carry, two are new + +1. **Portability invariant.** No project-specific identifier in kit content. A public target is named + only in this spec and the trace, never in shipped kit files. The private target is named nowhere. +2. **Framework-not-tooling, per file on merits.** A fixture that makes a policy the kit already + mandates runnable ships beside the existing ones. +3. **Sanitized only.** Every ported hunk is re-authored onto the kit's own text. A raw merge of a + target's file imports that project's prose. Echosphere's application of #69 found this: a naive + merge of `agent-cost.py` produced two `CACHE_READ` dictionaries, and the second silently shadowed + the first. +4. **One-way.** The kit changes; no target is modified by this pass. +5. **Defer-to-exercised.** The latest exercised form wins unless an earlier one says why it must not + generalise. For most items that is the private target's merged PR, re-authored. Where the kit's + application differs from a target's, the kit wins and the difference is a sync note. +6. **Traceability (R14).** Every ask has a trace row with its location, `#N/body` or `#N/c`, and its item label. An issue closes only when every row for it is landed or a named + non-goal. Before merge, a refute-by-default audit re-checks every row not marked landed. +7. **Verify at execution (D56).** An unverified review finding lands only if it still holds when its + commit is made. A platform fact is re-fetched raw and matched with `grep -F` on the day it is + written into kit text, and dated there. + +## Decisions (settled with the operator, 2026-09-29/30) + +Numbering continues from harvest 4 (D30–D40). + +| # | Decision | Outcome | +|---|---|---| +| D41 | Release mechanics | After merge, annotated tags go on the five past harvest merges and on this one: **v0.1.0** `db0eac7` (harvest 1, with everything before it), **v0.2.0** `6560f72`, **v0.3.0** `88b1ede` (axis parity), **v0.4.0** `e92e9c4` (harvest 3's four PRs), **v0.5.0** `74edf84`, and **v1.0.0** on this PR. `CHANGELOG.md` has one section per tag, and each carries **Sync notes**: what a target does by hand. The GitHub Release text is the v1.0.0 section. SemVer is informal: a major bump means a sync needs hand reconciliation beyond `git merge-file`; a minor bump is a harvest; a patch is fixes only. Scaffold's Kit commit row records `vX.Y.Z (sha)`. | +| D42 | Shape | One PR. Atomic commits grouped by the item they close. The verification block is green after every commit. | +| D43 | Shared path resolver | **Ship `.claude/hooks/lib/resolve-path.sh`.** This reverses harvest 4's no-helper-library non-goal. The suffix match #60 proposed closes 3 of the 10 bypass spellings. The resolver closes all 10, plus a bind mount and a linked worktree. The frozen-corpus recipe gives it a second caller in any target with a corpus. With it comes the sourced-helper contract in § Hook authoring: source only after the stdin drain, fail open naming the backstop when the helper is missing, define functions only. The `!/.claude/hooks/lib/` gitignore trap is named too. | +| D44 | Behavioural hook fixtures | Ship all three: guards, payloads and protected paths. They need only bash, git and jq, and the block runs them. They close R1, R6 and R8. | +| D45 | finish-ab | Ship the 2–4-arm workflow and the headless runner `run-arms-headless.py`, with the latest hardening. A workflow agent has no Agent tool, so only the runner can measure a `/finish` whose review floor fans out. | +| D46 | review-sweep | Dedup keys on file and line, using the normalized title only when a finding names no line. A merged finding carries every distinct title, and the verify prompt asks which report the evidence proves. Also: finders can carry a prompt, a failed roster read logs why it threw, find and verify prompts carry a read-only clause, and an interrupted run is re-run fresh. | +| D47 | Review-bot model | Family aliases `opus` and `sonnet`, never `best`. An explicit `--effort` on every branch, including `claude.yml` at `high`. A step records the model that actually ran. `ANTHROPIC_MODEL` is never set. The action's pinned SHA is the model pin, and Dependabot bumps it. | +| D48 | Review-bot trigger | `paths:` with `!**/*.md`, then `.claude/**` and `docs/adr/**` re-included last, so rule and ADR markdown is reviewed. A trigger fixture in the kit sub-block pins the order. | +| D49 | Recalibration | Opus 5.5, Sonnet 5.5 and Claude Code 2.1.284+:
• effort defaults per model and per surface; the kit's `high` settings called pins that need a sweep;
• thinking named per model;
• the ladder from Opus 5.5; current prices and per-model cache-read rates;
• Haiku 4.5's re-check trigger at 2026-10-15;
• subagent-driven `/finish` recorded as measured and not adopted, citing Anthropic's own "When delegation doesn't pay";
• the triad's task-tracking leg becomes "a tracked checklist". | +| D50 | Instruction budget | Record always-loaded bytes before and after. The block prints a non-failing warning above 140,000 bytes. | +| D51 | Block as a gate | #73's two fixes. Templates wire the block into `check` (R4). A check that every CI workflow and script the rules name as a backstop exists. `CACHE_READ` diffed against the quoted pricing sentence. The retired-section check widened to any citation. The spellchecker note (R5). | +| D52 | Target hygiene | The anchored `.gitignore` harness block. Kit-owned code kept out of a target's formatter and linter, with the exclusion forced for explicit paths. What Dependabot really covers for container images, and its three gaps. mise inline tasks run under bash with pipefail. Pinned MCP servers. | +| D53 | Citation hygiene | Kit content cites its own issues as `context-builder-kit#N` or not at all; a bare `#58` links to the target's own #58. Citations point at headings every target has by construction, which means the conventions, never a CONTRIBUTING or STANDARDS heading a template does not emit. | +| D54 | Branch rule | A PR that closes any issue carries that issue's key in the branch. The issue-less form is only for work no issue tracks. The Quick reference names both forms, and names the github-issues key form. | +| D55 | Frozen-corpus backstop | Consultation's enforcement set gains the list of ways a corpus CI check is fooled, and each closure. The bootstrap checklist gets one row per enforcement item. The ADR job takes the `:(glob)` body, fails closed on any git error, and checks out blobless. No corpus CI script ships. | +| D56 | Review scope | The whole-kit review's 68 findings join v1.0.0. The 38 verified land as written. The 30 unverified are re-checked when their commit is made, and land only if they hold. | +| D57 | Commands | `disable-model-invocation: true` on `/finish`, `/enrich`, `/intake`, `/pr-respond` and `finish-procedure`. They open branches, issues and PRs, the same one-way doors the phase skills already guard. Arguments come from named `arguments:` frontmatter, never `$1`. | +| D58 | `.mcp.json.example` | Rebuilt. `${VAR}` references instead of literal secrets; `type: "http"` on hosted servers; Linear's hosted endpoint; `time` pinned through `uvx`. The GitHub MCP entry is dropped, since `gh` is the kit's GitHub interface (D22). `.mcp.json` is committed with references, the model `tooling.md` already states. | +| D59 | Budget lever | Split `knowledge-backend.md` (20,336 bytes, always loaded, the one rule not yet split) into an always-loaded contract and a path-scoped `knowledge-backend-reference.md`. The recalibration's growth is paid from it. **Target: no net growth over 130,628 bytes.** | +| D60 | Install and upgrade | The Quick start extracts only the drop-in set from a **tagged** tarball, never the whole repository over the target. The README's "no upgrade mechanism" becomes the `git merge-file` sync against the installed tag. | +| D61 | Unreadable payloads | A payload jq cannot parse makes a hard-deny hook refuse and an ask-gate ask, instead of failing open. A missing dependency still fails open, naming its backstop. Probed at `74edf84`: the lock-file, main-branch and PR-state guards all pass a lone-surrogate payload today. | +| D62 | Repo hygiene | At HEAD, bracket-split the private target's name in the harvest-4 plan's sanitization grep. History keeps the old text; a public history rewrite is not proposed. | + +### Settled calls — the mappers' open questions, decided with their recommendations + +**Guards** +- The helper ships whole. Its corpus-mode functions are tested through a corpus double local to the + fixture. No corpus hook ships. +- `protect-lock-files.sh` keeps deciding on the path text, and gains a `Not seen:` line: an edit + through a symlink under another name. +- The ADR deny is path-independent: an existing `docs/adr/NNNN-*.md` is denied in any checkout, and + linked worktrees count as roots. The hook header states why: every target keeps ADRs at that + path, and a session in one target edits its siblings. +- The ADR job uses a three-dot diff (from the merge base). A two-dot diff reports an ADR added on + `main` as deleted when the PR branch is behind. +- The kit's own workflows get `defaults: run: shell: bash` and a named runner image, as the + templates do. +- The backstop-exists check reads workflow and script paths in the kit sub-block. Task-runner names + (`mise run X`, `just X`) are a templated project-sub-block line. The mutation table's ADR row names + its workflow file. +- #62's backstop wording is a bracketed slot, filled at scaffold's rule-file disposition pass. The + project sub-block refuses an unfilled slot. +- The corpus enforcement rows go into the bootstrap checklist's verification matrix, conditional on + a designated corpus. + +**Gate** +- Scaffold's disposition table gains a row for `cbk-conventions-reference.md`'s manifest and + lockfile globs. Without it, a fresh scaffold fails its own block. +- Every fixture runs in the kit sub-block. `hook-guards-fixture.sh` skips a knowledge-backend hook the + axis deleted, and reads the hook's name from the registry. +- A case that needs a tool the host lacks (`unshare`, GNU `stat`) prints an explicit `SKIP` line. + Fixtures state the userland they assume. A skip is never silent. +- Light mode's minimum task set includes the verification task. +- The runner owes no rail for "kit sub-block complete" beyond #73's project rail. + +**Review bot** +- The review-assert fixture ships with synthetic comment pages authored for the kit. + `extract-run-block.sh` is shared by it and the ADR-job fixture. +- The template's default effort stays `high`. +- Template comments state the action-bump re-check duty themselves, and cite `orchestration.md` only + secondarily. +- The record-model step runs with `continue-on-error: true`, because it is informational. +- Two issue quotes are reworded to the live page's text: the `fable` alias history, and the + `paths-ignore` negation rule. +- The assert step's comment states its base-SHA limit for PRs into a non-default base. +- The trigger fixture passes, with a notice, a filled workflow that has no path filter. +- Dated alias facts (which action release moves `opus` and `sonnet`) live in + `orchestration-reference.md` and the CHANGELOG, not in the templates. + +**Recalibration** +- The contract keeps operative clauses. Quotations, per-model numbers and changelog sub-lists go to + `orchestration-reference.md`. +- Alias wording names the provider and the version, e.g. "on the Anthropic API from Claude Code + 2.1.280". +- Probe P2 below settles "a workflow agent has no Agent tool" on the release CLI, dated, before + `pr-review.md` states it. +- The capstone close-marker sentence goes into `finish.md` item 8 as a dated observation: GitHub + rolls no closure up the sub-issue tree. This settles the critic's conflict flag. +- The subagent-driven arm protocol is not shipped. Its mechanics are described in prose. +- #65: the reference half's slot becomes prose, and the project sub-block refuses the contract's + unfilled posture slot. `§ The three surfaces + resolution order` is renamed `§ The dispatch + surfaces + resolution order` in both halves, with every citation swept. +- The synthesis-slot row gains a re-sweep note for the cost page's Fable 5.1-at-`low` result. + +**Harness** +- Every file is ported from the private target's merged form (a strict superset of the others), + sanitized. +- The runner's unconditional `mise trust` becomes an optional `worktree_setup` list. +- The MCP allowlist is a constant: `context7` and `time`. +- The runner's docstring names the environment it strips. +- The operator brief's review-workflow line becomes mode-aware. +- No verdict-rule template ships. The launching session adjudicates contamination. +- Judges get the read-only clause. + +**Review conventions** +- A merged finding is triaged by the report its verifier named. Its other titles stand as + unverified. +- No fourth `## Review gate` line. A post-floor verification workflow records on the existing lines, + as "not a second floor". No saved script ships for it. +- #74 item 2 lands in rough-in's `research-phase.md`, with a pointer in `contract.md` and a test + criterion. +- The accounting test's existing labels are left as they are. + +**Hygiene** +- The `.mcp.json.example` rebuild follows D58. +- `.claude/workflows/*` joins `format-on-edit.sh`'s skip floor. +- The kit's own `.gitignore` carries the harness block, plus `/.claude/settings.local.json`. +- The Dependabot non-Docker-Hub cooldown wording is re-authored from a fresh read. +- `tooling.md` step 1 carries the mise version floor. +- The kit's `dependabot.yml.example` gains the docker stubs. + +**Release** +- Tags as mapped in D41. Harvest 3's four PRs fold into v0.4.0. Tags are annotated. +- CHANGELOG headings carry the date and PR number. The squash sha is unknown until merge. +- The drop-in set: + - `.claude/` + - `.mcp.json.example` + - `.github/dependabot.yml.example` + - `.github/workflows/adr-immutability-check.yml` + + Never the README, LICENSE, CLAUDE.md, `.gitignore` or `docs/`. Scaffold instantiates the ADR + starters from `references/adr-starters/`. +- #66's three restatements become pointers. Its heading is retitled `## Relation grains`. +- R14's audit method is codified in this repository's `CLAUDE.md` § Working in this repo, as the + gate for every future harvest. +- The fail-fast residual stays recorded here only (D31). +- The Kit commit fallback for a target with an older, immutable `scaffold.md` goes in the reference + half. +- The `STANDARDS.md` citations in the executor procedure are in scope under D53. +- No separate workflow-fixtures task ships: the block runs every fixture. This settles the critic's + other conflict flag. + +## What lands — ten clusters, in commit order + +The block must be green after every commit, so each fixture lands in the same commit as the fix it +pins, and the gate lands first. + +### V1 · The block as a gate — #73, #58 R4 R5 (D51) + +- `run-verification-block.sh` gains a third rail. In a filled target, where `docs/cbk/scaffold.md` + exists, the output must also contain `verification: project sub-block complete`. It lands with + `run-verification-block-fixture.sh` (six cases), which the block runs. +- The Stop-hook check becomes `stop_hook_clean`. Exit 0 with a `WARNING` is "the hook could not + look", and that is red in a gate. Its negative case (outside any checkout, under a git ceiling) + lives in the block, so a regression is caught where the check runs. +- The blueprint `tooling.md` minimum task set, including light mode, gains a verification task that + `check` depends on. The bootstrap checklist's verification matrix runs it and expects both + sentinels. +- § Verification's preamble names the bracket idiom's cost for spellcheckers. It shows the `typos` + `[type.]` table with `extend-glob` naming the one file, never a repo-wide ignore. +- The budget line prints a non-failing warning above 140,000 bytes. + +### V2 · Guards and their CI backstops — #60, #61, #62, #58 R1 R6 R8 R11 R13 (D43, D44, D55, D61) + +- **Hook.** `lib/resolve-path.sh` ships. + - It reads the payload exactly: one jq, and an unparseable payload returns an error. + - It resolves each path twice, lexically and physically, one component at a time through + symlinks. + - It refuses `/proc` and unreadable symlinks. + - It compares device and inode with bash's `-ef`. + - Its roots are the hook's own checkout (derived lexically from `BASH_SOURCE`), linked worktrees, + and `CLAUDE_PROJECT_DIR`. +- **ADR guard.** `protect-immutable-adrs.sh` is rewritten onto the helper. The existence test runs on + the resolved path, so a new ADR stays creatable. Its header gains `Path:`, `Not seen:` and + `Depends:` lines, naming the residuals: Bash-tool writes, case-insensitive filesystems, hand + edits, and a symlink swapped between check and write. +- **D61.** Every hard-deny guard refuses an unreadable payload, and the ask-gate asks. § Hook + authoring's fail-open bullet gains the distinction: an environment defect opens; unreadable input + to a guard closes. +- **Main-branch guard.** `protect-main-branch.sh`'s `git commit` pattern stops matching only a + bare leading `git`. `bash -c`, `sh -c`, quoting, `/usr/bin/git` and chained verbs are caught, and + the same anchor in `guard-pr-state.sh` is fixed. The header states what a pattern guard cannot + see. +- **#62.** Every fail-open warning names its surviving backstop, with a bracketed slot for the + target's CI lockfile check and ruleset. The PR-state gate says honestly that no mechanical + backstop exists. The ADR hook's jq warning is included; that one came from the audit, not #62. +- **R8.** The lock-file fallback's remediation says to give the file a case arm of its own above the + `*.lock)` arm. +- **R13.** The fork detector's rule 1 states what it guarantees. The Residual line stays. +- **R11.** § Hook authoring and both advisory exemplars' `Register:` headers say that wiring one + takes three edits, not one. +- **§ Hook authoring** gains the sourced-helper contract, and one sentence holding a target's own + hooks to the same contract. `hook-contract-fixture.sh`'s early-reader tripwire reads `lib/*.sh`. +- **ADR job** (`adr-immutability-check.yml`): + - the `:(glob)` pathspec body, which parses no paths; + - `--diff-filter=a --no-renames`, three-dot from the merge base; + - fails closed on any git error; + - `filter: blob:none`, `shell: bash`, a named runner image. + + It lands with `adr-ci-body-fixture.sh`, which extracts the job's own `run:` body. The block pins + `absent grep 'mapfile … <('`. +- **Consultation's frozen-corpus enforcement set.** It gains the red-team closure list, and says + that whether an added file passes is the target's call. The recipe says to extend the ADR job + with the parse-nothing body. The bootstrap checklist gets one row per enforcement item. +- **Backstop-exists check.** Every `.github/workflows/*.yml` and script path the mutation table or + the hook registry names must exist. The checker is asked about bogus names first, so it cannot + pass by matching nothing. +- **Fixtures, run by the block:** + - `protected-paths-hook-fixture.sh`: the ADR half, the helper-missing and no-jq cases, and the + corpus double; + - `hook-guards-fixture.sh`: main-branch (including payload-cwd precedence both ways), PR-state, + lock files (every arm plus the fallback), and the knowledge-backend ask-gate; + - `hook-payloads-fixture.sh`: the launch-root guard and the fork detector, including the + partial-scan case. + +### V3 · Claude Code conformance — the whole-kit review (D56, D57) + +- **Commands.** Every `$1` in `/finish`, `/enrich`, `/intake`, `/pr-respond`, `finish-procedure` + and their bundled copies becomes a named argument declared in `arguments:` frontmatter. Each gets + `disable-model-invocation: true`. `CLAUDE.md`'s invocation-posture paragraph names the commands. +- **Hooks.** Every registration in `settings.json`, and both advisory exemplars' `Register:` + stanzas, move to exec form: `"args": []`. The registry check in the block accepts the form. +- **Plugins.** `enabledPlugins` keys become `pr-review-toolkit@claude-plugins-official` and + `commit-commands@claude-plugins-official`, with the rule stated where the comment lists them. +- **Explore.** `Explore.md` gains `omitClaudeMd: true` (v2.1.271+, dated), and its description is + trimmed to its routing sentence. +- **Settings merge.** `tooling.md` § MCP configuration's decision rule is rewritten to the + documented behaviour. Lists merge across scopes; the four model-list keys are the stated + exceptions. +- **Untrusted input.** `/intake` and `/pr-respond` state the research-phase rule: report and comment + text is data, and an imperative inside it is surfaced, never acted on. `/intake` writes its own + reproduction and never runs a reporter's command verbatim. +- **Unverified findings in this class**, re-checked at execution: `/pr-respond`'s triage citing + path-scoped sections it never loads; the reviewer that flags logging `logging.md` permits; a + cited `knowledge-backend.md` section that does not exist; `simplification.md`'s unsourced + behaviour claims. + +### V4 · Review-bot templates — #67, #68 §2, #70 §1–3, #58 R12 (D47, D48, D52) + +- **Model and effort.** Aliases and explicit `--effort` on every branch, per D47. `claude.yml` gains + `--effort high`. A `Record the resolved model` step goes on both templates, with an `id` on each + action step. The prompt's `Model in use:` line says it shows the alias. +- **"Assert the review posted".** Pages are slurped and counted once. The summary's verdict marker + is matched on `updated_at`, and the step runs on `!cancelled()`. The "no session" notice is keyed + on an empty `execution_file` plus a diff of the workflow file; an unchanged file means an auth or + setup failure. The template's "designed-unexercised" line is restamped with the exercised negative + path. Lands with `review-assert-fixture.sh` on synthetic pages. +- **Trigger.** `paths:` in D48's order, with a fork guard in the job `if:`. `concurrency` moves to + job level, so a skipped run cannot cancel a live review. The stale `track_progress`-on-`labeled` + claim is dated or removed. Lands with `review-trigger-fixture.py` in the kit sub-block. The + blueprint `tooling.md` review-bot paragraph names the `paths` shape. +- **Prompt:** + - The base-branch restore: `.claude/`, `.mcp.json` and the root `CLAUDE.md` read as the base's, + and the PR's copies are under `.claude-pr/`. A symlinked `CLAUDE.md` resolves through the PR's + `AGENTS.md`, and a nested `CLAUDE.md` reads at the PR head. The review states which rule + version it applied. + - Report only commands actually run. + - Read CI with `gh pr checks`. + - A comment above `claude_args` says a gate in the prompt needs a matching `--allowedTools` entry. + - N is sized against `--max-turns`: N = 40 against 60, re-measured on the pinned release. + - The action-bump PR re-checks effort, the turn cap and N. +- **Shell safety (#70).** Here-strings for every label check; the label read captured before it is + tested; `defaults: run: shell: bash`; `runs-on: ubuntu-24.04` with its reason. The kit sub-block + carries #70's three assertions. +- **`tooling.md`.** § Automated review says a workflow "cannot review any PR that changes it". It + also says a harness fact the bot asserts is checked against the operator's Claude Code version. + +### V5 · Recalibration and the budget — #69 (rule items), #65, #74 §3, #68 §3, #58 R9 (D49, D50, D59) + +- **`orchestration.md`, operative clauses only:** + - effort defaults per model and surface; thinking per model; + - `high` called a pin pending a sweep; + - the ladder from Opus 5.5; + - price steps 2× / 2× / 2.5×; + - Haiku 4.5's re-check trigger; + - caps recorded: `CLAUDE_CODE_WORKFLOW_MAX_CONCURRENT_AGENTS`, the size guideline, the ultracode + exemptions; + - the named-subagent-becomes-teammate trap; + - a text-only end of turn is a report; + - #74 §3: the Agent tool takes no effort, so a floor's effort is recorded, not pinned; + - #72 §4: read-only agents. +- **`orchestration-reference.md`:** every quotation, re-fetched raw and dated on the day it is + written; the price and cache-read table; the changelog rows; the dated alias facts. +- **Subagent-driven execution.** `workflows.md` § Subagent dispatch records why `/finish` is inline, + when subagent-driven pays, and the rules for anyone who delegates implementation. The evidence + cites `context-builder-kit#69` and Anthropic's "When delegation doesn't pay". The triad's + task-tracking leg becomes "a tracked checklist". +- **`simplification.md`** records `/simplify`'s changelog history in place of a local spot check. +- **`pr-review.md` § The floor** adds a workflow agent to the reasons an Agent tool is absent (after + probe P2). +- **#68 §3.** The grounding rules gain rule 4: check which binary a tool name resolves to from inside + the code's own execution context, with `command -v` or `type -a`, never `type -P`. The "three + rules" count is swept in `failure-modes.md` and framing's `research-phase.md`. +- **#65.** The three hygiene items, the heading rename, and the unfilled-posture check. +- **R9.** A conditional note on the 3.2× pooled cost ratio. +- **D59.** `knowledge-backend.md` splits into contract and reference. The block's always-loaded + inventory and § Rule loading name the new pair. Scaffold's `none`-axis disposition, which deletes + the rule, its hook and its settings stanza, now deletes both halves. + +### V6 · Harness code — #69 (harness items and comments) (D45) + +- **`finish-ab.js`:** + - two to four arms, per-arm model and effort; + - a Latin-square panel; + - structured, order-independent judge measures; + - `executed` mode, `base`/`brief`/`rubric` args; + - `runner_check` logs named in the judge prompt; + - blindness notes, and the read-only clause for judges. +- **`run-arms-headless.py`:** + - worktree per arm, auto mode; + - `--strict-mcp-config` with the allowlist constant; + - the deny list, with the bare `git push` rule dropped because `git push *` covers it; + - continuations that keep `--append-system-prompt`; + - `--resume-existing` that survives an empty payload; + - the continuation cap stated per pass; + - `check_command` run once per arm, serially, logs beside `out_dir`; + - `--dry-run`; closed files; + - the optional `worktree_setup` list. +- **Rubric and brief** state no arm count. The brief gains "The issue, verbatim". +- **`agent-cost.py`:** + - one version-keyed `PRICE` and one `CACHE_READ` (Fable 5.1 and Mythos 5.1 at 0.025×, Opus 5.5 + at 0.05×, default 0.1×), fixing the legacy Fable 5 misprice every target carries; + - `tier()` matches a key whole; + - `` messages and fast mode priced correctly. + + The block's price diff is re-keyed by version, and a `CACHE_READ` diff is added against the quoted + sentence. +- **Fixtures:** + - `agent-cost-fixture.sh` as a union of harvest 4's cases and #69's, run with `python3 -B`, with + both mutants failing; + - `finish-ab-shape.mjs` with the new scenarios, including the `ARM_SCHEMA` drift test between the + two copies; + - `run-arms-headless-fixture.sh` with a fake `claude`, failing on a `ResourceWarning`. +- **`.gitignore`** covers the runner's worktree root. + +### V7 · Review conventions — #72, #74 §1–2, #58 B R10, #64 (D46) + +- **`review-sweep.js`:** + - prompt-carrying finders; a finder with neither prompt nor agent is dropped coverage, named on + the gate line; + - file-and-line dedup carrying every title; + - the logged roster throw; + - `READ_ONLY` in every find and verify prompt. + + `review-sweep-accounting.mjs` gains the scenarios, and its stub records each call's prompt. +- **`pr-review.md`:** + - the finder shape; + - invariant (3) reworded, including triage by the report the verifier named; + - invariant (2) covers an interrupted floor or sweep: it is re-run fresh, and memory it wrote is + discarded; + - § The floor › Once gains the one-verification-workflow rule. +- **Rough-in's `research-phase.md`** gains the re-verify-a-consolidation-at-the-gate rule, with a + contract pointer and a test criterion. +- **`pr-review-reference.md` § Anti-patterns** gains "Folding one review layer into another" (B). +- **R10.** The roster guidance: enumerate a family of directories, because a family has no prefix + form. +- **#64.** `pr-respond.md`'s bullet agrees with Step 7. +- **Unverified findings in this class**, re-checked at execution: the "four-class" rubric with five + rows; the break-glass `--skip-review` flag the executor does not parse; the reference half's + docs-only anti-pattern that contradicts the floor. + +### V8 · Target hygiene — #70 §4–6, #71, #58 R7 (D52, D58) + +- **Dependabot.** A Dockerfile `FROM` and a compose `image:` are covered by the `docker` and + `docker-compose` ecosystems. The three gaps are named, with the `COPY --from` remediation: route + the image through a named stage. This is on all three surfaces that said "not covered by any bot". + The Dependabot examples gain the stubs. +- **mise.** The tooling template's mise section carries + `[task_config] shell = "bash -O inherit_errexit -c -o errexit -o pipefail"` with the version floor. + A pin checks it. +- **Kit-owned code** stays out of a target's formatter and linter scope, forced for explicit paths. + This is tool-neutral, with ruff's `--force-exclude` as the example. `format-on-edit.sh`'s arm + comment says so, and its skip floor names `.claude/workflows/`. The bootstrap checklist gets a + one-time row. +- **`.gitignore` harness block** in `github-starter-templates.md`. It is pinned by + `git check-ignore` in a throwaway repo, and the kit's own `.gitignore` carries it. The unanchored + `reference/` line in the kit's `.gitignore` is removed or anchored; that one is unverified, + re-checked at execution. § `.gitignore` anchoring names worktrees and kit bytecode as harness + transients. +- **D58.** `.mcp.json.example` is rebuilt. The pins are dated at least 7 days old on the release day. + The settle-window section says an MCP server is a dependency. README, `tooling.md` and the + example agree on how secrets are handled. + +### V9 · Consistency, citations and small rot — #63, #66, #68 §1, #58 R2 R3, review findings (D53, D54, D62) + +- **Citations.** The four executor citations of `CONTRIBUTING.md` § Branches, and the `STANDARDS.md` + section citations, point at the conventions. The eight citations of the retired § Deferred + meta-issues become § Pre-flight checks, and the block's check widens to any citation. The + `cascade-rule-reviewer`'s "commit format" pointer is fixed. Bare kit-issue citations become + `context-builder-kit#N` or the fact they stand for, and a kit-tree check keeps them out. The + sibling name in `cbk-conventions-reference.md` is removed. +- **#66.** The three restatements become pointers. +- **D54.** The branch rule and the Quick reference row. +- **R2.** `/finish` Step 1 admits a meta's child title as a fifth form, in all four executor files. +- **R3.** `adr-new` reads the index's rows, including both forms a row can take, and pins no + separator. +- **Verified review findings:** + - "six" or "seven" issue sections become eight everywhere (scaffold, the four backends copies, + `plan-mode-prompts.md`, test cases); + - one AC-id form across the contract, both templates and the conventions; + - blueprint's markdown-only acknowledgment rekeyed on `Planning backend = in-repo-markdown`; + - the unshipped `methodology_register.md` and `sdd.md` citations repointed at the shipped excerpt; + - the four `backends.md` copies dated and diffed by the block; + - the CI-skip trap sourced, with all five marker spellings; + - the Linear free-tier claim corrected; + - Notion's plan-gated Verification property named. +- **Unverified findings in this class**, re-checked at execution: the gate and state counts in + scaffold and blueprint; the rough-in 3–7 vs 2–6 count; the phase-exit claim; pointer headings in + the reference halves; the label taxonomy's three missing labels; the in-repo-markdown issue-template + claim; § Hook authoring's hook tally. +- **D62.** + +### V10 · The release — D41, D60, R14, the release-lens findings + +- **`README.md`:** + - counts and inventory from the tree; + - the Quick start per D60 (a tagged tarball, the drop-in set, the install tag recorded); + - declared prerequisites with what each absence costs: jq, git, gh, bash, node and python3 for + fixtures, and a Claude Code floor; + - "how to start a phase", since four phase skills are invoke-only; + - the "Exercised vs designed" table restamped, now that github-issues has been exercised; + - the advisory-stanza line corrected; + - the finish.md customisation paragraph rewritten. +- **`CHANGELOG.md`:** v0.1.0–v1.0.0, each with what landed and **Sync notes**. v1.0.0's notes list + the files a target has filled and must hand-merge: both review workflows, `.mcp.json`, the + orchestration pair, `tooling.md`, `settings.json`. They also say ahead-of-kit files merge against + the kit commit that lands them, not the install sha. +- **`cbk-conventions-reference.md` § Syncing the kit** speaks in versions, and gains three notes: + - after resolving `settings.json`, diff the `hooks` block's event keys against the pre-merge + copy; + - the Kit commit fallback for an older target; + - ahead-of-kit files merge against the landing commit. + + Scaffold's Kit commit row records `vX.Y.Z (sha)`. +- **This repository's `CLAUDE.md`** gains the R14 audit method, and a correction: the kit ships + hooks, JS and Python with fixtures, so "no code to verify" no longer holds. +- **`LICENSE`** gains a copyright line. +- **After merge, with the operator's confirmation:** annotated tags, then `gh release create v1.0.0` + from the CHANGELOG section. + +## Probes at execution — facts this spec does not assume + +| # | Probe | Why | Decides | +|---|---|---|---| +| P1 | A hook payload's `cwd` after the Bash tool runs `cd`, on the release CLI | The hooks page now says `cwd` "follows Claude … after Claude runs `cd`". Harvest 3 measured the opposite, and `require-repo-root-for-agents.sh` rests on that measurement. | The guard's header and trigger description, and whether the guard now fires on a `cd` | +| P2 | Whether a workflow agent has an Agent tool (plain and worktree-isolated) | A two-target probe, not documentation | The `pr-review.md` § The floor sentence and its date | +| P3 | Whether the record-model step's job-summary line lands | Unexercised in every target | Stays "designed-unexercised", dated, if it cannot run | +| P4 | Unprivileged user namespaces on the CI image | The bind-mount cases need them | The cases print SKIP; the dated observation records which | +| P5 | The current hosted MCP endpoints (Linear, Notion, context7) and the pinned `time` server | D58 | `.mcp.json.example` | +| P6 | Every quotation, raw, with `grep -F`, typographic apostrophes normalised | Constraint 7 | Each quotation's date | + +## Sequencing — one PR (D42) + +Branch `feat/harvest-5-v1.0.0`. The spec and the trace are its first commit. Clusters land in order +V1 → V10. Each commit is Conventional Commits and names the trace ids it closes. + +Every commit must be: +- green on the live block, extracted from the reference file on every run; +- green on every fixture; +- clean under `bash -n` for every hook. + +The review floor runs **once**, when the known work is complete: `/simplify`, then +`pr-review-toolkit:review-pr`, with `review-sweep` beside it. That review sweep uses the D46 +rewrite, which dogfoods it. Triage follows the four-class rubric, and the PR body carries +`## Review gate` and `## Triage`. + +Before the trace audit, the PR body records: +- the always-loaded total before and after; +- the trace summary: landed, non-goal, and not holding at execution. + +Tags and the GitHub Release follow the merge, after the operator confirms. + +## Non-goals — explicitly NOT harvested + +- The collect-then-exit block. D31's fail-fast stands, and a kit-side red still hides a target's + project checks. Recorded here, not in kit text. +- A file-count-scaled `--max-turns`, or an auto-selected deep-review path. D37 stands. +- you-are-hear's `/finish`-at-`medium` choice as a default. It is one project's data point. +- Mirroring Claude Code's full runtime ignore list, which is version-specific and grows. +- A frozen-corpus CI script or corpus hook exemplar (D55). The recipe and the resolver's corpus mode + are the kit's part. +- The subagent-driven arm protocol file, and a saved post-floor verification workflow. +- A public history rewrite (D62). +- Any target's own fills: globs, roster prefixes, plugin lists, reviewer bodies, recorded comment + data. +- Any reference to the private target in shipped kit content. + +## Verification — definition of done + +1. The block is green on the kit tree under literal `bash -e` after every commit, all sentinels + printed; `verify.yml` is green on the PR. +2. Every fixture is green and run by the block: + - `hook-contract`, `hook-guards`, `hook-payloads`, `protected-paths`; + - `adr-ci-body`, `review-assert`, `review-trigger`; + - `run-verification-block`, `run-arms-headless`, `agent-cost`; + - `finish-ab-shape`, `load-workflow-shape`, `review-sweep-accounting`. + + Each new fixture was shown red against the unfixed file before it went green. The plan records + how, since D42 lands a fixture with its fix. +3. `settings.json` parses. Every hook registration is exec form. Every `enabledPlugins` key is + `name@marketplace`. No top-level hook-shaped object. +4. No `$1`-style placeholder remains in a command. The four commands and `finish-procedure` carry + `disable-model-invocation: true`. The executor pair is byte-parallel with its bundled copies. + `/finish` Step 1 names five title forms. +5. The always-loaded total is at or below 130,628 bytes, or the PR body records the overage and why. +6. Sanitization: the private-target denylist grep is clean across the whole tree, including + `docs/`. No sibling name appears in `.claude/`, `.github/`, `README.md` or `CLAUDE.md`. No bare + kit-issue citation remains. +7. Trace audit: every row is landed, a named non-goal, or recorded as not holding at execution. A + refute-by-default verifier checks each row not marked landed. #58 and #60–#74 close by marker. +8. The floor ran once and is recorded in `## Review gate`, with the sweep's line. +9. `CHANGELOG.md` covers v0.1.0–v1.0.0. The D60 Quick start is tested by extracting a local tag + archive into a scratch repository, checking that only the drop-in set lands. After merge: + - annotated tags pushed; + - `gh release create v1.0.0` from the CHANGELOG section; + - the README's install command resolved against the real tag. diff --git a/docs/superpowers/specs/2026-09-30-harvest-5-trace.md b/docs/superpowers/specs/2026-09-30-harvest-5-trace.md new file mode 100644 index 0000000..73d5ef9 --- /dev/null +++ b/docs/superpowers/specs/2026-09-30-harvest-5-trace.md @@ -0,0 +1,394 @@ +# Harvest 5 trace — every ask, by issue and comment id + +Companion to `2026-09-30-cascade-kit-harvest-5-design.md` (constraint 6, R14). Generated 2026-09-30 from the pre-spec pass `wf_d6e0ae85-8be`; ids are `#/>/`. **Live** is the state at `74edf84` (`yes` · `partial` · `no` · `n/a`). **Status** starts `open` and is set at the pre-merge audit to `landed `, `non-goal`, or `not holding` (D56). Duplicate asks across clusters carry one owner; the plan names it. + +## Issue items + +| Id | Cl. | Ask | Live | D | Status | +|---|---|---|---|---|---| +| `#60/body/fix` | V2 | Fix protect-immutable-adrs.sh so a payload path outside CLAUDE_PROJECT_DIR is still denied (suffix match, or root from the file's own checkout) and add a test whose env dir and file dir disagree. | yes | D43 | open | +| `#60/body/alt` | V2 | Alternative fix: derive the root from the file's own checkout (`git -C "$(dirname "$file_path")" rev-parse --show-toplevel`), as analyze-on-edit.sh does. | n/a | D43 | open | +| `#60/body/fixture` | V2 | Add a fixture case for a payload whose path lies outside CLAUDE_PROJECT_DIR (hook-contract-fixture.sh never drives one). | yes | D44 | open | +| `#60/c5876324022/table` | V2 | The ten-spelling table (plus control) as the acceptance set the ADR guard must exit 2 on, with an exact payload read, and 'a guard should not depend on what Claude Code normalizes'. | yes | D44 | open | +| `#60/c5876324022/closed-1` | V2 | Read file_path and cwd exactly (`jq -j ...; printf x`) and refuse a payload jq cannot parse instead of failing open. | yes | D43 | open | +| `#60/c5876324022/closed-2` | V2 | Resolve the path two ways (lexically collapsing `..`, physically through symlinks one component at a time) and deny if either lands in the guarded tree. | yes | D43 | open | +| `#60/c5876324022/closed-3` | V2 | Refuse any path through /proc (magic links resolve per process). | yes | D43 | open | +| `#60/c5876324022/closed-4` | V2 | Compare device and inode against every guarded directory and file (hardlink, bind mount); echosphere used `stat -L`. | yes | D43 | open | +| `#60/c5876324022/closed-5` | V2 | Guard two roots: the checkout the hook lives in (derived lexically from BASH_SOURCE so a symlinked .claude/hooks cannot move it) and CLAUDE_PROJECT_DIR. | yes | D43 | open | +| `#60/c5876324022/closed-6` | V2 | A fixture of 32 cases whose allow side covers a same-prefix sibling, another checkout's copy and a symlink loop. | yes | D44 | open | +| `#60/c5876324022/closed-note` | V2 | For an ADR guard the existence test must run on the RESOLVED path (creating a new ADR stays allowed); name the residuals the hook cannot close (Bash-tool writes, case-insensitive filesystems, hand edits, a symlink swapped between… | partial | D43 | open | +| `#60/c5876324022/corpus-s1` | V2 | Suggestion 1: the verification block also checks that every CI job the mutation table or the registry names exists under .github/workflows/ (or as a gate task). | yes | D51 | open | +| `#60/c5876324022/corpus-s2` | V2 | Suggestion 2: the bootstrap checklist carries one row per frozen-corpus enforcement item, so one cannot be ticked off with the others. | yes | D55 | open | +| `#60/c5876324022/adr-job` | V2 | The ADR-immutability job mis-reads paths git quotes (non-ASCII) and awk splits (spaced): swap the body for a parse-nothing form (`:(glob)` pathspec, `--diff-filter=a`, fail on any output). | yes | D55 | open | +| `#60/c5876324022/echosphere-z` | V2 | echosphere's frozen-corpus leg read `git ls-tree` without -z, so a non-ASCII corpus name read as deleted (fails closed); fixed with -z through xargs -0. | n/a | D55 | open | +| `#60/c5881158391/status` | V2 | Status: echosphere main still has the bug; the private target carries the suffix match; the suffix match is a partial fix (3 of 10). | n/a | D41 | open | +| `#60/c5881158391/port-helper` | V2 | Port the hardened resolution recipe as a shared helper under .claude/hooks/lib/, sourced by the ADR hook (existence test on the resolved path) and by a corpus guard. | yes | D43 | open | +| `#60/c5881158391/port-glob-body` | V2 | Port the `:(glob)` ADR body and keep #61's guarantee (under `set -euo pipefail`, `changes=$(git diff ...)` stops on a failing git diff). | yes | D55 | open | +| `#60/c5881158391/port-s1` | V2 | Port suggestion 1: a verification-block check that every CI job or task the mutation table names exists. | yes | D51 | open | +| `#60/c5892401033/helper` | V2 | The hardened resolver as .claude/hooks/lib/resolve-path.sh, sourced by the ADR hook (and a corpus guard). | yes | D43 | open | +| `#60/c5892401033/adr-path-independent` | V2 | The ADR deny stays path-independent (any checkout), because every kit target keeps ADRs at docs/adr/ and a session in one target edits its siblings; a corpus guard is root-scoped instead. | partial | D43 | open | +| `#60/c5892401033/linked-worktrees` | V2 | Linked worktrees are roots too: a worktree session driven by the main checkout's hook would otherwise pass the two-root model. | n/a | D43 | open | +| `#60/c5892401033/ef-inode` | V2 | Use bash's `-ef` over a glob walk (echosphere's final form) rather than `stat -L`, so no missing tool switches the check off. | yes | D43 | open | +| `#60/c5892401033/gitignore-trap` | V2 | A target whose .gitignore carries the toptal Python template's unanchored `lib/` hides .claude/hooks/lib/ and `git add` silently skips the helper; it needs `!/.claude/hooks/lib/`. | yes | D43 | open | +| `#60/c5892401033/adr-job-glob-fixture` | V2 | The `:(glob)` body is live; adr-ci-body-fixture.sh extracts the job's own run: body from the YAML and runs it on a throwaway repo (9 cases + bad SHA, red first on the non-ASCII name). | yes | D44 | open | +| `#60/c5892401033/suggestion-1-built` | V2 | Suggestion 1, built: the block fails when the mutation table or the registry names a workflow, script or `mise run` task that does not exist; the checker is asked about bogus names first. | yes | D51 | open | +| `#60/c5892401033/corpus-leg` | V2 | The frozen-corpus CI leg for a corpus frozen whole: scripts/check-frozen-corpus.sh (fails on additions too), its 29-case fixture, in a required job with fetch-depth 0. | n/a | D55 | open | +| `#60/c5892401033/leg-defects` | V2 | Two defects in the sibling's leg: a dead `\|\| fail` after `link=$(readlink ...; printf x)`, and a pass-open where a file replaced by a symlink to an identical copy hid behind assume-unchanged; fix = compare type on disk before c… | n/a | D55 | open | +| `#60/c5892401033/fixture` | V2 | protected-paths-hook-fixture.sh, 57 cases at the sibling's PR head, including each guard's no-jq fail-open branch. | yes | D44 | open | +| `#60/c5901494943/1` | V2 | Consultation's frozen-corpus enforcement set lists the CI job's red-team closures (nine bullets: fully qualified base refs, default-branch merge base, cleared GIT_* environment and ignored replace refs, nothing sourced from the t… | yes | D55 | open | +| `#60/c5901494943/2a` | V2 | Sourced-helper contract: a hook that sources a helper keeps it in .claude/hooks/lib/, sources it only after the stdin drain, and names it on its Depends: line. | yes | D43 | open | +| `#60/c5901494943/2b` | V2 | A hook whose helper is missing fails open (exit 0) naming the surviving backstop. | yes | D43 | open | +| `#60/c5901494943/2c` | V2 | The helper defines functions only and never reads stdin. | n/a | D43 | open | +| `#60/c5901494943/2d` | V2 | hook-contract-fixture.sh's early-reader tripwire reads lib/*.sh too (a pipeline that can exit early planted in a helper was invisible). | yes | D43 | open | +| `#60/c5901494943/2e` | V2 | The unanchored `lib/` in the Python template's .gitignore needs `!/.claude/hooks/lib/` (restated here as part of the contract). | yes | D43 | open | +| `#60/c5901494943/3` | V2 | The always-run ADR job can check out blobless (`fetch-depth: 0` + `filter: blob:none`), since --no-renames name-status compares tree entries. | yes | D55 | open | +| `#61/body/fix` | V2 | adr-immutability-check.yml: a failing `git diff` inside `mapfile ... < <(...)` reads as 'no ADR changed'; capture with command substitution so `set -e` sees it. | yes | D55 | open | +| `#61/body/grep` | V2 | Grep the kit for other workflows that mapfile a process substitution under set -e. | no | D51 | open | +| `#62/body/lock-files` | V2 | protect-lock-files.sh:39 jq-missing warning names no surviving backstop; add a bracketed slot for the target's CI lockfile check. | yes | none (fills #62) | open | +| `#62/body/pr-state` | V2 | guard-pr-state.sh:25 jq-missing warning names no backstop; the honest wording is 'none mechanical, every gh pr ready/merge/close/reopen needs the operator's per-action OK'. | partial | none (fills #62) | open | +| `#62/body/main-branch` | V2 | protect-main-branch.sh:67 non-repo-cwd warning names no backstop; the base branch's ruleset (PRs only) is the backstop where one exists. | yes | none (fills #62) | open | +| `#62/body/extra-adr-hook` | V2 | (Found while auditing all fail-open exits; not listed in #62) protect-immutable-adrs.sh's jq-missing warning also names no backstop. | yes | none | open | +| `#58/c5901493591/R1` | V2 | Durable behavioural coverage of every hook's branches: hook-guards-fixture.sh (main-branch, PR-state, lock-file, knowledge-backend), hook-payloads-fixture.sh (launch-root guard, fork detector), protected-paths-hook-fixture.sh; §… | yes | D44 | open | +| `#58/c5901493591/R6` | V2 | The detector's partial-scan warning has no fixture (hook-contract-fixture probes only the degrade path). | yes | D44 | open | +| `#58/c5901493591/R8` | V2 | The *.lock fallback's remediation cites a 'hook's exemption comment' that does not exist; say to give the file a case arm above the fallback. | yes | D44 (asserted by the guards f… | open | +| `#58/c5901493591/R11` | V2 | Wiring an advisory hook is three edits, not one: the stanza into hooks.PostToolUse, the name into the project sub-block's ADVISORY_WIRED, and the name into cbk-conventions.md § Mutation discipline's two-views paragraph; say so in… | yes | D51-adjacent (no D) | open | +| `#58/c5901493591/R13` | V2 | The detector's rule 1 says 'Never prune a path that could BE or CONTAIN the tree', which its own Residual line contradicts; state what it guarantees (never prune a directory named one of the three protected names) and point at th… | yes | D53 (citation form) | open | +| `#73/body/1` | V1 | run-verification-block.sh accepts a filled target (docs/cbk/scaffold.md exists) whose project sub-block never ran, because it requires only `verification: done`. Add a third rail: in a filled target also require `verification: pr… | yes | D51 | open | +| `#73/body/2` | V1 | The block's Stop-hook check fails only on a non-zero exit, but detect-forked-agent-memory.sh fails OPEN (exit 0 + WARNING) when it could not look (no checkout, unenterable root, partial scan, unmonitored walk); in a gate that mus… | yes | D51 | open | +| `#58/c5901493591/R4` | V1 | Nothing in the templates wires the verification block into `check`. blueprint templates/tooling.md: the minimum task set gains a verification task `check` depends on (body: the runner), with a rule that it is a leg of check; scaf… | yes | D51 | open | +| `#58/c5901493591/R5` | V1 | Ship the promised one-line note in § Verification's preamble naming the bracket idiom's cost: an `absent` check brackets one letter so the grep never matches its own line; a target that spellchecks .claude/ reads each as a typo.… | yes | D51 | open | +| `#67/body/1a` | V4 | claude.yml: replace the model-ID fill-ins with family aliases (`--model opus`, `--fallback-model sonnet`). | yes | D47 | open | +| `#67/body/1b` | V4 | claude-review.yml three label branches name aliases: `opus` by default, `sonnet` under claude-fast-review, `opus` at xhigh under claude-deep-review (never `best`); --fallback-model `sonnet`. | yes | D47 | open | +| `#67/body/1c` | V4 | Template comment: what a project choosing `fable` must know (before Claude Code 2.1.257 `fable` means Fable 5 not 5.1; through the Agent SDK Claude Code bills Fable to usage credits without a consent prompt). | yes | D47/D49 | open | +| `#67/body/1d` | V4 | Template comments: the model version now comes from the action pin, so the SHA bump is the model bump; do not set ANTHROPIC_MODEL on the step. | yes | D47 | open | +| `#67/body/2a` | V4 | claude-review.yml: give the action step `id: review` and add a 'Record the resolved model' step that writes started/used models to the job summary. | yes | D47 | open | +| `#67/body/2b` | V4 | claude.yml gets its own copy of the record step (REQUESTED: opus, no EFFORT) and an id on the action step. | yes | D47 | open | +| `#67/body/2c` | V4 | The review prompt's 'Model in use:' line reads the alias, so it must say so honestly and point at the recorded model. | yes | D47 | open | +| `#67/body/3` | V4 | orchestration.md § Generation notes: the review bot is the one dispatch that names a family alias; the action-bump PR that moves the resolved model re-checks effort; templates must keep passing --effort. | yes | D47/D49/D50 | open | +| `#67/c5804984992/1` | V4 | claude.yml passes no --effort; name it (`--effort high`) with the header saying why (default is per model; Opus 5.5 defaults to medium). | yes | D47 | open | +| `#67/c5877251222/1` | V4 | Assert step: `gh api --paginate --jq '... \| length'` prints one number per page, so a posted review on a >30-comment PR reads as missing; slurp pages and count once. | yes | none | open | +| `#67/c5877251222/2` | V4 | With track_progress the tracker comment alone satisfies the assertion; match the summary's own verdict marker on updated_at instead of any review-app comment on created_at. | yes | none | open | +| `#67/c5877251222/3` | V4 | `if: always()` plus `cancel-in-progress: true` reports a superseded run as a failure; use `!cancelled()`. | yes | none | open | +| `#67/c5877251222/4` | V4 | The notice should say why (no session started vs session ran without a verdict); the empty execution_file means no model session. | yes | none | open | +| `#67/c5881158070/1 (corrected by c5892401572/1)` | V4 | Key the no-session notice on the workflow-validation skip. The first comment said the action has a `skipped_due_to_workflow_validation_mismatch` output; the later comment corrects it: not a declared output. | yes | none | open | +| `#67/c5881158070/2a` | V4 | Fast branch: from the action release installing Claude Code 2.1.284 (`sonnet` = Sonnet 5.5, default medium) the fast branch's explicit --effort becomes load-bearing. | partial | D47/D49 | open | +| `#67/c5881158070/2b + #67/c5892401572/Effort` | V4 | Deep branch: `--effort xhigh` is load-bearing from Claude Code 2.1.284 (ultracode no longer implies xhigh); the model-config page agrees the toggle 'leaves the effort level unchanged'. | partial | D49 | open | +| `#67/c5881158070/2c` | V4 | Record which action release moves which alias (v1.0.232 = first `opus` -> Opus 5.5; v1.0.236 = `sonnet` -> Sonnet 5.5) and the cooldown timing. | n/a | D41/D49 | open | +| `#67/c5892401572/2` | V4 | Fixture for the assert step: extract the step's run body from the YAML and run it with a fake gh on recorded and synthetic comment pages (11 cases + 3 structural). | yes | none (open) | open | +| `#67/c5892401572/3` | V4 | Measured fact: with track_progress the action ends its tracker comment with the session's final text, so the tracker can carry the whole review; the verdict regex counting it as landed is right. | yes | none | open | +| `#67/c5901495773/1a` | V4 | Trigger: auto-review one-way-door markdown - replace `paths-ignore: '**/*.md'` with `paths:` (`**`, negations, then re-includes `.claude/**` and `docs/adr/**` last); prompt 'What to skip' says markdown under .claude/ or docs/adr/… | yes | D48 | open | +| `#67/c5901495773/1b` | V4 | Pin the trigger's order with a fixture evaluated the way GitHub does, run in the verification block's kit sub-block (12 PR shapes; refuses to pass with no workflow). | yes | D48 | open | +| `#67/c5901495773/1c` | V4 | blueprint tooling.md's review-bot paragraph names the `paths` shape. | yes | D48 | open | +| `#67/c5901495773/2` | V4 | The review reports only commands it actually ran. | yes | none | open | +| `#67/c5901495773/3` | V4 | A gate added to the prompt needs a matching --allowedTools entry; a YAML comment above claude_args says the two lists are edited together, one command per gate. | yes | none | open | +| `#58/c5901493591/R12 = #67/c5901495773/4` | V4 | Degrade clause's `[N]` has no way to size it against --max-turns 60: turns are spent per tool call; at one Read a file keep about a third for gh/inline/summary, N = 40 against 60, both move together; measured on Claude Code 2.1.2… | yes | D53 | open | +| `#67/c5901495773/5` | V4 | The action-bump PR re-checks --max-turns and the degrade N, not only effort. | yes | D50 | open | +| `#67/c5901495773/6` | V4 | tooling.md § Automated review: a harness fact the bot asserts is checked against the operator's Claude Code version (the bot's Claude Code comes from the pinned action). | yes | D50 | open | +| `#67/c5901495773/7` | V4 | The summary reads CI (`gh pr checks`) and no longer says CI is unreadable, since the template grants checks: read. | yes | none | open | +| `#67/c5901495773/8` | V4 | Here-strings, `shell: bash`, named runner image for the templates - pointer to #70. | yes | D52 | open | +| `#67/c5901495773/9` | V4 | tooling.md says 'changes', not 'introduces': the workflow cannot review any PR that changes it. | yes | none | open | +| `#67/c5901495773/confirm-1` | V4 | Confirmation: `init.model` names the resolved model (claude-opus-5), not the alias; the job-summary half is unverified - the record step reads the resolved id. | partial | D47 | open | +| `#67/c5901495773/confirm-2` | V4 | Confirmation: the assert step's negative path was exercised live (validation skip -> red with notice); restamp the template's 'Designed-unexercised as of 2026-09-06' line. | yes | none | open | +| `#68/body/2a (+c5861166655/2, c5881158244/1, c5892402074/2)` | V4 | Template prompt tells the reviewer which paths are the base branch's on a PR and where the PR's versions are (`.claude-pr/`); the restore predates the issue's cited release (present at v1.0.183) so every target pinned at or above… | yes | none | open | +| `#68/body/2b` | V4 | Where a target adds gates to the prompt and allowlist, the template's comment says to keep any `.claude/`-hosted fixture out of them, or to state that it runs the base copy. | yes | none | open | +| `#68/body/2c` | V4 | On a PR touching `.claude/`, the review names CI's `check` as the gate. | yes | D51 | open | +| `#68/body/2d` | V4 | claude.yml (no prompt of its own) records the same restore facts in its header, for whoever adds a prompt. | yes | none | open | +| `#68/c5881158244/2 (+c5892402074/2)` | V4 | Where CLAUDE.md is a symlink to AGENTS.md the 'CLAUDE.md is the base branch's copy' line is wrong: AGENTS.md is not restored, so a PR can change the reviewer's instructions through it. | yes | none | open | +| `#68/c5901496131` | V4 | A nested CLAUDE.md (anywhere but under .claude/) is not on the restore list and reads at the PR head; state it in the prompt and the claude.yml header. | yes | none | open | +| `#70/body/1a` | V4 | claude-review.yml deep/fast label checks: `printf '%s' "$labels" \| grep -qF` reads a match as no match under pipefail (SIGPIPE 141); use here-strings. | yes | D52 | open | +| `#70/body/1b` | V4 | claude.yml skip-claude check uses the same pipe; use a here-string. | yes | D52 | open | +| `#70/body/1c` | V4 | claude-review.yml label-removal step: `gh pr view ... \| grep -qxF` reads a failed gh as 'Label already removed (benign)'; capture the label read first, error if it fails. | yes | D52 | open | +| `#70/body/2` | V4 | An unset `shell:` runs `bash -e {0}` with no pipefail; add `defaults: run: shell: bash` to both review templates, the CI skeleton in tooling.md and scaffold's starter CI stub. | yes | D52 | open | +| `#70/body/3` | V4 | `runs-on: ubuntu-latest` moves when GitHub re-points the label; use `ubuntu-24.04` with the reason in the two review templates, the CI skeleton and the starter stub. | yes | D52 | open | +| `#69/body/T1` | V5 | Upstream row 1: Opus 5.5 released 2026-09-22; `opus` resolves to it from Claude Code 2.1.280; Opus 5 legacy. | yes | D49 | open | +| `#69/body/T2` | V5 | Upstream row 2: the effort-parameter default is per model; omitting effort equals the model default (medium on Opus 5.5, high on other models). | yes | D49 | open | +| `#69/body/T3` | V5 | Upstream row 3: on Opus 5.5 adaptive thinking is always on, so effort is the primary spend control. | yes | D49 | open | +| `#69/body/T4` | V5 | Upstream row 4: Opus 5.5 at medium matches or exceeds Opus 5 at high; thinks more per turn at a level; reserve xhigh/max for measured gains. | yes | D49 | open | +| `#69/body/T5` | V5 | Upstream row 5: the Opus 5 guide's patterns 'remain a reasonable starting point' for Opus 5.5, so over-verification and subagent-spawning guidance carries over. | partial | D49 | open | +| `#69/body/T6` | V5 | Upstream row 6: treat a text-only end of turn as a report, keep a checklist the model updates. | yes | D49 | open | +| `#69/body/T7` | V5 | Upstream row 7: the ladder starts at Opus 5.5; Opus 5.5 at default matched Fable 5.1 for about a fifth of the cost per solved task; a Fable advisor added 1.7 points for about 2.1x the money. | yes | D49 | open | +| `#69/body/T8` | V5 | Upstream row 8: prices per MTok (Fable 5.1 10/50, Opus 5.5 4/20, Opus 5 5/25, Sonnet 5 2/10, Haiku 4.5 1/5); cache reads 10% of input, 2.5% on Fable 5.1 and Mythos 5.1, 5% on Opus 5.5; 5-minute writes 1.25x. | yes | D49, D51 | open | +| `#69/body/T9` | V5 | Upstream row 9: in Claude Code a user-settings `effortLevel` does not count for Opus 5.5; project/local/managed apply to every model; frontmatter effort overrides the session but not the env var. | yes | D49 | open | +| `#69/body/T10` | V5 | Upstream row 10: task-tracking tools are off by default on Opus 4.8+, Sonnet 5 and Fable (2.1.233), restated as an allowlist at 2.1.268; CLAUDE_CODE_ENABLE_TODO_TOOLS=1 re-enables. | yes | D49 | open | +| `#69/body/T11` | V5 | Upstream row 11: workflow size guideline small<5, medium<10 (was <15 before 2.1.271), large<50, small default on Pro; CLAUDE_CODE_WORKFLOW_MAX_CONCURRENT_AGENTS (1-256, 2.1.269); ultracode sessions skip the Large-workflow warning… | yes | D49 | open | +| `#69/body/T12` | V5 | Upstream row 12: three rails removed in kit on 2026-07-30 are real (2.1.212); 200-subagent cap removed 2.1.224; WebSearch cap and MCP auto-background still stand. | yes | D49 | open | +| `#69/body/T13` | V5 | Upstream row 13: with agent teams enabled a named subagent launches as a teammate unless a fork or `isolation` is passed on the call. | yes | D49 | open | +| `#69/body/T14` | V5 | Upstream row 14: subagent worktrees branch from the default branch unless `worktree.baseRef` is set to head. | yes | D49 | open | +| `#69/body/T15` | V5 | Upstream row 15: a workflow agent has no Agent tool (probe on 2.1.282); depth-limit text applies to Agent-tool subagents only. | partial | D49 | open | +| `#69/body/F1` | V5 | orchestration.md:54: 'The API default is high - omitting ... behave identically' is false; default is per model; name which settings bind Opus 5.5. | yes | D49 | open | +| `#69/body/F2` | V5 | orchestration.md:56: 'start at high' is Opus 5 guidance; call a kept high a pin needing a sweep. | yes | D49 | open | +| `#69/body/F3` | V5 | Thinking omission: add one line beside the effort axis that thinking is always on for Opus 5.5. | yes | D49 | open | +| `#69/body/F4` | V5 | orchestration-reference.md:25: ladder quote names Opus 5; add Opus 5.5 $4/$20 and per-model cache-read rates. | yes | D49, D53 | open | +| `#69/body/F5` | V5 | agent-cost.py: PRICE keyed by family bills Opus 5.5 at Opus 5's price and 0.1x cache reads; key by version, add Opus 5.5 0.05x, handle and fast mode. | yes | D51 | open | +| `#69/body/F6` | V5 | orchestration.md:107 'Over-verification on Opus 5': say the citation holds for 5.5; add a row for text-only end of turn. | yes | D49 | open | +| `#69/body/F7` | V5 | orchestration.md caps and dispatch: record CLAUDE_CODE_WORKFLOW_MAX_CONCURRENT_AGENTS, medium<10, ultracode exemptions, the three sourced rails, and the named-subagent teammate trap on the agent-team row. | yes | D49, D50 | open | +| `#69/body/F8` | V5 | workflows.md:14: triad's 'task-tracking (the harness's task list)' leans on tools current models are not offered; make the leg a tracked checklist. | yes | D49 | open | +| `#69/body/F9` | V5 | simplification.md:7 'bundled as of 2.1.263' is a local spot check; record the changelog history (added 2.1.63, renamed 2.1.147, restored 2.1.152, cleanup-only 2.1.154). | yes | D49, D41 | open | +| `#69/body/F10` | V5 | finish-ab: two to four arms, per-arm model/effort, Latin-square panel, executed input, run-arms-headless.py. RULE TEXT half only (the code is D45). | yes | D45 | open | +| `#69/body/Fq` | V5 | Port the quotation and attribution fixes found by re-fetching sources (you-are-hear e7b71d9, 3b18ad2, cdc28a7, 9ba5bb5, 9170e9e). | yes | D49 | open | +| `#69/body/S1` | V5 | Record the SDD evidence: superpowers' author says Native execution is about twice as fast and half as expensive (2026-09-21); Anthropic says a separate evaluator is worth the cost when the task sits beyond what the model does rel… | yes | D49 | open | +| `#69/body/S2` | V5 | Rules record why /finish is Native execution (implements inline, one whole-branch review), when subagent-driven pays (long plan on compacted context, per-task review gate, expensive main loop), and six rules for anyone who delega… | yes | D49, D50 | open | +| `#69/body/S3` | V5 | Record the dry fire and verdict: subagent-driven beat inline on no measure, cost 10% more, took nearly twice as long; not adopted; medium vs high is one project's data point. | yes | D49 | open | +| `#69/body/S4` | V5 | Harness finding 1: a workflow agent has no Agent tool, so arms that dispatch (and a /finish whose floor fans out) must run as headless claude -p sessions in their own worktrees. | partial | D45 | open | +| `#69/body/S5` | V5 | Harness finding 2: an unattended /finish on Opus 5.5 stops early (5 of 5); resume in the same session naming what is open, at most two or three times. | yes | D45 | open | +| `#69/body/S6` | V5 | Harness finding 3: a resumed claude -p run reports the conversation's whole total, so never sum totals across invocations. | yes | D45 | open | +| `#69/body/S7` | V5 | Harness finding 4: --max-budget-usd does not count spend restored from earlier runs; give each continuation only what is left. | yes | D45 | open | +| `#69/body/S8` | V5 | Harness finding 5: deny the GitHub MCP servers as well as git push and gh writes; write `git * push` too. | yes | D45 | open | +| `#69/body/S9` | V5 | Harness finding 6: a contamination diff against a reference the same model wrote over-flags; read the arms' transcripts for reads past the base. | yes | D45 | open | +| `#69/body/S10` | V5 | Harness finding 7: the rubric must quote the verdict rule's measure definitions verbatim (a stricter wording split a panel 3-3). | yes | D45 | open | +| `#69/body/S11` | V5 | Do not wrap superpowers' skill for this; borrow the mechanics its release notes justify (unpinned user plugin whose execution loop changed five times in three months). | yes | D49 | open | +| `#69/body/CAP` | V5 | Harvest candidate: /finish item 8 puts the milestone F-issue's close marker on the capstone PR, because GitHub never rolls closure up the sub-issue tree. | yes | D54 | open | +| `#69/c5859756889/3-C1` | V5 | Correction: task-tool scope is Opus 4.8+ and Sonnet 5+, not 'Opus 5.x, Sonnet 5 and Fable'. | n/a | D49 | open | +| `#69/c5859756889/3-C2` | V5 | Correction: 'Agent kept below the depth limit' is a paraphrase, not a quotation; the real sentence is 'At the depth limit, Claude Code withholds the `Agent` tool from every subagent except a fork'. | n/a | D49 | open | +| `#69/c5859756889/3-C3` | V5 | Missing from checklist: pr-review.md sec The floor names only the spawn-depth limit as why the Agent tool can be absent; add that a workflow agent has none at any depth. | yes | D49 | open | +| `#69/c5859756889/3-C4` | V5 | Belongs with #67: claude.yml still runs 'the workhorse model at its default effort' and passes no --effort. | yes | D47 | open | +| `#69/c5859756889/3-C5` | V5 | Label nit: the reference half's changelog row is labelled '2.1.263-2.1.282' but cites 2.1.63-2.1.233 entries. | n/a | D49 | open | +| `#69/c5881157875/1a` | V5 | Sonnet 5.5 shipped with 2.1.284: `sonnet` now means a recalibrated model (review-sweep finders, fast-review branch). | yes | D49, D47 | open | +| `#69/c5881157875/1b` | V5 | Effort defaults differ by surface (API vs Claude Code); name the surface; user-settings exclusion covers 'Opus 5.5 and models released after it'. | yes | D49 | open | +| `#69/c5881157875/1c` | V5 | Two Sonnet 5.5 behaviours bear on finders: it stops to check in at low/medium on long agentic tasks; on JSON answers it often skips thinking; remedy line 'Think the problem through before you answer.' | yes | D46, D49 | open | +| `#69/c5881157875/1d` | V5 | 'Effort is the primary control' is said of Opus 5.5 and Fable, not Sonnet 5.5 (which has between_tools); the thinking line must name its model. | yes | D49 | open | +| `#69/c5881157875/2a` | V5 | Price steps are now 2x / 2x / 2.5x (Haiku 4.5 -> Sonnet 5.5 -> Opus 5.5 -> Fable 5.1); the template's '2x / 2.5x / 2x' and Fable's '2x-Opus price' are stale. | partial | D49 | open | +| `#69/c5881157875/2b` | V5 | Sonnet 5's scheduled price increase is cancelled (footnote 3). | partial | D49 | open | +| `#69/c5881157875/2c` | V5 | Harvest 4 misprices legacy Fable 5 cache reads: CACHE_READ family key 'fable' bills Fable 5 at 0.025x instead of 0.1x (bug in every target on 74edf84). | yes | D51 | open | +| `#69/c5881157875/2d` | V5 | Fix is version keys plus a drift check diffing the CACHE_READ dict against the pricing sentence. | yes | D51 | open | +| `#69/c5881157875/3a` | V5 | Ultracode no longer implies xhigh (2.1.284); docs lag; cite the changelog; a template relying on ultracode for xhigh passes --effort xhigh explicitly. | yes | D49, D47 | open | +| `#69/c5881157875/3b` | V5 | 2.1.283 added /doctor prompt-audit, a first-party audit for prompts written for older models; de-prescription becomes a measurement. | yes | D49 | open | +| `#69/c5881157875/3c` | V5 | Auto mode is the new interactive default (2.1.283 third-party/telemetry-off, 2.1.284 all); permissions.defaultMode still overrides. | yes | D49 | open | +| `#69/c5881157875/3d` | V5 | The WebSearch cap and MCP auto-backgrounding rails still stand (no later removal through 2.1.284). | yes | D49 | open | +| `#69/c5881157875/4` | V5 | Haiku 4.5 retirement floor 'Not sooner than October 15, 2026' as the mechanical-tier re-check trigger. | yes | D49 | open | +| `#69/c5881157875/5` | V5 | Primary source for subagent-driven not adopted: cost page 'When delegation doesn't pay'. | yes | D49 | open | +| `#69/c5892402564/Q1` | V5 | Quote moved: workflows page now reads 'Up to 16 concurrent agents by default, fewer when...'; kit's template quote is stale. | yes | D49 | open | +| `#69/c5892402564/Q2` | V5 | Quote moved: model-config now says the ultracode toggle leaves the effort level unchanged. | yes | D49 | open | +| `#65/body/1` | V5 | orchestration-reference.md:65 ships a square-bracket template placeholder in the reference half; the sync's placeholder grep does not catch square brackets. Move the slot to the contract half or add the pattern to the grep. | yes | none | open | +| `#65/c5861166806/1b` | V5 | Kit's placeholder grep needs the bracket-split idiom (or an exclusion for the file holding it) so the check does not match its own source. | yes | none | open | +| `#65/body/2` | V5 | orchestration.md:36 says 'two worked exemplars' then names three. | yes | none | open | +| `#65/body/3` | V5 | Contract intro lists three dispatch surfaces; reference now documents a fourth (skill frontmatter). | yes | none | open | +| `#65/c5861166806/3b` | V5 | Heading 'The three surfaces + resolution order' now carries a count its list outgrew; renaming both halves' headings and citations together belongs in the kit. | yes | none | open | +| `#74/body/3` | V5 | The Agent tool takes no effort, so a floor's effort is recorded, not pinned: add the rule text after 'a worker gets a chosen level, stated.' | yes | none | open | +| `#68/body/3` | V5 | Check which binary a tool resolves before accepting an agent's claim about it (command -v / type -a from the child context, never type -P). | yes | none | open | +| `#68/c5892402074/3-count` | V5 | Sweep the rule count: adding rule 4 changes 'three rules' text elsewhere. | yes | none | open | +| `#58/c5901493591/R9` | V5 | Applied-instances '3.2x pooled top:workhorse' (and agent-cost.py '3.2x to 3.6x') was priced at the old 0.1x top-tier cache rate; add a conditional note (Fable 5.1, default Fable from Claude Code 2.1.257 published 2026-09-01T17:15… | yes | none | open | +| `#69/body/finish-ab/N-arm` | V6 | finish-ab generalised from two arms to two to four, with per-arm model and effort (and an `also` list of extra instruction files per arm) | yes | D45 | open | +| `#69/body/finish-ab/latin-square` | V6 | Latin-square panel check: every arm read in every position equally often, replacing the two-order even split | yes | D45 | open | +| `#69/body/finish-ab/measures` | V6 | Judges return structured, order-independent measures (JUDGE_SCHEMA `measures`) | yes | D45 | open | +| `#69/body/finish-ab/executed` | V6 | `executed` input: arms that ran outside the workflow (headless) are judged without an Execute phase | yes | D45 | open | +| `#69/body/finish-ab/replay-args` | V6 | (implicit in the port) `base`, `brief`, `rubric` args for replays and non-scratch briefs; echosphere 2e asks they be tested | yes | D45 | open | +| `#69/body/finish-ab/runner` | V6 | run-arms-headless.py: run arms as headless `claude -p` sessions, each in its own worktree, auto mode, deny rules for remote writes, CLAUDE* env stripped | yes | D45 | open | +| `#69/body/finish-ab/runner-fixture` | V6 | (companion of the runner) a hermetic fixture for run-arms-headless.py with a fake `claude` | yes | D45 | open | +| `#69/body/cost/version-keys` | V6 | agent-cost.py: PRICE keyed by model version, not family, so Opus 5.5 ($4/$20) is not billed as Opus 5 ($5/$25) | yes | D49 | open | +| `#69/body/cost/cache-read` | V6 | CACHE_READ per model: Opus 5.5 at 0.05x (and Fable/Mythos 5.1 at 0.025x), default 0.1x | yes | D49 | open | +| `#69/body/cost/block-diff` | V6 | The verification block's price diff matches versions on both sides ('Opus 5.5 $4/$20' -> 'opus-5-5 4 20') | yes | D51 | open | +| `#69/body/cost/fixture-rows` | V6 | agent-cost fixture gains an Opus 5.5 row and a Fable 5.1 row | partial | D49 | open | +| `#69/body/cost/synthetic` | V6 | `` zero-usage messages made whole rows unpriced | yes | D49 | open | +| `#69/body/cost/fast-mode` | V6 | Fast-mode spend (`usage.speed`) was priced at list | yes | D49 | open | +| `#69/body/H1` | V6 | Harness finding 1: a workflow agent has no Agent tool, so arms that must dispatch (subagent-driven, review floor with fan-out) run as headless `claude -p` sessions | partial | D49 | open | +| `#69/body/H2` | V6 | Harness finding 2: unattended /finish on Opus 5.5 stops early; resume in the same session with a message naming what is open, at most two or three times | yes | D45 | open | +| `#69/body/H3` | V6 | Harness finding 3: a resumed `claude -p` run reports the conversation's whole total; never sum totals across invocations | yes | D49 | open | +| `#69/body/H4` | V6 | Harness finding 4: --max-budget-usd does not count restored spend, so give each continuation only what the cap has left | yes | D45 | open | +| `#69/body/H5` | V6 | Harness finding 5: deny the GitHub MCP servers, not only `git push` and gh writes; write `git * push` too | yes | D45 | open | +| `#69/body/H6` | V6 | Harness finding 6: a contamination diff against a reference the same model wrote over-flags; check transcripts for reads past the base commit | yes | D45 | open | +| `#69/body/H7` | V6 | Harness finding 7: the rubric must quote the verdict rule's measure definitions verbatim (a stricter wording split a panel 3-3) | yes | D45 | open | +| `#69/c5859470658/1` | V6 | The workflow fixtures run in the gate (finish-ab-shape, load-workflow-shape, review-sweep-accounting, agent-cost-fixture) | partial | D51 | open | +| `#69/c5859470658/2` | V6 | run-arms-headless.py worktree_root must be gitignored (arm reviewers write .claude/agent-memory/ in the worktree; the fork detector blocks the launching session) | yes | D52 | open | +| `#69/c5859470658/3` | V6 | A rubric reused across replicates should not state the arm count (seventh eval departure) | yes | D45 | open | +| `#69/c5859756889/1a` | V6 | agent-cost.py merges 'clean' with two CACHE_READ definitions | partial | D49 | open | +| `#69/c5859756889/1b` | V6 | The two fixtures reuse the same names for different cases (agent-cost eee/fff/ggg; finish-ab-shape 11-13 vs 9-11): must be unions | partial | D45 | open | +| `#69/c5859756889/1c` | V6 | Arm-label guard message names otherFile(), which the N-arm rewrite deletes; keep the check, change the reason | yes | D45 | open | +| `#69/c5859756889/2a` | V6 | --resume-existing crashes on a killed run's empty payload | yes | D45 | open | +| `#69/c5859756889/2b` | V6 | Continuations drop --append-system-prompt UNATTENDED | yes | D45 | open | +| `#69/c5859756889/2c` | V6 | DENY is target-specific; docstring should say so; add the planning-backend MCP; --strict-mcp-config with an --mcp-config naming only read servers is the stronger form | yes | D45 | open | +| `#69/c5859756889/2d` | V6 | agent-cost fixture misses two regressions (first-match tier(); one-timestamp minutes rule; paired independent greps) | yes | D49 | open | +| `#69/c5859756889/2e` | V6 | finish-ab-shape leaves three arguments untested (four-arm ceiling, base, brief/rubric) | yes | D45 | open | +| `#69/c5859756889/2f` | V6 | ARM_SCHEMA `dispatched` is required in the runner but not in finish-ab.js; two hand-kept copies need a drift test | yes | D45 | open | +| `#69/c5859756889/2g` | V6 | CACHE_READ has no drift check while PRICE does; diff it against the quoted pricing sentence | yes | D51 | open | +| `#69/c5859811353/H1` | V6 | Review finding 1 (High): `--disallowedTools *DENY` may apply only the first rule; comma-join it | n/a | D45 | open | +| `#69/c5859811353/H2` | V6 | Review finding 2: --resume-existing re-grants a fresh max_continuations per invocation | yes | D45 | open | +| `#69/c5859811353/H3` | V6 | Review finding 3 (doc): DENY's blanket `gh api *` would block CI-status reads the bot template suggests | yes | D45 | open | +| `#69/c5881157875/2-steps` | V6 | Price steps changed (Haiku 4.5 $1, Sonnet 5.5 $2, Opus 5.5 $4, Fable 5.1 $10 = 2x/2x/2.5x); the template's '2x / 2.5x / 2x' and Fable '2x-Opus' are stale | yes | D49 | open | +| `#69/c5881157875/2-sonnet5` | V6 | Sonnet 5's scheduled increase to $3/$15 is cancelled (pricing footnote 3) | partial | D49 | open | +| `#69/c5881157875/2-fable5-misprice` | V6 | Harvest 4 misprices legacy Fable 5 cache reads (family key bills Fable 5 at 0.025x) | yes | D49 | open | +| `#69/c5881157875/2-version-keys-fix` | V6 | The fix is version keys: claude-fable-5-1 and claude-mythos-5-1 at 0.025, claude-opus-5-5 at 0.05, default 0.1; echosphere 2g check diffs the dict | yes | D51 | open | +| `#69/c5881157875/6` | V6 | Finding 1 of the automated review does not reproduce; keep the spread; a comment citing the probe stops re-raising | yes | D45 | open | +| `#69/c5892402564/cost-1` | V6 | One version-keyed CACHE_READ: Fable 5.1 and Mythos 5.1 0.025x, Opus 5.5 0.05x, default 0.1x | yes | D49 | open | +| `#69/c5892402564/cost-2` | V6 | PRICE gains Fable 5, Mythos 5.1 and Sonnet 5.5 | yes | D49 | open | +| `#69/c5892402564/cost-3` | V6 | Verification block diffs CACHE_READ against the pricing page's quoted cache sentence | yes | D51 | open | +| `#69/c5892402564/cost-4` | V6 | Both mutants fail the fixture (first-match tier(); one-timestamp minutes rule) | yes | D49 | open | +| `#69/c5892402564/transcripts` | V6 | message.model never carries [1m] in transcripts; the suffix that matters is the dated snapshot claude-haiku-4-5-20251001 | yes | D49 | open | +| `#69/c5892402564/probe` | V6 | Workflow-agent probe re-run on 2.1.284; pr-review.md § The floor cites it | partial | D49 | open | +| `#69/c5892402564/finish-ab-drift` | V6 | Drift test between the two ARM_SCHEMA copies; `dispatched` commented in both | yes | D45 | open | +| `#69/c5892402564/finish-ab-dryrun` | V6 | --dry-run prints each arm's argv | yes | D45 | open | +| `#69/c5892402564/finish-ab-allowlist` | V6 | Runner's MCP gate is an allowlist (context7, time); an unknown server is refused | yes | D45 | open | +| `#69/c5892402564/finish-ab-nonobject` | V6 | A non-object arm is a config error, not a traceback | yes | D45 | open | +| `#69/c5892402564/finish-ab-failcarried` | V6 | A complete arm whose last invocation failed carries that failure into its notes and runner line | yes | D45 | open | +| `#69/c5892402564/finish-ab-closedfiles` | V6 | Unclosed files (payload, config, mcp_config reads); fixture fails on ResourceWarning; echosphere's runner carries two of three | yes | D45 | open | +| `#69/c5892402564/finish-ab-deny` | V6 | DENY covers every write-capable server the private target wires | n/a | D45 | open | +| `#69/c5892402564/finish-ab-cap` | V6 | Finding 2 settled by stating the continuation cap per runner pass | yes | D45 | open | +| `#69/c5901496433/1a` | V6 | Runner check_command: run once per complete arm, one at a time, after every arm has finished, logs beside out_dir (never in it), runner_check in the result, executed.json written before and after | yes | D45 | open | +| `#69/c5901496433/1b` | V6 | finish-ab.js: when a result carries runner_check, name each arm's log and exit in the judge prompt and tell judges not to re-run the whole gate | yes | D45 | open | +| `#69/c5901496433/1c` | V6 | judge-rubric.md gate line says read the runner's log | yes | D45 | open | +| `#69/c5901496433/2` | V6 | Under --strict-mcp-config an arm cannot read an issue behind an MCP server: brief gains 'The issue, verbatim'; runner docstring says so | yes | D45 | open | +| `#69/c5901496433/3` | V6 | Deny list's bare `git push` is redundant (sole trailing wildcard matches the bare command); `git * push` stays | yes | D45 | open | +| `#69/c5901496433/4` | V6 | Brief and rubric state no arm count | yes | D45 | open | +| `#69/c5901496433/5` | V6 | Judge blindness leaks past arm ids: args comment, rubric callout on what to strip, never point a judge at out_dir | yes | D45 | open | +| `#69/c5901496433/6` | V6 | tier() matches a key whole (longest-match still prices claude-opus-5-6 as Opus 5) | yes | D49 | open | +| `#69/c5901496433/measurement` | V6 | Budget for #69's instruction-budget cost (the private target +11,163 B; echosphere +9.7 KB) | yes | D50 | open | +| `#72/body/1 — a caller-named finder cannot carry a prompt` | V7 | The finder slot takes only {key, agentType}, so a targeted concern with no defined agent cannot ride in the sweep. Make a finder {key, prompt?, agentType?}: the prompt becomes the find prompt's focus, agentType is passed only whe… | yes | D46 | open | +| `#72/body/2 — paraphrased duplicates each take a verify slot (dedup on file+line)` | V7 | Dedup keys on file:line:title, so three dimensions paraphrasing one defect make three findings and three verifies. Key on file:line (title only when a finding has no line), carry every distinct title and detail, count a dimension… | yes | D46 | open | +| `#72/body/3 — the roster read's catch discards why it threw` | V7 | `.catch(() => null)` on the roster read degrades correctly but keeps no record of the reason; log the message before returning null, and assert it in the throwing-roster scenario. | yes | D46 | open | +| `#72/body/4 — nothing tells a finder or verifier not to touch the tree (read-onl…` | V7 | Every find and verify prompt carries a READ_ONLY clause (never modify the working tree, not even to restore; probe on a copy with its own build cache); orchestration.md § Fan-out discipline gains 'Read-only agents stay read-only'… | yes | D46 | open | +| `#72/body/5 — an interrupted floor or sweep` | V7 | Invariant (2) covers a failed agent but not an interrupted or stopped run: state that an interrupted or stopped floor or sweep is re-run fresh, none of its partial output is used, and reviewer memory it wrote is discarded, never… | yes | D46 | open | +| `#74/body/1 — a post-floor delta is checked by one verification workflow, never…` | V7 | Append to § The floor › Once the rule for the two cases the once-only rule leaves open (an operator who wants a delta checked before the flip; an issue-less tooling PR with no floor to run): one fresh-context verification workflo… | yes | none (no D-number; covered by… | open | +| `#74/body/2 — a consolidation the operator prompts at rough-in's gate is re-veri…` | V7 | Add to rough-in's spec-verification stage: a change made at the gate (issues merged, a criterion moved or dropped, counts reshaped) is a new draft; run one more fresh-context verifier on the consolidated set before the one-way co… | yes | none (D42 atomic-per-cluster) | open | +| `#74/body/Cross-reference — keep the review layers independent (duplicate of #58…` | V7 | The #33 closure's dropped note 'keep the review layers independent' (floor, sweep, flip auto-review and each /pr-respond round each caught what the previous missed) is carried in the #58 residue comment; land it in pr-review-refe… | yes | none | open | +| `#58/c5901493591/B — #33's review-layer calibration note landed nowhere` | V7 | Add the dropped #33 note to pr-review-reference.md § Anti-patterns as 'Folding one review layer into another': the floor, the sweep, the flip's auto-review and each /pr-respond round are separate layers that each caught what the… | yes | none (D53 for the citation fo… | open | +| `#58/c5901493591/R10 — the roster-line 'enumerate a family of directories' guida…` | V7 | State in the rules a project reads when writing its roster line that a family of directories (a segment or family pattern) has no prefix form: enumerate each directory with its slash and add one when the family grows. pr-review.m… | yes | none (D53 citation form; D50… | open | +| `#64/body — pr-respond.md 'Does not modify the PR title or description body' con…` | V7 | Reconcile pr-respond.md:141 with Step 7 (line 103): the bullet still describes the pre-harvest-4 behaviour (a summary comment only) while Step 7 appends a round block to the PR body. Change the bullet, presumably to 'Does not mod… | yes | none | open | +| `#70/body/4` | V8 | Stop saying a container base-image tag is outside every update bot; say a Dockerfile FROM and a compose image: ARE covered (docker and docker-compose ecosystems bump tag+digest, both support cooldown default-days) and name the th… | yes | D52 | open | +| `#70/body/4-stubs` | V8 | (Derived from D52 'Dependabot docker coverage', not a separate line in #70) Because a Dockerfile FROM / compose image: is bot-covered, the kit's Dependabot examples should offer the covering ecosystems with the floor, not a singl… | partial | D52 | open | +| `#70/body/5` | V8 | mise runs inline tasks under `sh -c -o errexit` (dash on Debian/Ubuntu, no pipefail); ship a `[task_config] shell = "bash -O inherit_errexit -c -o errexit -o pipefail"` with the reasons and the mise version floor for the kit's mi… | yes | D52 | open | +| `#70/body/6a` | V8 | Pin the servers in `.mcp.json.example` to exact versions past the settle window (unpinned `npx -y ...` fetches the newest at every session start, outside every lockfile). | yes | D52 | open | +| `#70/body/6b` | V8 | Say in the settle-window section that an MCP server is a dependency. | yes | D52 | open | +| `#70/table/mise-mcp-row` | V8 | 'Where it lands' table row: 'the mise template; `.mcp.json.example` \| items 5, 6 \| inline above' (the other rows map items 1-4 to the templates, starter and reference; rows for 2-3 belong to another cluster). | yes | D52 | open | +| `#71/body/1` | V8 | Kit-owned code under .claude/workflows/** (byte-identical, `copy` rows assert it) is rewritten by a target's repo-wide formatter or linter (a target's pre-commit ruff rewrote agent-cost.py with 18 findings). Say in § Syncing the… | yes | D52 | open | +| `#71/body/2` | V8 | An edit-time formatter hook hands the formatter an explicit path and bypasses the formatter's own excludes (ruff formats an explicit path unless force-exclude); the kit's format-on-edit.sh exemplar arm `YOUR_PYTHON_FORMATTER "$fi… | yes | D52 | open | +| `#71/body/3` | V8 | Harness trees need anchored ignores in the committed .gitignore, never only a local .git/info/exclude: /.claude/worktrees/ (Claude Code --worktree, isolated subagents, finish-ab headless arms; an arm's reviewers also write .claud… | yes | D52 | open | +| `#71/table/bootstrap-checklist` | V8 | 'Where it lands' row: scaffold/references/bootstrap_checklist_template.md gets 'one-time choice: formatter and linter scope, plus the harness block'. | yes | D52 | open | +| `#71/decision/other-formatters` | V8 | Left for the kit to decide: other formatters. the private target measured only ruff; whether biome (and others) honour an ignore for a path handed explicitly is unverified; write the rule tool-neutral ('forced for explicit paths'… | yes | D52 | open | +| `#71/decision/runtime-paths` | V8 | Left for the kit to decide: Claude Code's own runtime paths (a `# claude-code-runtime` block in .git/info/exclude: scheduled_tasks.*, routines/.state/, worktrees/, checkpoints/, mailbox/, agent-registry.json, agent-memory-local,… | yes | D52 | open | +| `#58/c5901493591/R7` | V8 | R7: harvest 4's spec says github-starter-templates.md's .gitignore starter gained anchored /.claude-pr/, but the file has no .gitignore section at all. Add `## .gitignore — the harness block` with /.claude/agent-memory-local/, /.… | yes | D52 | open | +| `#63/body` | V9 | finish-procedure.md cites `CONTRIBUTING.md` § Branches, a heading a target may rename; cite the conventions instead (or make the heading a fill slot). | yes | D53 (with D54 for the branch-… | open | +| `#63/c5901495240/1` | V9 | Retire the eight prose citations of the retired frame section 'Deferred meta-issues'; cite § Pre-flight checks. | yes | D53 (citation class); the che… | open | +| `#63/c5901495240/1/check` | V9 | Widen the verification block's retired-section check from the heading to any citation: `absent grep -rnE "Deferred meta-issue[s]" .claude/skills/`. | yes | D51 | open | +| `#63/c5901495240/2` | V9 | cascade-rule-reviewer.md points 'commit format' at a section cbk-conventions.md lacks; cite the heading every target has (§ Closes-keyword conventions), never a CONTRIBUTING heading. | yes | D53 | open | +| `#66/body/1` | V9 | adr-new § 'Refines vs Supersedes vs Extends' restates cbk-conventions-reference.md § ADR relation grains; reduce to a pointer plus the skill's own mechanics. | yes | none (taste-class; recommend… | open | +| `#66/body/2` | V9 | settings.json `_comment_hooks` (4,103 chars) restates the four-tier definitions and per-hook facts each hook header already carries; trim to the registry plus pointers, keeping every hook name and tier word the block reads. | yes | none (taste-class; recommend… | open | +| `#66/c5901495490` | V9 | knowledge-backend.md § Hook enforcement layer restates the ask-gate tier rationale; replace with a pointer and keep the hook, its dated matcher and the unmatched reads. | yes | D50 (budget accounting); othe… | open | +| `#68/body/1a` | V9 | Say which reading of the issue-less branch holds when an issue exists but is not a cascade issue; simpler rule: any issue the PR closes puts its key in the branch. | yes | D54 | open | +| `#68/body/1b` | V9 | Quick reference row 'Naming a branch' gives only the keyed form; add the issue-less form. | yes | D54 | open | +| `#58/c5901493591/R2` | V9 | A meta's children `[::R<#>]` still fail /finish Step 1's four-form title check; admit the child form as a fifth. | yes | none (residue of harvest-4 #5… | open | +| `#58/c5901493591/R3` | V9 | adr-new:52 still pins `Accepted · Dn superseded by ADR-${NNNN}` and § ADR relation grains still pins `·`; tell the executor to read the index's rows and let a target's index set its own cell and separator. | yes | D39 (harvest 4, adr-new reads… | open | +| `#58/c5901493591/R14` | V9 | Kit-internal traceability slips (H3 claims a .gitignore starter that does not exist; H4 says it closes item 6 against D31; cluster headings cite 'the 2026-09-13 comment' though three comments carry that date; items covered only b… | yes | D41/D42 (one PR, atomic commi… | open | +| `#58/c5901493591/residue-1` | V9 | Recorded, not applied: the adr-new heading 'Refines vs Supersedes vs Extends' is cosmetic (its sentence was fixed). | partial | none (taste-class); rides #66… | open | +| `#58/c5901493591/residue-2` | V9 | Recorded, not applied: item 4's 'Shape 2' (run the project sub-block first) is declined by D31 fail-fast; under fail-fast a kit-side red still hides the state of a target's project checks. | yes | D31 (harvest 4) stands; harve… | open | +| `#69/c5859756889/apply-h4/1` | V9 | § Hook authoring should say in one sentence that a target's own hooks are held to the same stdin/exit contract (the fixture reads every .claude/hooks/*.sh and caught a project hook that drained stdin after its jq check). | yes | D43/D44 adjacent | open | +| `#69/c5859756889/apply-h4/2` | V9 | § Syncing the kit should say: after resolving settings.json, diff the hooks block's event keys against the pre-merge copy (a Stop registration fell inside a conflict hunk and was dropped by taking the kit's side). | yes | D41 (Sync notes) | open | +| `#69/c5859756889/apply-h4/3` | V9 | D40's Kit commit row has no home in a target whose immutable scaffold.md predates it; § Syncing the kit could name the fallback. | yes | D41 | open | +| `#69/c5859756889/apply-h4/4` | V9 | Kit text cites its own issues bare (#58, #33, #34); bare numbers autolink to the target's issues. Cite as context-builder-kit#N. | yes | D53 | open | +| `#69/c5859756889/apply-h4/5` | V9 | H7 on a target that already had it: echosphere's claude-review.yml already carried `checks: read` in both places and `gh pr diff`; only the degrade clause was new (N = 40 against 60). | n/a | D41 | open | +| `#69/c5859756889/apply-h4/6` | V9 | echosphere's cbk-verify now calls run-verification-block.sh, since the runner is the generalized form of its inline body. | yes | D51 | open | +| `#69/c5901496433/sync-note` | V9 | A sync note for the mint: a target running files ahead of the kit records them in § Syncing the kit grouped by kit issue, and its next sync three-way-merges those files against the kit commit that lands them, not the install sha;… | yes | D41 | open | +| `release/1` | V10 | Release: § Syncing the kit and its contract pointer must speak in versions (tags as merge bases, CHANGELOG Sync notes first, the Kit commit row records `vX.Y.Z (sha)`). | yes | D41 | open | +| `release/2` | V10 | Release: scaffold's Cascade metadata Kit commit row must record `vX.Y.Z (sha)` and scaffold must actually produce it. | yes | D41 | open | +| `release/3` | V10 | Release: refresh README.md inventory claims that are stale at HEAD or will be by the harvest. | yes | D41, D50 | open | +| `release/4` | V10 | Release: README Quick start installs an unpinned main tarball with a placeholder fork and would overwrite the target's own files. | yes | D41 | open | +| `release/5` | V10 | Release: every bare #N self-citation of a kit issue in kit content becomes context-builder-kit#N, and a check keeps it that way. | yes | D53 | open | +| `release/6` | V10 | Release: create CHANGELOG.md with one section per tag and per-tag Sync notes. | yes | D41 | open | +| `release/7` | V10 | Release: back-tag v0.1.0-v0.5.0 on past harvest merges and tag v1.0.0 on this merge (process, no file). | yes | D41 | open | +| `release/8` | V10 | Release: the kit's own CLAUDE.md layout and the conventions' Surface inventory must reflect releases. | yes | D41 | open | +| `release/9` | V10 | D53 sweep beyond CONTRIBUTING: other kit citations of target-doc headings that the templates do not produce. | yes | D53 | open | +| `release/10` | V10 | Release: kit content names a sibling project and cites its issue/PR bare. | yes | D53 (portability invariant) | open | + +## Asks the completeness critic added + +| Id | Cl. | Ask | Status | +|---|---|---|---| +| `critic/1` · #70/body 'Where it lands' tail (kit sub-block check, the private target) | V4 | Add a kit sub-block check to the verification block with three assertions: no `runs-on: ubuntu-latest` under .claude/skills/; each of claude-review.yml, claude.yml, tooling.md and github-starter-templates.md sets `shell: bash`; n… | open | +| `critic/2` · #70/body 'Where it lands' table, claude-review.yml row (293f03b) | V4 | In the label-removal step, rewrite the comment to say that a label deleted from the repository is the benign branch. Every other failure is an error. | open | +| `critic/3` · #70/body item 4 (first gap) | V8 | State the remediation for the COPY --from gap: route such an image through a named `FROM … AS ` so the docker updater reads it. | open | +| `critic/4` · #70/body items 5 and 6 (the private target tests its pipefail test / test_supply_cha… | V8 | Optionally give the kit a guard for the mise shell (fail if an inline-task shell drops pipefail or a task overrides it) and for `.mcp.json.example` (fail on an unpinned npx/uvx stdio server), mirroring the private target's two te… | open | +| `critic/5` · #71/body item 2 | V8 | format-on-edit.sh's hand-kept skip list does not name `.claude/workflows/*`. As the floor ('trees no formatter may touch whatever a config says'), it should include the kit-owned tree, not only carry the --force-exclude comment. | open | +| `critic/6` · #60/c5876324022 (ADR-job section, you-are-hear paragraph) | V2 | The frozen-corpus recipe tells a target to extend the ADR-immutability job, and doing so inherits the awk/quoted-path gap. Change the recipe in consultation frozen_corpus_ingestion.md / cbk-conventions: extend with the parse-noth… | open | +| `critic/7` · #60/c5901494943/1 (closing sentence of the quoted enforcement set) | V2 | The enforcement set also states that whether an added file passes is the target's call: one target allows additions, the other freezes the directory whole. | open | +| `critic/8` · #58/c5901493591 R1 (proof column) and closing paragraph | V2 | Port the three hook fixtures with the kit's names: rename a renamed knowledge-backend hook to require-knowledge-backend-ok.sh inside hook-guards-fixture.sh. Make the kit's verification block run all three fixtures, as the private target's do… | open | +| `critic/9` · #58/c5901493591 'How the audit was made' | V9 | Adopt the audit method as the gate for the next mint: every ask listed as one entry, each claim located in the spec or PR, checked against the tree with file:line, given a verdict, and every non-landed verdict re-checked by a ref… | open | +| `critic/10` · #65/c5861166806 item 1 and Update 2026-09-28 | V5 | The placeholder check should catch both unfilled slots, the contract's Posture slot in always-loaded orchestration.md and the reference half's pins slot, for example with the bracket-split `\[Recor[d] the project` over .claude/ru… | open | +| `critic/11` · #63/body | V9 | The bundled copy `rough-in/references/finish-procedure.md` carries the same CONTRIBUTING.md § Branches citation twice more, four citations in all, and needs the same fix. | open | +| `critic/12` · #67/c5881158070 item 1 (last bullet) | V4 | v1.0.236 adds a declared `conclusion` output ('success' or 'failure'). Consider using it in the assert or record steps where the pinned action has it; v1.0.183 lacks it. | open | +| `critic/13` · #67/c5892401572 correction paragraph | V4 | The operative skip-notice mechanism: key the 'no session' notice on an empty execution_file plus `git diff --quiet "$BASE_SHA" HEAD -- .github/workflows/claude-review.yml`. A changed file means the validation skip (re-triggering… | open | +| `critic/14` · #67/c5901495773 item 4 | V4 | The template tells a target to re-measure the degrade N / --max-turns ratio on the Claude Code release its pinned action installs (the 2.1.274 measurement is not portable). | open | +| `critic/15` · #68/c5861166655 item 2 | V4 | Echosphere's restore section also asks the reviewer to state which rule version (base or PR) the review applied. Include that in the template prompt section. | open | +| `critic/16` · #69/c5859756889 §4 'What does not port to a kit-text target' | V5 | Leave six things out of the kit's #69 application: the replay-78 records (cite by path), the '/finish at medium' row, the posture change (#65 owns it), the workflow-fixtures mise task (the runner is the kit's shape), the finish-c… | open | +| `critic/17` · #69/c5859756889 §4 last paragraph and intro | V5 | Re-author #69 onto the kit's text rather than merging you-are-hear's 3148517. Reattribute sentences that assert you-are-hear facts ('measured here on a replay of #78', 'the operator chose the file', 'the protocol is kept as the r… | open | +| `critic/18` · #69/c5881157875 item 1 (API quote on Sonnet 5.5) | V5 | Sonnet 5.5's effort levels are recalibrated ('Run a fresh effort sweep ... rather than carrying over the setting you used on Claude Sonnet 5'). The mid-tier finder row (medium, retry high) and the fast-review branch's effort need… | open | +| `critic/19` · #69/c5881157875 item 1 + #69/c5892402564 cost reader | V6 | The reference half's quoted pricing row and the verification block's PRICE diff must cover the new PRICE keys: Sonnet 5.5 at $2/$10 with $0.20 cache reads, Fable 5, and Mythos 5.1. Otherwise the diff fails or the quote goes stale. | open | +| `critic/20` · #69/c5859756889 item 2d | V6 | The agent-cost fixture's importlib check must run as `python3 -B`, or it writes __pycache__ into the kit tree. | open | +| `critic/21` · #72/body 'Where it lands' (review-sweep-accounting.mjs) | V7 | Port the red-first stub scenarios 25–27 and the logged-throw assertion to review-sweep-accounting.mjs, with the stub recording each call's prompt. Each fix should turn its own scenario green in sequence. | open | +| `critic/22` · #73/body item 1 (Test paragraph and table) | V1 | The verification block itself runs run-verification-block-fixture.sh. Reword § Verification › Run it to 'three fail-loud rails'. | open | + +## Whole-kit review findings (D56) + +Verified findings land as written; unverified ones are re-checked when their commit is made. + +| Id | Cl. | Location | Finding | Verified | Status | +|---|---|---|---|---|---| +| `review/consistency/1` | V10 | `README.md:430` | README status claims the tree supersedes: 'No upgrade mechanism yet' and 'no real cascade run has exercised' github-issues / v1.0.0 has no versioning statement or usable upgrade path; README says 'No… | yes | open | +| `review/consistency/2` | V10 | `README.md:3` | README inventory contradicts the tree: 'two reviewer agents, one slash command', plus files missing from 'What the kit ships' / Headline counts are stale and contradict README's own later text / READ… | yes | open | +| `review/consistency/3` | V9 | `.claude/skills/blueprint/SKILL.md:95` | Blueprint's markdown-only acknowledgment keys on a `profile` field scaffold no longer writes; one axis value has two names / Blueprint's markdown-only acknowledgment keys on a `profile: markdown-only… | yes | open | +| `review/consistency/4` | V9 | `.claude/skills/scaffold/SKILL.md:167` | Rough-in issue template is described as six or seven sections in eight files; the template and executor require eight | yes | open | +| `review/consistency/5` | V9 | `.claude/skills/rough-in/references/templates/rough-in-spec-template.md:177` | Acceptance-criteria ID form disagrees across the contract, both templates and the conventions | yes | open | +| `review/consistency/6` | V10 | `README.md:45` | Three answers to whether .mcp.json holds literal secrets and is committed or gitignored | yes | open | +| `review/consistency/7` | V10 | `README.md:34` | Quick start extracts the whole kit repo over the target, contradicting CLAUDE.md's statement that the deliverable is `.claude/` | yes | open | +| `review/consistency/8` | V3 | `.claude/commands/finish.md:21` | Kit issue number and a sister-repo name leak into shipped, byte-parallel content; the portability check the kit describes does not run on the kit tree | yes | open | +| `review/claude-code/9` | V3 | `.claude/commands/finish.md:6` | Commands use $1 for the first argument; current Claude Code treats $1 as the SECOND (0-based), so it is never substituted | yes | open | +| `review/claude-code/10` | V3 | `.claude/settings.json:11` | Hook commands reference ${CLAUDE_PROJECT_DIR} unquoted in shell form: a project path containing a space makes every HARD-DENY guard exit 127 (non-blocking) and silently fail open | yes | open | +| `review/claude-code/11` | V9 | `.claude/skills/rough-in/references/plan-mode-prompts.md:306` | The rough-in drafting reference tells drafters to emit six headings (and other files say seven) while /finish requires exactly eight | yes | open | +| `review/claude-code/12` | V8 | `.mcp.json.example:22` | The notion and context7 MCP entries have a url but no type, so Claude Code skips them; credentials are literal placeholders, not env references / Two of the example's stdio servers do not exist on np… | yes | open | +| `review/claude-code/13` | V3 | `.claude/settings.json:64` | enabledPlugins keys omit @marketplace, so the declared plugins are never enabled / enabledPlugins keys are bare plugin names; Claude Code documents `plugin-name@marketplace-name` | yes | open | +| `review/claude-code/14` | V3 | `.claude/commands/finish.md:2` | /finish, /enrich, /intake and /pr-respond are model-invocable although they create branches, PRs, issues and pushes | yes | open | +| `review/claude-code/15` | V3 | `.claude/agents/Explore.md:1` | The cost-motivated Explore override lacks omitClaudeMd, so it loads CLAUDE.md and every unscoped rule that the built-in Explore skips | yes | open | +| `review/claude-code/16` | V3 | `.claude/rules/tooling.md:84` | Always-loaded rule states that a repeated list in settings.local.json 'silently replaces' the committed one; the docs say lists merge and opting a plugin out locally is the documented route | yes | open | +| `review/security/17` | V8 | `.mcp.json.example:8` | Kit tells targets to paste literal PATs/API keys into .mcp.json while its own rules say to commit env references, and nothing gitignores .mcp.json | yes | open | +| `review/security/18` | V4 | `.claude/skills/blueprint/references/templates/claude-review.yml:58` | Workflow-level cancel-in-progress lets an unrelated, job-skipped run cancel the in-flight review | yes | open | +| `review/security/19` | V2 | `.github/workflows/adr-immutability-check.yml:41` | Two-dot `git diff BASE HEAD` reports an ADR added on main after the PR branched as a Deleted ADR, so the required check goes red | yes | open | +| `review/security/20` | V2 | `.claude/hooks/protect-main-branch.sh:62` | Left and right anchors in the hard-deny `git commit` regex are bypassed by quoting, subshells and `;`/`&&` after the verb (same anchor in guard-pr-state.sh:42) | yes | open | +| `review/security/21` | V3 | `.claude/commands/intake.md:45` | /intake runs a reproduction derived from an outside reporter's text, and /pr-respond acts on any commenter's text, with no untrusted-input rule the kit states elsewhere | yes | open | +| `review/security/22` | V4 | `.claude/skills/blueprint/references/templates/claude-review.yml:65` | Review workflow has no fork guard: on fork PRs it can never run and instead goes red (and label removal errors) | yes | open | +| `review/release/23` | V10 | `README.md:33` | Quick start tarball overwrites the adopter's README, LICENSE, CLAUDE.md, .gitignore and ships kit-internal files / Quick start extracts the whole kit repo over the target root, overwriting its README… | yes | open | +| `review/release/24` | V8 | `.mcp.json.example:7` | Example MCP config launches a deprecated package and two packages that do not exist on npm / Example MCP config points at two npm packages that do not exist and two url entries Claude Code rejects | yes | open | +| `review/release/25` | V10 | `README.md:29` | Undeclared prerequisites and no Claude Code version floor: only git and mise are named, but hooks, gh flows, the verification block and review CI need jq, gh, node, python3, bash and a CI token | yes | open | +| `review/release/26` | V10 | `README.md:119` | 'Exercised vs designed' still says GitHub Issues planning has never been exercised, contradicted by the kit's own harvest-4 spec / README and four backends.md copies still say no real run has exercis… | yes | open | +| `review/release/27` | V10 | `README.md:313` | README tree says the advisory hook stanzas live in settings.json; settings.json says the opposite and the reverse is load-bearing | yes | open | +| `review/release/28` | V8 | `.mcp.json.example:9` | Secret handling is incoherent across README, the example, tooling.md, blueprint and the shipped .gitignore | yes | open | +| `review/release/29` | V3 | `.claude/settings.json:63` | enabledPlugins uses bare plugin names; Claude Code keys are name@marketplace, so the shipped entries match no installed plugin | yes | open | +| `review/release/30` | V10 | `README.md:379` | README tells adopters to edit finish.md for their task runner and STANDARDS 'Step 4', but finish.md no longer contains either, and editing it breaks the sync's byte-identity | yes | open | +| `review/portability/31` | V9 | `.claude/rules/cbk-conventions-reference.md:397` | A sibling project's name and issue/PR numbers ship in a reference half that targets copy verbatim | yes | open | +| `review/portability/32` | V9 | `.claude/rules/cbk-conventions-reference.md:476` | Kit content cites the kit's own issue numbers as bare #N (29 places), which autolink to a target's unrelated issues | yes | open | +| `review/portability/33` | V9 | `.claude/skills/blueprint/references/methodology-selection.md:7` | Blueprint tells the agent to read `methodology_register.md` and consultation cites `sdd.md`, neither of which the kit ships | yes | open | +| `review/portability/34` | V9 | `.claude/skills/scaffold/references/backends.md:92` | 'Board-automation gap (as of current cascade version)' is an undated rail in four byte-identical copies with no drift guard, next to uncited Projects v2 automation claims | yes | open | +| `review/portability/35` | V4 | `.claude/skills/blueprint/references/templates/claude-review.yml:160` | 'track_progress is unsupported on `labeled` events (the action fails)' is undated and contradicted by the action's current source | yes | open | +| `review/portability/36` | V9 | `.claude/rules/cbk-conventions.md:126` | The CI-skip trap is stated without a source, names one marker literal, and overstates the events it applies to | yes | open | +| `review/portability/37` | V9 | `.claude/skills/scaffold/references/manual_steps.md:13` | Linear free-tier claim is stale: it is unlimited members and a 250-issue cap, not 'up to 10 users' | yes | open | +| `review/portability/38` | V9 | `.claude/rules/knowledge-backend.md:254` | Notion platform claims are unsourced, and the mandated Verification property is plan-gated though the file says plans are 'not the kit's concern' | yes | open | +| `review/consistency/39` | V9 | `.claude/skills/scaffold/references/github_only_profile.md:22` | Scaffold's label taxonomy omits three labels that templates and commit flows apply | no | open | +| `review/consistency/40` | V3 | `.claude/commands/finish-procedure.md:166` | Bundled executor cites STANDARDS.md sections the kit's own template never emits | no | open | +| `review/consistency/41` | V9 | `.claude/skills/scaffold/references/github_only_profile.md:11` | Detection states are numbered three ways: a duplicated empty State 1 heading, and a 'four states' intro | no | open | +| `review/consistency/42` | V9 | `.claude/skills/rough-in/references/test_cases.md:24` | Rough-in test case expects 3-7 R-issues; the contract, SKILL and README say 2-6 | no | open | +| `review/consistency/43` | V9 | `.claude/rules/cbk-conventions-reference.md:365` | Conventions claim framing's phase-exit checklist carries the 'restamp unexercised calls' item; it does not | no | open | +| `review/consistency/44` | V7 | `.claude/rules/pr-review.md:94` | 'Four-class' rubric has five rows, and ADR conflicts land in both Defer and Reject | no | open | +| `review/consistency/45` | V7 | `.claude/rules/pr-review.md:146` | Break-glass documents a `/finish --skip-review` flag the executor does not parse | no | open | +| `review/consistency/46` | V9 | `.claude/skills/scaffold/SKILL.md:21` | Scaffold says five HITL gates in one place and six in another | no | open | +| `review/consistency/47` | V9 | `.claude/skills/framing/references/templates/frame-output-template.md:292` | Frame template cites a framing planning-backend-commit section that exists only in rough-in's file | no | open | +| `review/consistency/48` | V9 | `.claude/rules/cbk-conventions-reference.md:394` | § Hook authoring's tally of hooks and of headers that carry which lines does not match the hooks | no | open | +| `review/consistency/49` | V9 | `.claude/rules/cbk-conventions-reference.md:17` | Each reference half claims a pointer heading for every section it holds; eight sections have none | no | open | +| `review/consistency/50` | V9 | `.claude/skills/rough-in/references/planning-backend-matrix.md:107` | Rough-in says scaffold commits the issue templates on every planning axis; scaffold skips them on in-repo-markdown | no | open | +| `review/consistency/51` | V9 | `.claude/skills/blueprint/SKILL.md:178` | Blueprint says six foundation docs and six gate iterations; its own table lists seven and blueprint.md is counted twice | no | open | +| `review/claude-code/52` | V3 | `.claude/commands/pr-respond.md:43` | /pr-respond triages against pr-review.md sections that moved to a path-scoped file which a triage never loads | no | open | +| `review/claude-code/53` | V7 | `.claude/rules/pr-review-reference.md:87` | Reference file lists 'running review-toolkit on a docs-only PR' as an anti-pattern and says to skip it, contradicting the two-skill floor | no | open | +| `review/claude-code/54` | V9 | `.claude/skills/rough-in/references/procedure.md:120` | Foreign-project identifiers and the kit's own issue numbers ship inside portable skills, commands and templates | no | open | +| `review/claude-code/55` | V9 | `.claude/skills/scaffold/references/github_only_profile.md:89` | Scaffold's light-mode reference says to skip the issue templates that SKILL.md says can never be skipped | no | open | +| `review/claude-code/56` | V9 | `.claude/skills/blueprint/references/templates/tooling.md:71` | Blueprint's CI skeleton, copied into every generated workflow, pins actions to a mutable tag against the kit's own SHA-pin rule / CI skeleton pins actions/checkout by mutable tag, against the kit's o… | no | open | +| `review/claude-code/57` | V9 | `.claude/rules/knowledge-backend.md:1` | 1,335 lines of always-loaded rules against a documented 200-line target; the largest (knowledge-backend.md, 308 lines) is unsplit | no | open | +| `review/claude-code/58` | V3 | `.claude/agents/logging-discipline-reviewer.md:77` | Reviewer flags per-tick debug logging in hot-path loops that logging.md explicitly permits | no | open | +| `review/claude-code/59` | V3 | `.claude/agents/cascade-rule-reviewer.md:45` | Reviewer cites a knowledge-backend.md section that does not exist | no | open | +| `review/claude-code/60` | V3 | `.claude/rules/simplification.md:20` | Always-loaded rule attributes unsourced behaviors to the bundled /simplify skill, and workflows.md describes the file as something it is not | no | open | +| `review/release/61` | V10 | `README.md:50` | README never says how to start a phase: four of the five phase skills are disable-model-invocation and cannot be triggered by describing the intent | no | open | +| `review/release/62` | V8 | `.gitignore:1` | Kit .gitignore carries an unanchored, unexplained `reference/` entry that violates the kit's own anchoring rule and is installed into targets / Kit .gitignore has an unanchored, unmarked `reference/`… | no | open | +| `review/release/63` | V3 | `.claude/agents/Explore.md:5` | Explore.md is described as an 'exemplar' but is a live override that pins the built-in Explore agent to Haiku in every adopting project | no | open | +| `review/release/64` | V10 | `LICENSE:1` | No copyright holder or NOTICE for a kit meant to be copied into other projects; the adopter's licence guidance and the install path collide with Apache-2.0 attribution | no | open | +| `review/release/65` | V10 | `CLAUDE.md:7` | CLAUDE.md says the kit has no code or test suite and that verification is editorial, but the tree ships hooks, JS and Python with fixtures and a required CI job | no | open | +| `review/portability/66` | V9 | `.github/dependabot.yml.example:10` | A platform guarantee is cited with an elided URL ('docs.github.com … dependabot-options-reference') | no | open | +| `review/portability/67` | V9 | `.claude/rules/cbk-conventions-reference.md:599` | The block's price-table diff uses GNU-only `\L` in sed, so it fails on macOS/BSD and busybox | no | open | +| `review/portability/68` | V9 | `.claude/skills/scaffold/references/bootstrap_checklist_template.md:77` | Rule-file disposition pass omits the sixth placeholder-carrying rule, cbk-conventions-reference.md | no | open | + +371 rows. + From 1fee19b341c59b1f42f1efe2a423eb2bfee5a7d2 Mon Sep 17 00:00:00 2001 From: Justin Forth Date: Wed, 30 Sep 2026 10:32:09 -0600 Subject: [PATCH 002/135] =?UTF-8?q?docs:=20harvest=205=20implementation=20?= =?UTF-8?q?plan=20=E2=80=94=20v1.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A master plan (constraints, review focus, ownership map, baseline, and the closing tasks F1-F5: budget and sanitization gate, the review floor once, the trace audit, the draft PR, and the post-merge tags and release) and ten cluster plans, V1-V10, 94 tasks in commit order. Every ported hunk is re-authored inline; every trace row has a home; each cluster file was dry-run on a scratch copy of the kit and reconciled against its neighbours. Co-Authored-By: Claude Opus 5.5 --- .../plans/2026-09-30-harvest-5-v1.0.0.md | 343 ++ .../plans/2026-09-30-harvest-5/V1-gate.md | 1256 ++++++ .../plans/2026-09-30-harvest-5/V10-release.md | 1881 +++++++++ .../plans/2026-09-30-harvest-5/V2-guards.md | 2839 +++++++++++++ .../2026-09-30-harvest-5/V3-conformance.md | 1520 +++++++ .../2026-09-30-harvest-5/V4-review-bot.md | 2372 +++++++++++ .../2026-09-30-harvest-5/V5-recalibration.md | 2602 ++++++++++++ .../plans/2026-09-30-harvest-5/V6-harness.md | 2623 ++++++++++++ .../V7-review-conventions.md | 1987 ++++++++++ .../plans/2026-09-30-harvest-5/V8-hygiene.md | 1190 ++++++ .../2026-09-30-harvest-5/V9-consistency.md | 3515 +++++++++++++++++ 11 files changed, 22128 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-30-harvest-5-v1.0.0.md create mode 100644 docs/superpowers/plans/2026-09-30-harvest-5/V1-gate.md create mode 100644 docs/superpowers/plans/2026-09-30-harvest-5/V10-release.md create mode 100644 docs/superpowers/plans/2026-09-30-harvest-5/V2-guards.md create mode 100644 docs/superpowers/plans/2026-09-30-harvest-5/V3-conformance.md create mode 100644 docs/superpowers/plans/2026-09-30-harvest-5/V4-review-bot.md create mode 100644 docs/superpowers/plans/2026-09-30-harvest-5/V5-recalibration.md create mode 100644 docs/superpowers/plans/2026-09-30-harvest-5/V6-harness.md create mode 100644 docs/superpowers/plans/2026-09-30-harvest-5/V7-review-conventions.md create mode 100644 docs/superpowers/plans/2026-09-30-harvest-5/V8-hygiene.md create mode 100644 docs/superpowers/plans/2026-09-30-harvest-5/V9-consistency.md diff --git a/docs/superpowers/plans/2026-09-30-harvest-5-v1.0.0.md b/docs/superpowers/plans/2026-09-30-harvest-5-v1.0.0.md new file mode 100644 index 0000000..9a20d7d --- /dev/null +++ b/docs/superpowers/plans/2026-09-30-harvest-5-v1.0.0.md @@ -0,0 +1,343 @@ +# Harvest 5 — v1.0.0 — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Land, as one PR whose verification block is green after every commit, the sixteen open +issues (#58's residue, #60–#74) and the whole-kit review's findings. Then release the kit as +v1.0.0, back-tagging the five earlier harvests, with a CHANGELOG that carries each release's sync +notes. + +**Architecture:** This master file carries the constraints, the ownership map, the baseline +(Task 0) and the closing tasks (F1–F5). The work itself is ten cluster files under +`docs/superpowers/plans/2026-09-30-harvest-5/`, executed in order V1 → V10. Each cluster task is +one atomic commit that names the trace ids it closes. Every fixture lands in the commit whose fix +it pins, having first been run red against the unfixed file. + +**Tech Stack:** +- Markdown (skills, rules, commands, templates). +- bash (hooks, the path helper, fixtures, the verification block). +- JSON (`settings.json`, `.mcp.json.example`). +- JavaScript (workflow scripts and `.mjs` stub harnesses). +- Python 3 (`agent-cost.py`, `run-arms-headless.py`, `review-trigger-fixture.py`). +- YAML (review-workflow templates, the kit's CI). + +Tools the block and fixtures need: `bash`, `git`, `jq`, `node`, `python3`. + +**Spec:** `docs/superpowers/specs/2026-09-30-cascade-kit-harvest-5-design.md`. Read it in full +before Task 0. The trace is `docs/superpowers/specs/2026-09-30-harvest-5-trace.md`, one row per +ask. Every task names the rows it closes, the spec cluster (V1–V10) and the decisions (D41–D62) +it implements. + +## Global Constraints + +- **The private target is never named** in any committed file, this plan included: not its name, + its paths, its ticket keys, its commit shas or its stack. A hunk ported from it is re-authored + generic. The two public targets (`j4th/you-are-hear`, `j4th/echosphere`) are named in the spec, + the trace and these plans, never in shipped kit content (`.claude/`, `.github/`, `README.md`, + `CLAUDE.md`). +- **Re-author, never merge raw.** A target's file carries that project's prose. Port the change + onto the kit's text; never copy a target's file over the kit's. +- **The block is the gate after every commit.** Run + `bash .claude/workflows/tests/run-verification-block.sh`. It extracts the block from + `.claude/rules/cbk-conventions-reference.md` § Verification on every run, so it is never a stale + copy. It must print `verification: kit sub-block complete` and `verification: done`, and exit 0. +- **Read the runner by content, never by position.** From V1.1 on, the runner's last lines include + `run-verification-block-fixture` output, and each cluster adds fixture lines before the kit + sentinel. So a step judges a run by `exit=0` and the presence of the lines it names: + `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | grep -E 'always-loaded total|WARN|VIOLATION|verification:'; echo "exit=${PIPESTATUS[0]}"`. + A cluster step that still shows `| tail -N` is read the same way: the lines it expects must be + present, and `exit` must match. +- **New checks append at the sentinels.** A kit-sub-block check goes on the line(s) immediately + before `echo "verification: kit sub-block complete"`. A project-sub-block check goes immediately + before ` echo "verification: project sub-block complete"`, which is indented two spaces. +- **Check syntax.** + - A must-be-absent check uses `absent` with a bracket-split literal, e.g. `absent grep -rn "opinionate[d] profile" …`. + - A positive chain is wrapped `{ …; } || { echo ""; exit 1; }`. + - Every check prints a message that names what failed. +- **Hooks.** + - `bash -n` on every hook after editing it. + - `set -uo pipefail`, never `set -e`. + - `input="$(cat)"` is the first statement after `set -uo pipefail`. + - No pipeline decides on a reader that can exit first: use here-strings. + - No bash-4-only builtins (`mapfile`, `readarray`, `declare -A`); a stock macOS bash is 3.2. + - Every header carries `Tier:` and `Depends:`. +- **Fixtures.** They need only `bash`, `git`, `jq`, plus `node` or `python3` for their own + language. They run in throwaway trees under `mktemp -d`, never in the real checkout. A case that + needs a tool the host lacks (`unshare`, GNU `stat`) prints `SKIP: ( unavailable)` + and does not fail; a silent skip is a defect. Each fixture exits non-zero on any failed case, + and prints the case name. +- **Red first, inside the commit.** A new or extended fixture is written first and run against the + unfixed file, where it must FAIL with the named case. Record that output line in the PR-body + "red-first" table. Then make the fix, the fixture goes green, and both are committed together. +- **Byte-parallel copies stay byte-parallel.** The block diffs each pair: + - `.claude/commands/finish.md` ↔ `.claude/skills/rough-in/references/finish-command.md` below its + `--- BEGIN TEMPLATE ---` marker; + - `.claude/commands/finish-procedure.md` ↔ `.claude/skills/rough-in/references/finish-procedure.md`; + - `docs/adr/` ↔ `.claude/skills/scaffold/references/adr-starters/`; + - the four `references/backends.md` copies (after V9 adds that diff). +- **Commits.** Branch `feat/harvest-5-v1.0.0` (it exists; the spec is its first commit). One + commit per task. Conventional Commits, the subject naming the cluster and the body listing the + trace ids closed. Trailer `Co-Authored-By: Claude Opus 5.5 `. No CI-skip + marker on any commit. +- **Platform claims.** Every quotation or behaviour claim about Claude Code, GitHub, npm, Anthropic, + Linear, Notion or Dependabot written into kit text is fetched raw (`curl -sL `, or the page's + `.md` form on `code.claude.com`) and matched with `grep -F` on the day it is written. It carries + `, read ` beside it. Typographic apostrophes are normalised before matching. + Nothing from memory. +- **Kit issues are cited as `context-builder-kit#N`** or not at all (D53). A bare `#58` in shipped + content links to a target's own #58. +- **Size and budget.** + - Every `SKILL.md` stays under 500 lines; the block enforces it. + - Always-loaded rules target ≤ **130,628** bytes (D59). The block prints a non-failing `WARN` above + **140,000** (D50). +- **Anchor edits on text, never on line numbers.** Line numbers in these plans are "at `74edf84`", + for orientation only. Every edit is an exact-string replacement anchored on text that no other + cluster's task changes (§ Ownership map). If an anchor is missing when a task runs, stop and + reconcile; never guess. + +## Review Focus + +Five inputs the spec implies but no cluster's own tests would reach. Each is pinned by a test in +the task named. + +1. **A target syncing v1.0.0 onto filled copies.** A three-way `git merge-file` from the version it + installed should conflict only in files the v1.0.0 Sync notes name. → **V10**, the simulated-sync + task. It merges echosphere's public `.claude/` from base `74edf84` to the branch head, and asserts + every conflicting file is named in the CHANGELOG's Sync notes. +2. **A project path that contains a space.** Every hard-deny guard still denies when the project + root is `…/a b/…`. → **V3** asserts every registration is exec form (`args` present). **V2**'s + `protected-paths-hook-fixture.sh` and `hook-guards-fixture.sh` each run one case from a root + containing a space. +3. **The knowledge axis set to `none`.** Deleting both knowledge-backend rule halves, their hook + and its settings stanza leaves the block green, and `hook-guards-fixture.sh` skips the deleted + hook by name. → **V5**, the D59 split task, dry-runs it in a scratch copy of the tree. +4. **A freshly scaffolded target.** With `docs/cbk/scaffold.md` present and the disposition pass's + globs stamped, the runner prints all three sentinels. An unstamped + `cbk-conventions-reference.md` glob turns it red and names the file. → **V1**, the fresh-scaffold + task. +5. **`/finish 42 --skip-review`.** The named argument receives `42`, and the break-glass flag is + honoured by the step that reads it, or else its documentation in `pr-review.md` is corrected to + the operator-instruction form. → **V3**, the commands task. + +## Ownership map + +A file on the left is edited **only** by the tasks of its owner. A **shared** file is edited by +region: each cluster touches only the sections named for it, anchored on text inside those +sections. When an item's change spans a file another cluster owns, that part is **handed** to the +owner. The owner lands it and names the item's trace id in its commit. + +| File or region | Owner | +|---|---| +| `.claude/hooks/*.sh`, `.claude/hooks/lib/`, `.github/workflows/adr-immutability-check.yml`; `tests/hook-*.sh`, `tests/protected-paths-hook-fixture.sh`, `tests/adr-ci-body-fixture.sh`, `tests/extract-run-block.sh` | V2 | +| except `format-on-edit.sh`, `analyze-on-edit.sh` (the advisory exemplars) | V8 | +| `.claude/settings.json` (exec form, `enabledPlugins`, `_comment_hooks`, #66 item 2) | V3 | +| `.claude/commands/intake.md`, `enrich.md`; frontmatter and arguments of every command | V3 | +| `pr-respond.md` body (#64) | V7 | +| `finish.md`, `finish-procedure.md` and their bundled copies below the frontmatter (citations, R2, the capstone sentence, D54) | V9 | +| `.claude/rules/orchestration.md`, `orchestration-reference.md`, `knowledge-backend.md` and the new `knowledge-backend-reference.md` | V5 | +| `.claude/workflows/review-sweep.js`, `tests/review-sweep-accounting.mjs` | V7 | +| `.claude/workflows/finish-ab/**`, `agent-cost.py`, `tests/agent-cost-fixture.sh`, `tests/finish-ab-shape.mjs`, `tests/run-arms-headless-fixture.sh`, `tests/load-workflow*.mjs` | V6 | +| `tests/run-verification-block.sh`, `tests/run-verification-block-fixture.sh`, `.github/workflows/verify.yml`; the **structure** of § Verification (preamble, Run it, the Stop-hook check, the budget line) | V1 | +| `blueprint/references/templates/claude-review.yml`, `claude.yml`, `tests/review-assert-fixture.sh`, `tests/review-trigger-fixture.py` | V4 | +| `.mcp.json.example`, `.gitignore`, `.github/dependabot.yml.example` | V8 | +| `README.md`, `CHANGELOG.md`, `CLAUDE.md`, `LICENSE` | V10 | +| `.claude/skills/adr-new/SKILL.md`; `docs/superpowers/plans/2026-09-21-harvest-4-applied-defects.md` (D62) | V9 | +| **shared** `cbk-conventions-reference.md` | § Verification structure: V1 · § Hook authoring, § Required-checks trap: V2 · § Dependency settle-window, § .gitignore anchoring: V8 · § ADR relation grains and remaining citations: V9 · § Syncing the kit: V10, including #71's formatter-scope clause, handed from V8. Block checks: every cluster, appended at the sentinel. | +| **shared** `cbk-conventions.md` | § Rule loading: V5 · § Mutation discipline: V2 · § Branch naming, Quick reference, citations: V9 · § Syncing the kit pointer: V10 | +| **shared** `pr-review.md` | § The floor's Agent-tool sentence: V5 · the rest (§ The floor › Once, the sweep, invariants, craft rules): V7 | +| **shared** `.claude/rules/tooling.md` | § MCP configuration's merge rule: V3 · § Automated review: V4 | +| **shared** blueprint `templates/tooling.md` | minimum task set, light mode: V1 · CI skeleton, review-bot paragraph: V4 · mise, sanity 6b: V8 | +| **shared** `github-starter-templates.md` | starter CI stub: V4 · § `.gitignore` — the harness block, the Dependabot "not covered" line: V8 | +| **shared** `bootstrap_checklist_template.md` | verification-matrix run row, the reference-glob disposition row: V1 · corpus rows, backstop slots: V2 · the Explore-override one-time choice: V3 · knowledge-axis-none disposition: V5 · formatter-scope and harness rows: V8 · Kit commit row: V10 | +| **shared** `.claude/skills/rough-in/references/research-phase.md`, `test_cases.md`, `contract.md` | grounding rule 4: V5 · #74 item 2 and its criterion: V7 | +| everything else | the cluster whose items name it | + +**Bare kit-issue citations (D53):** +- The owner of a file rewrites every bare `#N` in it when it first edits that file. +- V9 rewrites every remaining bare citation in files no cluster otherwise edits. +- V9 lands the block check that keeps them out. +- V10's files are checked by V10. + +## Decisions and observations taken while planning (for the PR body) + +- **P1, observed live on 2026-09-30 (Claude Code 2.1.285).** The Bash tool ran + `cd docs/superpowers/plans/2026-09-30-harvest-5`. A Workflow launch from the same session was + then **blocked** by `require-repo-root-for-agents.sh`, which named that subdirectory as the launch + directory. A hook's payload `cwd` follows the Bash tool's `cd`, as the hooks page now says + ("`cwd` follows Claude … after Claude runs `cd`"). Harvest 3 recorded the opposite (2026-09-07). + V2's probe-P1 step still runs its own controlled probe. This observation is the first data point, + and it means the guard now fires on a `cd` into a subdirectory, not only on a session launched + from one. The guard's header and the conventions' account of its trigger change accordingly + (V2). +- **V1.4 keys the kit-sub-block `CLAUDE.md` check on `docs/cbk/blueprint.md` in a target.** A + target before blueprint has no `CLAUDE.md` yet, since blueprint writes both files in the same + phase, and the check made a fresh scaffold red forever. The residual: a brownfield target with a + pre-existing `CLAUDE.md` goes unchecked until blueprint. That makes the check less strict, but it + never produces a false red. +- **D61's breadth, stated for the operator.** A payload jq cannot parse makes + `protect-main-branch.sh` refuse *every* Bash call carrying it, not only a commit, and makes + `require-repo-root-for-agents.sh` refuse that dispatch. Such a payload is abnormal (a lone UTF-16 + surrogate is the known case). A refusal names the cause, and the operator's route is to retry + without the unreadable text. +- **V7 reads the floor strictly:** a docs-only PR may skip the sweep, never the floor. That is + consistent with § The floor, where the only waiver is break-glass. It corrects the reference + half's docs-only anti-pattern. +- **`/pr-respond`'s author filter** trusts the PR's author. On a fork PR, that contributor's own + comments can still reach Apply. The marginal risk is low, because they wrote the PR's code. + Recorded, not changed. +- **Left for F2's triage:** scaffold and blueprint prose that still describes provisioning "via + GitHub MCP" after D58 drops the GitHub MCP entry from `.mcp.json.example`; and + `rough-in/references/handoff-to-finish.md:3`'s stale "bootstrap-finish" phrase. + +## Task 0: Baseline + +**Files:** none changed. + +- [ ] **Step 1: Confirm the branch, the head and a clean tree** + +Run: `cd /home/j4th/Code/create/context-builder-kit && git branch --show-current && git log --oneline -2 && git status --short` +Expected: `feat/harvest-5-v1.0.0`. The head is the plan commit, and before it `643f7ff docs: harvest 5 design spec and trace — v1.0.0`. The tree is clean. + +- [ ] **Step 2: Run the block through the runner and record the always-loaded total** + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | grep -E 'always-loaded total|WARN|VIOLATION|verification:'; echo "exit=${PIPESTATUS[0]}"` +Expected: `always-loaded total: 130628 bytes`, `verification: kit sub-block complete`, +`verification: done`, `exit=0`, and no `WARN` or `VIOLATION` line. Record `130628` as the baseline +for the PR body. + +- [ ] **Step 3: Record the interpreter context (#68 item 3)** + +Run: `bash -c 'type -a grep; grep --version | head -1'` +Expected: `/usr/bin/grep` (GNU). The block runs in a child bash; an interactive shell's `grep` +function does not reach it. Record the line for the PR body. + +## The clusters — execute in this order + +| Order | File | Closes | Tasks (94 in all) | +|---|---|---|---| +| 1 | `2026-09-30-harvest-5/V1-gate.md` | #73, #58 R4 R5; the block's structure | V1.1–V1.7 (7) | +| 2 | `2026-09-30-harvest-5/V2-guards.md` | #60, #61, #62, #58 R1 R6 R8 R13; D61; probes P1, P4 | V2.1–V2.9 (9) | +| 3 | `2026-09-30-harvest-5/V3-conformance.md` | the review's Claude Code findings; D57; #66 item 2 | V3.1–V3.7 (7) | +| 4 | `2026-09-30-harvest-5/V4-review-bot.md` | #67, #68 §2, #70 §1–3, #58 R12; probe P3 | V4.1–V4.6 (6) | +| 5 | `2026-09-30-harvest-5/V5-recalibration.md` | #69 (rules), #65, #74 §3, #68 §3, #58 R9, D59; probe P2 | V5.1–V5.14 (14) | +| 6 | `2026-09-30-harvest-5/V6-harness.md` | #69 (harness) | V6.1–V6.4 (4) | +| 7 | `2026-09-30-harvest-5/V7-review-conventions.md` | #72, #74 §1–2, #58 B R10, #64 | V7.1–V7.15 (15) | +| 8 | `2026-09-30-harvest-5/V8-hygiene.md` | #70 §4–6, #71, #58 R7, D58; probe P5 | V8.1–V8.5 (5) | +| 9 | `2026-09-30-harvest-5/V9-consistency.md` | #63, #66, #68 §1, #58 R2 R3, D53, D54, D62; the review's consistency findings | V9.1–V9.22 (22) | +| 10 | `2026-09-30-harvest-5/V10-release.md` | D41, D60, R14; the release-lens findings | V10.1–V10.5 (5) | + +## Task F1: The whole-branch budget and sanitization gate + +**Files:** none changed, unless a gate fails. + +- [ ] **Step 1: Always-loaded total** + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | grep -E 'always-loaded total|WARN'` +Expected: `always-loaded total: N bytes` with N ≤ 130628, and no `WARN` line. If N > 130628, +record N and the reason for the PR body (DoD 5). Do not cut operative text to hit the number. + +- [ ] **Step 1b: Review Focus 4 at the branch head.** Recreate V1.4's `fresh_scaffold` helper from + V1.4's own step (it is not committed). Run it with the disposition pass filled; it must print all + three sentinels. Then run it with one unstamped glob; it must go red and name the file. V2 and + V5 extended the helper for their slots, so use the helper as V5 last left it. + +- [ ] **Step 2: The private-target denylist over the whole tree, plans and specs included** + +Run: `git grep -n -i -E '\bcr[e]ase\b|CR[E]-[0-9]' -- . ; echo "hits=$?"` +Expected: `hits=1`, meaning no match. + +- [ ] **Step 3: No sibling name in shipped content, and no bare kit-issue citation** + +Run: `git grep -n -E 'you-are-he[a]r|echospher[e]' -- .claude .github README.md CLAUDE.md; echo "names=$?"` +Expected: `names=1`. The bare-citation check is in the block (V9), so Step 1's green run covers it. + +## Task F2: The review floor, once + +**Files:** whatever the triage applies, one commit per applied finding. + +- [ ] **Step 1: `/simplify`** — invoke the skill as a skill. Record its agents, findings and applied + count. +- [ ] **Step 2: `pr-review-toolkit:review-pr`** — invoke the skill with no arguments. Record its + agents and findings. +- [ ] **Step 3: `review-sweep` beside it.** Launch the workflow from the repository root, with the + changed files and no caller-named finders beyond the project reviewers. The diff is broad, so add + at most three finders by name, with prompts (the V7 finder shape): hook path resolution, + review-template YAML, rule-text accuracy against live docs. Keep the defaults of 3 per dimension + and 8 verified. +- [ ] **Step 4: Triage every finding with the four-class rubric** (`.claude/rules/pr-review.md`). + Apply each Apply or Apply-with-care finding as its own commit. Re-run the runner after each. +- [ ] **Step 5: Write the `## Review gate` and `## Triage` blocks** for the PR body, exactly in the + shape `pr-review.md` § The floor gives. Record any effort the toolkit agents ran at the session's + level (V5's #74 §3 rule). + +## Task F3: The trace audit (R14, constraint 6) + +**Files:** +- Modify: `docs/superpowers/specs/2026-09-30-harvest-5-trace.md` (the Status column) + +- [ ] **Step 1: Set every row's Status.** Use `landed ` from the commit that names the + row, `non-goal` (spec § Non-goals), or `not holding` (D56, with the one-line reason). + +Run: `grep -c '| open |$' docs/superpowers/specs/2026-09-30-harvest-5-trace.md` +Expected after this step: `0`. + +- [ ] **Step 2: A refute-by-default audit of every row not marked `landed`.** Dispatch one + fresh-context verifier, workhorse tier at effort `high`, read-only. Give it the trace path and the + branch, and tell it to try to show that each `non-goal` or `not holding` row was in fact + required and is still live at the branch head. Correct any row it refutes by landing the item. +- [ ] **Step 3: Commit** `docs: harvest 5 trace — every row closed`. + +## Task F4: The draft PR + +- [ ] **Step 1: Push** — `git push -u origin feat/harvest-5-v1.0.0`. +- [ ] **Step 2: Open the PR as draft.** + + Title: `feat: harvest 5 — v1.0.0: guards that resolve paths, the block as a gate, review bots on aliases, the Opus 5.5 recalibration, and the first release`. + + The body carries, in this order: + - `Closes #58`, `Closes #60` … `Closes #74`, one per line; + - a summary by cluster; + - the decisions D41–D62; + - the always-loaded before and after; + - the red-first table; + - the probe results P1–P6; + - `## Review gate`; + - `## Triage`; + - the trace summary (counts per status); + - the "Sync notes" preview from `CHANGELOG.md`. + + End with the line `🤖 Generated with [Claude Code](https://claude.com/claude-code)`. +- [ ] **Step 3: Confirm the branch ends on a commit without a CI-skip marker**, and that `verify.yml` + and `ADR immutability` are green on the PR. +- [ ] **Step 4: Fix the CHANGELOG's two predictions.** Set the `[1.0.0]` heading's PR number to the + real one, and re-date it to the merge's UTC date after merge (the back-dated headings already use + each merge's UTC date, matching GitHub's `mergedAt`). Commit each fix as + `docs(changelog): v1.0.0 heading — PR number` and, after merge on `main` via a follow-up PR if it + moved, `… — merge date`. +- [ ] **Step 5: Record P4.** Read the `verify.yml` run's log on the PR. Record whether the + bind-mount cases ran or printed `SKIP`, and write that line into the dated observation V2.9 + planted. + +## Task F5: The release, after merge and only with the operator's go-ahead + +- [ ] **Step 1: Confirm the squash-merge sha** + (`gh pr view --json mergeCommit -q .mergeCommit.oid`) and pull `main`. +- [ ] **Step 2: Create the annotated tags.** Each message is the CHANGELOG heading for its version. + +```bash +git tag -a v0.1.0 db0eac7 -m "v0.1.0 — harvest 1 (PR #4) and everything before it" +git tag -a v0.2.0 6560f72 -m "v0.2.0 — harvest 2 (PR #5)" +git tag -a v0.3.0 88b1ede -m "v0.3.0 — axis parity (PR #7)" +git tag -a v0.4.0 e92e9c4 -m "v0.4.0 — harvest 3 (PRs #54–#57)" +git tag -a v0.5.0 74edf84 -m "v0.5.0 — harvest 4 (PR #59)" +git tag -a v1.0.0 -m "v1.0.0 — harvest 5 (PR #)" +``` + +- [ ] **Step 3: Show the operator the six tags** (`git show --no-patch v0.1.0 … v1.0.0`) and wait for + an explicit go-ahead. Then `git push origin v0.1.0 v0.2.0 v0.3.0 v0.4.0 v0.5.0 v1.0.0`. +- [ ] **Step 4: Publish the release.** + `gh release create v1.0.0 --title "v1.0.0" --notes-file <(awk '/^## \[1\.0\.0\]/{p=1;next} /^## \[/{p=0} p' CHANGELOG.md)`. + Do not create GitHub Releases for the back-tags; they are named sync points. +- [ ] **Step 5: Resolve the README's install command against the real tag.** Run it into a scratch + directory and check that only the drop-in set lands (V10's test, now against GitHub). Report the + release URL. diff --git a/docs/superpowers/plans/2026-09-30-harvest-5/V1-gate.md b/docs/superpowers/plans/2026-09-30-harvest-5/V1-gate.md new file mode 100644 index 0000000..517fd76 --- /dev/null +++ b/docs/superpowers/plans/2026-09-30-harvest-5/V1-gate.md @@ -0,0 +1,1256 @@ +# Harvest 5 — V1: The block as a gate + +**Scope.** This cluster makes the verification block a gate that cannot pass on nothing, and wires it into +every target's `check`. It closes the trace rows `#73/body/1`, `#73/body/2` (the block's half; the hook's +half is V2's), `#58/c5901493591/R4`, `#58/c5901493591/R5` and `critic/22`. It implements D51 (the block as a +gate), D50 (a non-failing warning above 140,000 always-loaded bytes) and D52's clause for the kit's own +`verify.yml`. It also lands two things the gate cannot be true without. The first is the scaffold +disposition row for `cbk-conventions-reference.md`'s manifest-and-lockfile glob: the spec's settled call under +**Gate**, and the same defect as V9's unverified review row `review/portability/68`. The second is a repair the +planning dry run found. A freshly scaffolded target has no `CLAUDE.md` until blueprint writes it, and the kit +sub-block's `CLAUDE.md` check is red there before the glob check is ever reached; the check now keys on +`docs/cbk/blueprint.md` in a target. V1 runs first and consumes nothing from earlier clusters. Every later +cluster's commit is gated by the runner this cluster strengthens. Review Focus 4 (a freshly scaffolded +target) is pinned in Task V1.4. + +## Before you start + +- Work from the repository root on `feat/harvest-5-v1.0.0`. Line numbers below are "at `643f7ff`", for + orientation only; every edit is an exact-string replacement anchored on text. +- **Replacement format.** "**Replace** in ``" is followed by two fenced blocks, the existing text and its + replacement. A block marked *(whole lines)* replaces complete lines; *(inline)* replaces a span inside one line. + Use the Edit tool with the first block as `old_string` and the second as `new_string`. If an anchor is not + found exactly once, stop and reconcile; never guess. +- The gate after every commit: `bash .claude/workflows/tests/run-verification-block.sh`. It must end with + `verification: kit sub-block complete` and `verification: done`, and exit 0. +- **Planning dry run.** Every step below was executed on a scratch clone at `643f7ff` on 2026-09-30, in task + order, and each "Expected" block is the observed output. The red lines go into the PR body's red-first + table. +- New kit-sub-block checks go immediately before `echo "verification: kit sub-block complete"` (the master + plan's sentinel rule). The tasks below therefore stack their checks in task order after the budget line. +- No hook is touched in this cluster. `bash -n` runs on the two scripts it edits. + +--- + +### Task V1.1: The runner's third rail and its fixture (#73/body/1, critic/22; D51) + +**Files:** +- Create: `.claude/workflows/tests/run-verification-block-fixture.sh` (mode 755) +- Modify: `.claude/workflows/tests/run-verification-block.sh`, the header comment (lines 2–7) and after the + done-sentinel check (line 20) +- Modify: `.claude/rules/cbk-conventions-reference.md`, § Verification › Run it (line 476) and the kit + sub-block at its sentinel (line 690) + +**Interfaces:** +- Consumes: the runner at `643f7ff` (two rails) and the block's sentinel strings `verification: kit sub-block + complete`, `verification: project sub-block complete`, `verification: done`. +- Produces: the runner's third rail. When `docs/cbk/scaffold.md` exists and the output lacks the project + sentinel, it prints `verification: a filled target's project sub-block never completed — …` and exits 1. +- Produces: `.claude/workflows/tests/run-verification-block-fixture.sh`. It prints one `ok: ` line per case + and ends `run-verification-block-fixture: 6 cases ok`; on a failed case it prints `FAIL: (…)` and exits 1. +- Produces: the kit-sub-block line `bash .claude/workflows/tests/run-verification-block-fixture.sh || { echo + "run-verification-block.sh lost a rail (the fixture names the case)"; exit 1; }`, so the block guards the + script that runs it. +- Produces: § Verification › Run it says "three fail-loud rails" and names the residual (a deleted or renamed + scaffold file). Task V1.5 extends the same paragraph. + +- [ ] **Step 1: Write the fixture** + +**Create** `.claude/workflows/tests/run-verification-block-fixture.sh` with this content, then +`chmod +x .claude/workflows/tests/run-verification-block-fixture.sh`: + +````bash +#!/usr/bin/env bash +# Fixture for the verification-block runner (run-verification-block.sh): its fail-loud rails, driven +# against synthetic blocks in throwaway checkouts. The runner is copied in at its real relative path, +# so the runner tested is the runner the gate runs, and the block runs this fixture (the block guards +# the script that runs it). +# green: a kit tree (no docs/cbk/scaffold.md) that prints `verification: done`; a filled target that +# prints the project sentinel and `verification: done`. +# red: a filled target whose project sub-block never ran (only `verification: done` printed: a guard +# that stops firing turns every project check off with the gate green, context-builder-kit#73); +# an empty extraction; an exit 0 without the done sentinel; a block that exits non-zero. +# Not modelled: a deleted or renamed scaffold.md. The rail keys on the same file as the block's guard, so +# such a target looks like the kit's own tree, which owes only the done sentinel. +# Needs bash, git, awk and mktemp (the runner's own set); no network, no jq. +# Run: bash .claude/workflows/tests/run-verification-block-fixture.sh +set -euo pipefail +here=$(cd "$(dirname "$0")" && pwd) +runner="$here/run-verification-block.sh" +[ -f "$runner" ] || { echo "run-verification-block-fixture: the runner is missing ($runner)"; exit 1; } +d=$(mktemp -d) +trap 'rm -rf "$d"' EXIT + +n=0 +case_() { # case_ [text the output must carry] + local name=$1 want=$2 target=$3 body=$4 says=${5:-} + local c="$d/c$n"; n=$((n + 1)) + git -c init.defaultBranch=main init -q "$c" + mkdir -p "$c/.claude/workflows/tests" "$c/.claude/rules" + cp "$runner" "$c/.claude/workflows/tests/" + if [ "$target" = yes ]; then mkdir -p "$c/docs/cbk"; : > "$c/docs/cbk/scaffold.md"; fi + if [ "$body" = NONE ]; then + printf '## Verification\n\nNo fence here.\n' > "$c/.claude/rules/cbk-conventions-reference.md" + else + printf '## Verification\n\n```bash\n%s\n```\n' "$body" > "$c/.claude/rules/cbk-conventions-reference.md" + fi + local rc=0 out + out=$(bash "$c/.claude/workflows/tests/run-verification-block.sh" 2>&1) || rc=$? + [ "$rc" -eq "$want" ] || { echo "FAIL: $name (want exit $want, got $rc)"; printf '%s\n' "$out" | sed 's/^/ | /'; exit 1; } + [ -z "$says" ] || grep -qF -- "$says" <<<"$out" || { echo "FAIL: $name (output lacks '$says')"; printf '%s\n' "$out" | sed 's/^/ | /'; exit 1; } + echo "ok: $name" +} + +KIT='echo "verification: kit sub-block complete"' +PROJ='if [ -f docs/cbk/scaffold.md ]; then echo "verification: project sub-block complete"; fi' +SKIPPED='if [ -f docs/cbk/scaffold-moved.md ]; then echo "verification: project sub-block complete"; fi' +DONE='echo "verification: done"' + +case_ "a kit tree that prints the done sentinel passes" 0 no "$KIT +$PROJ +$DONE" +case_ "a filled target that prints both sentinels passes" 0 yes "$KIT +$PROJ +$DONE" +case_ "a filled target whose project sub-block never ran fails" 1 yes "$KIT +$SKIPPED +$DONE" "project sub-block complete" +case_ "an empty extraction fails" 1 no NONE "EMPTY EXTRACTION" +case_ "an exit 0 without the done sentinel fails" 1 no "$KIT" "WITHOUT the done sentinel" +case_ "a block that exits non-zero fails" 1 yes "$KIT +false +$DONE" "block exited 1" + +echo "run-verification-block-fixture: $n cases ok" +```` + +- [ ] **Step 2: Run it against the unfixed runner — it must fail on the filled-target case** + +Run: `bash -n .claude/workflows/tests/run-verification-block-fixture.sh && bash .claude/workflows/tests/run-verification-block-fixture.sh; echo "exit=$?"` + +Expected: +``` +ok: a kit tree that prints the done sentinel passes +ok: a filled target that prints both sentinels passes +FAIL: a filled target whose project sub-block never ran fails (want exit 1, got 0) + | verification: kit sub-block complete + | verification: done +exit=1 +``` +Record `FAIL: a filled target whose project sub-block never ran fails (want exit 1, got 0)` in the red-first +table. + +- [ ] **Step 3: Add the rail, and say "three rails" everywhere the runner is described** + +**Replace** in `.claude/workflows/tests/run-verification-block.sh` *(whole lines)*: +````text +# Runs the verification block (cbk-conventions-reference.md § Verification) with two fail-loud +# rails the block cannot carry itself: an EMPTY extraction is red (the heading or the fence +# moved — a plain `bash -e` on an empty file exits 0), and an exit 0 that never printed the +# closing sentinel is red (the block ended early). The kit's CI calls this; a target project +# copies it as the body of the task its check command runs (cbk-conventions-reference.md +# § Verification › Run it). Run from anywhere inside the checkout: +```` +with: +````text +# Runs the verification block (cbk-conventions-reference.md § Verification) with three fail-loud +# rails the block cannot carry itself: an EMPTY extraction is red (the heading or the fence +# moved — a plain `bash -e` on an empty file exits 0); an exit 0 that never printed the +# closing sentinel is red (the block ended early); and in a filled target (docs/cbk/scaffold.md +# exists) an exit 0 that never printed the project sentinel is red (the project sub-block was +# skipped). Fixture: run-verification-block-fixture.sh, which the block itself runs. The kit's CI +# calls this; a target project copies it as the body of the task its check command depends on +# (cbk-conventions-reference.md § Verification › Run it). Run from anywhere inside the checkout: +```` + +**Replace** in `.claude/workflows/tests/run-verification-block.sh` *(whole lines)*: +````text +grep -q '^verification: done$' <<<"$out" || { echo "verification: exit 0 WITHOUT the done sentinel — the block ended early"; exit 1; } +```` +with: +````text +grep -q '^verification: done$' <<<"$out" || { echo "verification: exit 0 WITHOUT the done sentinel — the block ended early"; exit 1; } +# A filled target also owes the project sentinel. `verification: done` prints whether or not the project +# sub-block ran, so a guard that stops firing (moved, edited, pointed at another file) would turn every +# project check off with the gate green (context-builder-kit#73). The rail keys on the same file as the +# guard, so a deleted or renamed scaffold.md makes a target look like the kit's own tree, which owes only +# the done sentinel; that case is not caught here. +if [ -f docs/cbk/scaffold.md ]; then + grep -q '^verification: project sub-block complete$' <<<"$out" \ + || { echo "verification: a filled target's project sub-block never completed — docs/cbk/scaffold.md exists but 'verification: project sub-block complete' was not printed"; exit 1; } +fi +```` + +**Replace** in `.claude/rules/cbk-conventions-reference.md` *(whole lines; the § Verification › Run it paragraph. +The bare `#58` becomes `context-builder-kit#58` (D53), because this task owns the paragraph)*: +````text +**Run it** through `.claude/workflows/tests/run-verification-block.sh`: it performs the documented extraction and adds two fail-loud rails the block cannot carry for itself — an empty extraction is red (a plain `bash -e` on an empty file exits 0), and an exit 0 that never printed `verification: done` is red. The kit's CI runs it on every pull request; a target wires the same script as the body of a task its check command depends on (a check nobody re-runs is a belief with a date on it — #58, second application, item 7). The block stays fail-fast: every red is fixed, or the check is narrowed in the project's own copy with an inline comment saying why — a "recorded" red cannot reach the sentinels. +```` +with: +````text +**Run it** through `.claude/workflows/tests/run-verification-block.sh`: it performs the documented extraction and adds three fail-loud rails the block cannot carry for itself — an empty extraction is red (a plain `bash -e` on an empty file exits 0); an exit 0 that never printed `verification: done` is red; and in a filled target (`docs/cbk/scaffold.md` exists) an exit 0 that never printed `verification: project sub-block complete` is red, because the done sentinel prints whether or not the project sub-block ran. The third rail keys on the same `docs/cbk/scaffold.md` as the project sub-block's guard, so a deleted or renamed scaffold file makes a target look like the kit's own tree, and that is not caught. `run-verification-block-fixture.sh` drives the three rails on synthetic blocks, and the block runs it, so the block guards the script that runs it. The kit's CI runs the runner on every pull request; a target wires the same script as the body of a task its check command depends on (a check nobody re-runs is a belief with a date on it — context-builder-kit#58, second application, item 7). The block stays fail-fast: every red is fixed, or the check is narrowed in the project's own copy with an inline comment saying why — a "recorded" red cannot reach the sentinels. +```` + +**Replace** in `.claude/rules/cbk-conventions-reference.md` *(whole lines; the kit sub-block's sentinel)*: +````text +echo "verification: kit sub-block complete" +```` +with: +````text +# The runner's three rails hold on synthetic blocks, the runner copied into throwaway checkouts at its real +# path (§ Verification › Run it): the block guards the script that runs it. +bash .claude/workflows/tests/run-verification-block-fixture.sh || { echo "run-verification-block.sh lost a rail (the fixture names the case)"; exit 1; } + +echo "verification: kit sub-block complete" +```` + +- [ ] **Step 4: The fixture is green, and the runner runs it inside the block** + +Run: `bash -n .claude/workflows/tests/run-verification-block.sh && bash .claude/workflows/tests/run-verification-block-fixture.sh; echo "exit=$?"` + +Expected: +``` +ok: a kit tree that prints the done sentinel passes +ok: a filled target that prints both sentinels passes +ok: a filled target whose project sub-block never ran fails +ok: an empty extraction fails +ok: an exit 0 without the done sentinel fails +ok: a block that exits non-zero fails +run-verification-block-fixture: 6 cases ok +exit=0 +``` + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | tail -10; echo "exit=${PIPESTATUS[0]}"` + +Expected: +``` +always-loaded total: 130628 bytes +ok: a kit tree that prints the done sentinel passes +ok: a filled target that prints both sentinels passes +ok: a filled target whose project sub-block never ran fails +ok: an empty extraction fails +ok: an exit 0 without the done sentinel fails +ok: a block that exits non-zero fails +run-verification-block-fixture: 6 cases ok +verification: kit sub-block complete +verification: done +exit=0 +``` + +Run: `grep -c 'two fail-loud' .claude/rules/cbk-conventions-reference.md .claude/workflows/tests/run-verification-block.sh` +Expected: both counts `0`. + +- [ ] **Step 5: Commit** + +```bash +git add .claude/workflows/tests/run-verification-block-fixture.sh .claude/workflows/tests/run-verification-block.sh .claude/rules/cbk-conventions-reference.md +git commit -F - <<'EOF' +feat(verification): V1 — the runner's third rail, pinned by a fixture the block runs + +A filled target (docs/cbk/scaffold.md exists) now also owes `verification: +project sub-block complete`: the done sentinel prints whether or not the +project sub-block ran, so a guard that stopped firing turned every project +check off with the gate green. run-verification-block-fixture.sh drives the +three rails in six cases on synthetic blocks, and the kit sub-block runs it. +§ Verification › Run it says three rails, names the residual (a deleted or +renamed scaffold.md), and cites context-builder-kit#58 instead of a bare #58. + +Trace: #73/body/1, critic/22 (D51). + +Co-Authored-By: Claude Opus 5.5 +EOF +``` + +--- + +### Task V1.2: A Stop hook that could not look is red in the gate (#73/body/2; D51) + +**Files:** +- Modify: `.claude/rules/cbk-conventions-reference.md`, the § Verification preamble (line 474) and the kit + sub-block's Stop-hook check (lines 636–638) + +**Interfaces:** +- Consumes: `detect-forked-agent-memory.sh`'s contract that every could-not-look path (no checkout, an + unenterable root, a partial scan, an unmonitored walk) exits 0 and prints the literal word `WARNING` on + stderr. V2 owns the hook, keeps that literal, and pins the partial-scan path in `hook-payloads-fixture.sh` + (handed out, see § Handed to other clusters). +- Produces: the block function `stop_hook_clean [git ceiling]`. It returns 0 only when the + hook exited 0 with no `WARNING`. Its red messages are `a reviewer-memory tree exists outside the root + (detect-forked-agent-memory.sh exit N). The hook said:` and `the Stop hook could not look (exit 0 with a + WARNING). The hook said:`. +- Produces: the in-block negative case. Outside any checkout, under a git ceiling, the check must return 1; + otherwise the block prints `the Stop-hook check passed a hook that could not look (exit 0 with a WARNING) — + in a gate that is red`. +- Produces: the preamble sentence "a check that could not look is red". + +- [ ] **Step 1: Write the negative case first, around the check's current semantics (exit code only)** + +**Replace** in `.claude/rules/cbk-conventions-reference.md` *(whole lines)*: +````text +printf '{"stop_hook_active":false}' | CLAUDE_PROJECT_DIR="$PWD" .claude/hooks/detect-forked-agent-memory.sh || { echo "a reviewer-memory tree exists outside the root (detect-forked-agent-memory.sh blocked; its stderr above names the paths)"; exit 1; } +```` +with: +````text +stop_hook_clean() { # stop_hook_clean [git ceiling]: 0 only when the hook looked and found nothing + local rc=0 err + err=$(printf '{"stop_hook_active":false}' | CLAUDE_PROJECT_DIR="$1" GIT_CEILING_DIRECTORIES="${2:-${GIT_CEILING_DIRECTORIES:-}}" .claude/hooks/detect-forked-agent-memory.sh 2>&1 >/dev/null) || rc=$? + [ "$rc" -eq 0 ] || { echo "a reviewer-memory tree exists outside the root (detect-forked-agent-memory.sh exit $rc). The hook said:"; printf ' %s\n' "$err"; return 1; } +} +stop_hook_clean "$PWD" || exit 1 +# The check bites on a hook that could not look: outside a checkout the hook exits 0 with a WARNING. The +# ceiling at the temp dir's parent keeps git from finding a checkout above it (a TMPDIR inside a work tree +# would otherwise). +nc=$(mktemp -d); if stop_hook_clean "$nc" "${nc%/*}" >/dev/null; then rmdir "$nc"; echo "the Stop-hook check passed a hook that could not look (exit 0 with a WARNING) — in a gate that is red"; exit 1; fi; rmdir "$nc" +```` + +The function keeps the two comment lines above it (`# No reviewer-memory tree outside the root — …` and +`# so the exclusion list has one home …`) unchanged. `local rc=0 err` and the `2>&1 >/dev/null` capture order +are bash 3.2 safe and keep only the hook's stderr. + +- [ ] **Step 2: Run the block — the negative case must turn it red** + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | tail -3; echo "exit=${PIPESTATUS[0]}"` + +Expected: +``` +hook-contract-fixture: ok +the Stop-hook check passed a hook that could not look (exit 0 with a WARNING) — in a gate that is red +verification: block exited 1 +exit=1 +``` +Record `the Stop-hook check passed a hook that could not look (exit 0 with a WARNING) — in a gate that is red` +in the red-first table. This is the state at `643f7ff`: the hook names the block as its backstop, and the +block accepted the hook's could-not-look exit. + +- [ ] **Step 3: Read the hook's stderr, and state the rule in the preamble** + +**Replace** in `.claude/rules/cbk-conventions-reference.md` *(whole lines)*: +````text + [ "$rc" -eq 0 ] || { echo "a reviewer-memory tree exists outside the root (detect-forked-agent-memory.sh exit $rc). The hook said:"; printf ' %s\n' "$err"; return 1; } +} +```` +with: +````text + [ "$rc" -eq 0 ] || { echo "a reviewer-memory tree exists outside the root (detect-forked-agent-memory.sh exit $rc). The hook said:"; printf ' %s\n' "$err"; return 1; } + # Exit 0 with a WARNING is a hook that could not look: no checkout, an unenterable root, a partial scan, an + # unmonitored walk. A Stop hook fails open by design, so a stop is never blocked on a check it could not + # make; in a gate that state is red (context-builder-kit#73). + if grep -q 'WARNING' <<<"$err"; then echo "the Stop hook could not look (exit 0 with a WARNING). The hook said:"; printf ' %s\n' "$err"; return 1; fi +} +```` + +**Replace** in `.claude/rules/cbk-conventions-reference.md` *(inline; the § Verification preamble, line 474)*: +````text +Every check says what it catches. Run the block after +```` +with: +````text +Every check says what it catches, and a check that could not look is red: a hook fails open by design when it cannot see, so a gate that reads only its exit code passes on nothing (the Stop-hook check reads the hook's stderr for that reason). Run the block after +```` + +- [ ] **Step 4: Green on the tree; red on a partial scan** + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | tail -2; echo "exit=${PIPESTATUS[0]}"` + +Expected: +``` +verification: kit sub-block complete +verification: done +exit=0 +``` + +Mutation probe, a partial scan. `chmod 000` is ignored for root, so as root this prints the SKIP line. Record +which line printed. + +```bash +if [ "$(id -u)" -eq 0 ]; then echo "SKIP: partial-scan probe (running as root; chmod 000 is ignored)"; else + mkdir -p v1-unreadable-probe/x && chmod 000 v1-unreadable-probe + bash .claude/workflows/tests/run-verification-block.sh 2>&1 | grep -E 'could not look|scan was partial|block exited'; echo "exit=${PIPESTATUS[0]}" + chmod 755 v1-unreadable-probe && rm -rf v1-unreadable-probe +fi +git status --short +``` + +Expected (non-root): +``` +the Stop hook could not look (exit 0 with a WARNING). The hook said: + detect-forked-agent-memory: WARNING — the scan was partial; find could not read: +verification: block exited 1 +exit=1 +``` +followed by a `git status --short` that prints only ` M .claude/rules/cbk-conventions-reference.md`: this task's +edit, not yet committed. The probe directory is gone. As root, the SKIP line is followed by the same status line. + +- [ ] **Step 5: Commit** + +```bash +git add .claude/rules/cbk-conventions-reference.md +git commit -F - <<'EOF' +fix(verification): V1 — a Stop hook that could not look is red in the gate + +detect-forked-agent-memory.sh fails open by design (exit 0 with a WARNING) +when it could not look: no checkout, an unenterable root, a partial scan, an +unmonitored walk. The block's check read only the exit code, so it passed on +nothing while the hook named the block as its backstop. stop_hook_clean reads +the hook's stderr; an in-block negative case, outside any checkout under a git +ceiling, must itself come back red, so a regression is caught where the check +runs. The preamble states the rule. + +Trace: #73/body/2, the block's half (D51). The hook's WARNING contract and +the partial-scan fixture case are V2's. + +Co-Authored-By: Claude Opus 5.5 +EOF +``` + +--- + +### Task V1.3: The budget line warns above 140,000 bytes (D50) + +**Files:** +- Modify: `.claude/rules/cbk-conventions-reference.md`, the kit sub-block's always-loaded budget line (line 688) + +**Interfaces:** +- Consumes: the budget loop's `$total`. +- Produces: a non-failing line `WARN: always-loaded total N bytes is above the 140000-byte budget line — …`, + printed only when N > 140000. Task F1 greps `always-loaded total|WARN`. V5's D59 split works against + 130,628 and never needs this line to fire. + +- [ ] **Step 1: The failing check — a padded always-loaded rule must produce a WARN line** + +The check is a dry run in a throwaway clone. It copies the working tree's reference file in, then pads +`workflows.md`, an always-loaded rule, by 10,002 bytes: + +```bash +d=$(mktemp -d) && git clone -q . "$d/k" && cp .claude/rules/cbk-conventions-reference.md "$d/k/.claude/rules/" \ + && ( cd "$d/k" && { printf '\n'; head -c 10000 /dev/zero | tr '\0' 'x'; printf '\n'; } >> .claude/rules/workflows.md \ + && bash .claude/workflows/tests/run-verification-block.sh > "$d/out" 2>&1; echo "exit=$?"; grep -E '^always-loaded total|^WARN' "$d/out"; echo "warn-lines=$(grep -c '^WARN' "$d/out")" ) +rm -rf "$d" +``` + +- [ ] **Step 2: Run it before the change — no WARN line** + +Expected: +``` +exit=0 +always-loaded total: 140630 bytes +warn-lines=0 +``` +Record `always-loaded total: 140630 bytes` with `warn-lines=0` in the red-first table. + +- [ ] **Step 3: Add the warning** + +**Replace** in `.claude/rules/cbk-conventions-reference.md` *(whole lines)*: +````text +total=0; for f in .claude/rules/*.md; do head -1 "$f" | grep -q '^---$' || { s=$(wc -c < "$f"); total=$((total+s)); echo "always-loaded: $f ($s bytes)"; }; done; echo "always-loaded total: $total bytes" +```` +with: +````text +total=0; for f in .claude/rules/*.md; do head -1 "$f" | grep -q '^---$' || { s=$(wc -c < "$f"); total=$((total+s)); echo "always-loaded: $f ($s bytes)"; }; done; echo "always-loaded total: $total bytes" +# Above 140,000 bytes the budget line warns and never fails: the number is a signal to path-scope, split or delete +# a rule, not a gate (cbk-conventions.md § Rule loading and the instruction budget). The WARN prefix is what a +# release's budget check greps for. +[ "$total" -le 140000 ] || echo "WARN: always-loaded total $total bytes is above the 140000-byte budget line — path-scope, split or delete a rule (cbk-conventions.md § Rule loading and the instruction budget)" +```` + +- [ ] **Step 4: The padded tree warns and stays green; the real tree does not warn** + +Re-run Step 1's commands. Expected: +``` +exit=0 +always-loaded total: 140630 bytes +WARN: always-loaded total 140630 bytes is above the 140000-byte budget line — path-scope, split or delete a rule (cbk-conventions.md § Rule loading and the instruction budget) +warn-lines=1 +``` + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | grep -E 'always-loaded total|WARN|^verification: (kit|done)'; echo "exit=${PIPESTATUS[0]}"` + +Expected: +``` +always-loaded total: 130628 bytes +verification: kit sub-block complete +verification: done +exit=0 +``` + +- [ ] **Step 5: Commit** + +```bash +git add .claude/rules/cbk-conventions-reference.md +git commit -F - <<'EOF' +feat(verification): V1 — the budget line warns above 140,000 always-loaded bytes + +The always-loaded loop printed the standing cost but nothing flagged growth. +Above 140,000 bytes it now prints a WARN line and the run goes on: the number +is a signal to path-scope, split or delete a rule, not a gate. Shown by +padding an always-loaded rule to 140,630 bytes in a throwaway clone: no WARN +before, one WARN after, exit 0 both times. + +Decision: D50 (no trace row of its own). + +Co-Authored-By: Claude Opus 5.5 +EOF +``` + +--- + +### Task V1.4: A freshly scaffolded target passes its own block (Review Focus 4; the Gate settled call; review/portability/68) + +**Files:** +- Modify: `.claude/rules/cbk-conventions-reference.md`, the kit sub-block's `CLAUDE.md` check (lines 506–508), + plus a new check at the kit sub-block's sentinel +- Modify: `.claude/skills/scaffold/references/bootstrap_checklist_template.md`, § 4 Rule-file disposition's first + sentence (line 77) and its table (after line 91) +- Modify: `.claude/skills/scaffold/SKILL.md`, the Phase exit checklist's disposition item (line 315) +- Modify: `.claude/skills/scaffold/references/test_cases.md`, Test 1's bootstrap-checklist criterion (line 22) + +**Interfaces:** +- Consumes: the project sub-block's stamped-globs check (`unfilled paths placeholder in `), unchanged. +- Produces: the disposition row `| \`cbk-conventions-reference.md\` | stamped | … |`. V5's D59 split, V8's one-time + choices and V10's Kit commit row edit the same checklist in other rows. V5 must add a row if + `knowledge-backend-reference.md` ships a `paths:` placeholder, because the pin below fails otherwise. +- Produces: a kit-sub-block pin. Every rule whose `paths:` block ships a `<` placeholder must have a + `| \`\` |` row in the disposition table; otherwise the block prints ` ships a paths: placeholder, + but scaffold's rule-file disposition table has no row for it`. +- Produces: the rule that a scaffolded target owes the `CLAUDE.md` mention once `docs/cbk/blueprint.md` exists, + while the kit tree always owes it. Red message: `CLAUDE.md is missing or does not mention cbk-conventions (a + backticked mention, never an @ import)`. +- Produces: the helper `${TMPDIR:-/tmp}/v1-fresh-scaffold.sh`, used for this task's test and re-run in V1.5 Step 4. It + is not committed. + +- [ ] **Step 1: Write the pin and the fresh-scaffold test** + +**Replace** in `.claude/rules/cbk-conventions-reference.md` *(whole lines; the kit sub-block's sentinel)*: +````text +echo "verification: kit sub-block complete" +```` +with: +````text +# Every rule whose `paths:` block ships a placeholder has a row in scaffold's rule-file disposition table, so the +# pass that stamps it asks about it: a placeholder no row names reaches a target unstamped and fails the project +# sub-block's stamped-globs check on the target's first run. The closed set is diffed here because the tree can. +for f in $(grep -l '^paths:' .claude/rules/*.md); do awk '/^---$/{c++; next} c==1' "$f" | grep -q '<' || continue; grep -q "^| \`$(basename "$f")\` |" .claude/skills/scaffold/references/bootstrap_checklist_template.md || { echo "$f ships a paths: placeholder, but scaffold's rule-file disposition table has no row for it"; exit 1; }; done + +echo "verification: kit sub-block complete" +```` + +Write the Review Focus 4 test to `${TMPDIR:-/tmp}/v1-fresh-scaffold.sh`. It lives outside the repository and is +not committed. It builds a target as it stands at scaffold's end, before blueprint: the drop-in set from this +working tree, the ADR starters instantiated, `docs/cbk/scaffold.md` and the problem brief committed, a README, no +`CLAUDE.md`. It then performs the rule-file disposition pass's stamping and runs the real runner: + +```bash +cat > "${TMPDIR:-/tmp}/v1-fresh-scaffold.sh" <<'EOF' +fresh_scaffold() { # fresh_scaffold [blueprint]: a target at scaffold's end (with "blueprint", past blueprint with no CLAUDE.md) + local t; t=$(mktemp -d) + git -c init.defaultBranch=main init -q "$t" + git ls-files -z .claude .mcp.json.example .github/dependabot.yml.example .github/workflows/adr-immutability-check.yml | xargs -0 cp --parents -t "$t" + mkdir -p "$t/docs/cbk" "$t/docs/adr" + cp -a .claude/skills/scaffold/references/adr-starters/. "$t/docs/adr/" + printf '# Scaffold output\n' > "$t/docs/cbk/scaffold.md" + printf '# Problem brief\n' > "$t/docs/cbk/problem_brief.md" + printf '# demo\n' > "$t/README.md" + [ "${2:-}" = blueprint ] && printf "# Blueprint\n" > "$t/docs/cbk/blueprint.md" + # The rule-file disposition pass (bootstrap checklist section 4) stamps every paths: placeholder. + sed -i 's|"\*\*/\*\."|"**/*.py"|' "$t/.claude/rules/logging.md" + sed -i 's||py|g' "$t/.claude/rules/testing.md" + [ "$1" = yes ] && sed -i 's|^ - ""$| - "**/pyproject.toml"\n - "**/uv.lock"|' "$t/.claude/rules/cbk-conventions-reference.md" + ( cd "$t" && git add -A && git -c user.name=t -c user.email=t@t commit -qm scaffold && bash .claude/workflows/tests/run-verification-block.sh > "$t.out" 2>&1; echo "exit=$?"; grep -E '^verification: |unfilled paths placeholder|^CLAUDE.md is missing|^grep: ' "$t.out" ) + rm -rf "$t" "$t.out" +} +EOF +``` + +`cp --parents` and `sed -i` are GNU forms; the executing host is Linux (Task 0 Step 3 records GNU grep). The +copy uses `git ls-files`, so it takes tracked paths with their working-tree content, V1.1's fixture included. + +- [ ] **Step 2: Run both — the pin and the fresh scaffold must be red** + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | tail -2; echo "exit=${PIPESTATUS[0]}"` + +Expected: +``` +.claude/rules/cbk-conventions-reference.md ships a paths: placeholder, but scaffold's rule-file disposition table has no row for it +verification: block exited 1 +exit=1 +``` + +Run: `. "${TMPDIR:-/tmp}/v1-fresh-scaffold.sh"; fresh_scaffold yes` + +Expected: +``` +exit=2 +grep: CLAUDE.md: No such file or directory +verification: block exited 2 +``` + +Record both red lines in the red-first table. The second line is the planning-time finding. At `643f7ff`, even a +target that stamped every glob fails its own block at scaffold's end, because `grep -q "cbk-conventions" +CLAUDE.md` runs before blueprint has written `CLAUDE.md`. Scaffold never commits `CLAUDE.md`, per +`github_only_profile.md`: "Does not commit `CLAUDE.md`, `AGENTS.md`, `.claude/`, or `.mcp.json`". + +- [ ] **Step 3: The disposition row, the key on blueprint, and their restatements** + +**Replace** in `.claude/rules/cbk-conventions-reference.md` *(whole lines)*: +````text +# CLAUDE.md points at this file as a backticked mention — deliberately NOT an `@` import, +# which would expand this whole file into every session at launch (memory docs). +grep -q "cbk-conventions" CLAUDE.md +```` +with: +````text +# CLAUDE.md points at this file as a backticked mention — deliberately NOT an `@` import, +# which would expand this whole file into every session at launch (memory docs). Blueprint writes a target's +# CLAUDE.md, so a scaffolded target owes the mention once docs/cbk/blueprint.md exists; the kit tree always does. +if [ ! -f docs/cbk/scaffold.md ] || [ -f docs/cbk/blueprint.md ]; then grep -q "cbk-conventions" CLAUDE.md || { echo "CLAUDE.md is missing or does not mention cbk-conventions (a backticked mention, never an @ import)"; exit 1; }; fi +```` + +**Replace** in `.claude/skills/scaffold/references/bootstrap_checklist_template.md` *(inline; § 4's first sentence)*: +````text +and two path-scoped rules whose `paths:` globs are placeholders (`logging.md`, `testing.md`). +```` +with: +````text +and three path-scoped rules whose `paths:` block carries a placeholder glob (`logging.md`, `testing.md`, and the manifest-and-lockfile entry in `cbk-conventions-reference.md`). +```` + +**Replace** in `.claude/skills/scaffold/references/bootstrap_checklist_template.md` *(whole lines; the disposition +table)*: +````text +| `testing.md` | stamped | `paths:` set to the project's test globs and directories (inline-test stacks: directories alone) | +```` +with: +````text +| `testing.md` | stamped | `paths:` set to the project's test globs and directories (inline-test stacks: directories alone) | +| `cbk-conventions-reference.md` | stamped | the bracketed `paths:` entry replaced with the project's manifest and lockfile globs (e.g. `**/pyproject.toml`, `**/uv.lock`); left bracketed, it fails the verification block's stamped-globs check | +```` + +**Replace** in `.claude/skills/scaffold/SKILL.md` *(inline; the Phase exit checklist)*: +````text +`logging.md` and `testing.md` carry stamped globs (no `` left) +```` +with: +````text +`logging.md`, `testing.md` and `cbk-conventions-reference.md` carry stamped globs (no `` and no bracketed manifest entry left) +```` + +**Replace** in `.claude/skills/scaffold/references/test_cases.md` *(inline; Test 1)*: +````text +section 4 lists every shipped template and path-scoped rule with a disposition, and prints the always-loaded set first. +```` +with: +````text +section 4 lists every shipped template and path-scoped rule with a disposition (the manifest-and-lockfile entry in `cbk-conventions-reference.md`'s `paths:` included), and prints the always-loaded set first. +```` + +- [ ] **Step 4: Green on the kit tree; the fresh scaffold prints all three sentinels, and each failure names its file** + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | tail -2; echo "exit=${PIPESTATUS[0]}"` + +Expected: +``` +verification: kit sub-block complete +verification: done +exit=0 +``` + +Run: `. "${TMPDIR:-/tmp}/v1-fresh-scaffold.sh"; fresh_scaffold yes; echo ---; fresh_scaffold no; echo ---; fresh_scaffold yes blueprint` + +Expected: +``` +exit=0 +verification: kit sub-block complete +verification: project sub-block complete +verification: done +--- +exit=1 +verification: kit sub-block complete +unfilled paths placeholder in .claude/rules/cbk-conventions-reference.md +verification: block exited 1 +--- +exit=1 +grep: CLAUDE.md: No such file or directory +CLAUDE.md is missing or does not mention cbk-conventions (a backticked mention, never an @ import) +verification: block exited 1 +``` + +These are Review Focus 4's three assertions. With the globs stamped, the runner prints all three sentinels. An +unstamped reference-half glob turns it red and names the file. A target past blueprint still owes `CLAUDE.md`. +This test is deliberately not a block check. Running the real block from inside the block would recurse through +`run-verification-block-fixture.sh`. + +- [ ] **Step 5: Commit** + +```bash +git add .claude/rules/cbk-conventions-reference.md .claude/skills/scaffold/references/bootstrap_checklist_template.md .claude/skills/scaffold/SKILL.md .claude/skills/scaffold/references/test_cases.md +git commit -F - <<'EOF' +fix(scaffold): V1 — a freshly scaffolded target passes its own verification block + +Two causes kept a fresh scaffold red. The disposition pass never asked about +cbk-conventions-reference.md's bracketed manifest-and-lockfile glob, which the +project sub-block's stamped-globs check rejects: the table gains the row, § 4's +first sentence counts three path-scoped placeholders, and the phase-exit item +and Test 1 name it. And the kit sub-block required a CLAUDE.md mention that +blueprint, not scaffold, writes: a scaffolded target now owes it once +docs/cbk/blueprint.md exists, and the kit tree always does. A kit-sub-block pin +fails when a rule ships a paths: placeholder the disposition table has no row +for. Shown on a simulated scaffold (drop-in set, ADR starters, scaffold.md, +globs stamped): all three sentinels; unstamped, red naming the file. + +Trace: Review Focus 4; the Gate settled call (disposition row); the defect +behind review/portability/68 (V9's row, landed here). + +Co-Authored-By: Claude Opus 5.5 +EOF +``` + +--- + +### Task V1.5: The templates wire the block into `check` (#58/c5901493591/R4; D51) + +**Files:** +- Modify: `.claude/skills/blueprint/references/templates/tooling.md`: step 1's minimum task set (line 29), the + § Rules list (after line 48), and § Light-mode behavior (line 211) +- Modify: `.claude/skills/blueprint/SKILL.md`, the Phase exit checklist (after line 324) +- Modify: `.claude/skills/blueprint/references/test_cases.md`, Test 1's tooling criterion (line 20) +- Modify: `.claude/skills/scaffold/references/bootstrap_checklist_template.md`, the verification matrix (after + line 70) +- Modify: `.claude/skills/scaffold/references/test_cases.md`, Test 1's bootstrap-checklist criterion (line 22, + as V1.4 left it) +- Modify: `.claude/rules/cbk-conventions-reference.md`, § Verification › Run it (as V1.1 left it) plus a new check at + the kit sub-block's sentinel + +**Interfaces:** +- Consumes: V1.1's Run it paragraph; V1.4's disposition row and its Test 1 sentence; V1.4's fresh-scaffold helper. +- Produces: the tooling template's **verification task** that `check` depends on, whose body is `bash + .claude/workflows/tests/run-verification-block.sh`, and which light mode keeps. V4 (the CI skeleton at lines + 58–74, the review-bot paragraph) and V8 (the `mise` note under step 1's `mise.toml` bullet, a Rules line on + pipefail discipline, sanity 6b) edit the same file at other anchors. Line 29 and the first Rules bullet are + this task's. +- Produces: the bootstrap checklist's matrix row `| Verification block runs | … |`, the matrix's last row at this + commit. V2's conditional corpus rows and V10's Kit commit row are added beside it. +- Produces: a kit-sub-block pin. Both templates must name `run-verification-block.sh`; otherwise the block prints ` + does not name .claude/workflows/tests/run-verification-block.sh (the template that wires the block into + check)`. + +- [ ] **Step 1: Write the pin** + +**Replace** in `.claude/rules/cbk-conventions-reference.md` *(whole lines; the kit sub-block's sentinel)*: +````text +echo "verification: kit sub-block complete" +```` +with: +````text +# The templates wire the block into `check`: blueprint's tooling template names the runner as the body of the +# verification task `check` depends on, and scaffold's bootstrap checklist runs it once (§ Verification › Run it). +for f in .claude/skills/blueprint/references/templates/tooling.md .claude/skills/scaffold/references/bootstrap_checklist_template.md; do grep -q 'run-verification-block\.sh' "$f" || { echo "$f does not name .claude/workflows/tests/run-verification-block.sh (the template that wires the block into check)"; exit 1; }; done + +echo "verification: kit sub-block complete" +```` + +- [ ] **Step 2: Run the block — the pin must be red** + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | tail -2; echo "exit=${PIPESTATUS[0]}"` + +Expected: +``` +.claude/skills/blueprint/references/templates/tooling.md does not name .claude/workflows/tests/run-verification-block.sh (the template that wires the block into check) +verification: block exited 1 +exit=1 +``` +Record the first line in the red-first table. + +- [ ] **Step 3: The verification task, its rule, light mode, the matrix row, and the restatements** + +Two corrections to the exercised text this ports. First, the block needs `node` and `python3` as well as `jq`, +because the kit sub-block runs the `.mjs` and Python fixtures. Second, the ported sentence "on a fresh scaffold it +is green" is not carried: V1.4 made it true, but the claim belongs to the checklist row, which states its +precondition. + +**Replace** in `.claude/skills/blueprint/references/templates/tooling.md` *(whole lines)*: +````text + Must define at minimum: `setup`, `check`, `test`, `lint`, `dev` (or local equivalents). +```` +with: +````text + Must define at minimum: `setup`, `check`, `test`, `lint`, `dev` (or local equivalents), and a **verification task** that `check` depends on, whose body is `bash .claude/workflows/tests/run-verification-block.sh` (`cbk-conventions-reference.md` § Verification › Run it). The runner needs `bash`, `git`, `awk` and `mktemp`; the block it runs also needs `jq`, `node` and `python3`, because it runs the kit's fixtures, so `setup` installs them or the stack decisions say where they come from. A host missing one of them runs the block red. In a filled target the runner requires both `verification: project sub-block complete` and `verification: done`. Name the task in CLAUDE.md's command list like every other. +```` + +**Replace** in `.claude/skills/blueprint/references/templates/tooling.md` *(whole lines; § Rules)*: +````text +- **Every command in CLAUDE.md = actual task definition.** No exceptions. +```` +with: +````text +- **Every command in CLAUDE.md = actual task definition.** No exceptions. +- **The verification block is a leg of `check`, never a command someone remembers.** It holds the `.claude/` tooling contract (the hook registry, the byte-parallel copies, the always-loaded budget, the project sub-block), and a check nobody re-runs is a belief with a date on it. Because `check` locally = the CI pipeline (the next rule), the CI job that runs `check` runs the block too. Run the task once before the HITL presentation; a red line is fixed before hand-off, not recorded. +```` + +**Replace** in `.claude/skills/blueprint/references/templates/tooling.md` *(whole lines; § Light-mode behavior)*: +````text +- **Task runner config has the minimum set**: setup, check, test. Skip lint/typecheck/dev/fix/etc. unless they were explicitly chosen during stack decisions. +```` +with: +````text +- **Task runner config has the minimum set**: setup, check, test, and the verification task `check` depends on (it keeps the `.claude/` contract checked, so light mode keeps it too). Skip lint/typecheck/dev/fix/etc. unless they were explicitly chosen during stack decisions. +```` + +**Replace** in `.claude/skills/blueprint/SKILL.md` *(whole lines; the Phase exit checklist)*: +````text +- [ ] `CLAUDE.md` mentions the other docs as backticked paths: `grep -n "^- @\|@docs/" CLAUDE.md` prints nothing +```` +with: +````text +- [ ] `CLAUDE.md` mentions the other docs as backticked paths: `grep -n "^- @\|@docs/" CLAUDE.md` prints nothing +- [ ] The task runner defines the verification task that `check` depends on, and `bash .claude/workflows/tests/run-verification-block.sh` exits 0 with `verification: project sub-block complete` and `verification: done` as its last two lines +```` + +**Replace** in `.claude/skills/blueprint/references/test_cases.md` *(inline; Test 1)*: +````text +every CI gate in STANDARDS.md maps to an actual job in the workflow file +```` +with: +````text +every CI gate in STANDARDS.md maps to an actual job in the workflow file; the task runner defines a verification task that `check` depends on, running `.claude/workflows/tests/run-verification-block.sh`, and it ran once before the HITL presentation with both sentinels printed +```` + +**Replace** in `.claude/skills/scaffold/references/bootstrap_checklist_template.md` *(whole lines; § 3 Verification +matrix)*: +````text +| Branch protection (if configured) | Try to push directly to main from a clone | Push is rejected | ☐ | +```` +with: +````text +| Branch protection (if configured) | Try to push directly to main from a clone | Push is rejected | ☐ | +| Verification block runs | With the kit's `.claude/` in the clone and the rule-file disposition below done, from the clone's root: `bash .claude/workflows/tests/run-verification-block.sh` | Exits 0; the last two lines are `verification: project sub-block complete` and `verification: done` (a filled target owes both — `cbk-conventions-reference.md` § Verification › Run it). Blueprint wires the same script into `check`. | ☐ | +```` + +**Replace** in `.claude/skills/scaffold/references/test_cases.md` *(inline; Test 1, as V1.4 left it)*: +````text +and prints the always-loaded set first. +```` +with: +````text +and prints the always-loaded set first; the verification matrix's last row ran the runner after that pass and read both sentinels. +```` + +**Replace** in `.claude/rules/cbk-conventions-reference.md` *(inline; § Verification › Run it, as V1.1 left it)*: +````text +a target wires the same script as the body of a task its check command depends on (a check nobody re-runs +```` +with: +````text +a target wires the same script as the body of a task its check command depends on — blueprint's `templates/tooling.md` names that task, and scaffold's bootstrap checklist runs the script once in its verification matrix (a check nobody re-runs +```` + +- [ ] **Step 4: Green; the matrix row's claim holds on the fresh scaffold; SKILL.md stays under 500 lines** + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | tail -2; echo "exit=${PIPESTATUS[0]}"` + +Expected: +``` +verification: kit sub-block complete +verification: done +exit=0 +``` + +Run: `. "${TMPDIR:-/tmp}/v1-fresh-scaffold.sh"; fresh_scaffold yes` + +Expected (the row's "last two lines" claim): +``` +exit=0 +verification: kit sub-block complete +verification: project sub-block complete +verification: done +``` + +Run: `wc -l < .claude/skills/blueprint/SKILL.md; grep -c 'run-verification-block.sh' .claude/skills/blueprint/references/templates/tooling.md .claude/skills/scaffold/references/bootstrap_checklist_template.md` +Expected: `355`, then counts `1` and `1`. + +- [ ] **Step 5: Commit** + +```bash +git add .claude/skills/blueprint/references/templates/tooling.md .claude/skills/blueprint/SKILL.md .claude/skills/blueprint/references/test_cases.md .claude/skills/scaffold/references/bootstrap_checklist_template.md .claude/skills/scaffold/references/test_cases.md .claude/rules/cbk-conventions-reference.md +git commit -F - <<'EOF' +feat(blueprint): V1 — the templates wire the verification block into check + +Nothing in the templates ran the block, so a target ran it only if someone +remembered. Blueprint's tooling template: the minimum task set, light mode +included, gains a verification task that check depends on, whose body is the +runner, with the tools it needs (bash, git, awk, mktemp; jq, node, python3 +for the block's fixtures); § Rules makes it a leg of check, run in CI through +check and once before the HITL presentation. The phase-exit checklist and Test +1 assert it. Scaffold's verification matrix runs the runner after the +disposition pass and expects both sentinels. Run it names the two homes, and a +kit-sub-block pin keeps both templates naming the runner. + +Trace: #58/c5901493591/R4 (D51). + +Co-Authored-By: Claude Opus 5.5 +EOF +``` + +--- + +### Task V1.6: The bracket idiom's cost for a spellchecker, scoped to one file (#58/c5901493591/R5; D51) + +**Files:** +- Modify: `.claude/rules/cbk-conventions-reference.md`: § Verification, a paragraph after Run it, plus a new check + at the kit sub-block's sentinel + +**Interfaces:** +- Consumes: V1.5's Run it paragraph, which ends `a "recorded" red cannot reach the sentinels.` and is followed by the + block's fence. +- Produces: § Verification's second fenced block, a ` ```toml ` fence placed before the ` ```bash ` fence. The runner's + extraction takes only ` ```bash ` fences, so the toml fence is never executed; the Step 4 run shows it. Nothing else + may add a fence to § Verification without re-running that proof. +- Produces: a kit-sub-block pin, red as `§ Verification lacks the bracket idiom's file-scoped spellchecker exemption`. + The paragraph deliberately does not enumerate the bracketed letters, because later clusters' `absent` lines + enlarge the set. + +- [ ] **Step 1: Re-fetch the typos reference and write the pin** + +The quotation and the table shape are platform facts, re-read raw on the day they are written: + +```bash +curl -sL https://raw.githubusercontent.com/crate-ci/typos/master/docs/reference.md -o "${TMPDIR:-/tmp}/typos-reference.md" +grep -cF 'File globs for matching `NAME`. This is required when defining new file types.' "${TMPDIR:-/tmp}/typos-reference.md" +grep -cF '# ... see `default`' "${TMPDIR:-/tmp}/typos-reference.md" +grep -cF 'extend-ignore-re = []' "${TMPDIR:-/tmp}/typos-reference.md" +curl -sL -o /dev/null -w '%{http_code}\n' https://github.com/crate-ci/typos/blob/master/docs/reference.md +``` +Expected: `1`, `1`, `1`, `200` (observed 2026-09-30). If a count is `0`, stop and reconcile the paragraph with the +live page. If the execution date is not 2026-09-30, change `read 2026-09-30` in Step 3's paragraph to that date. + +**Replace** in `.claude/rules/cbk-conventions-reference.md` *(whole lines; the kit sub-block's sentinel)*: +````text +echo "verification: kit sub-block complete" +```` +with: +````text +# The bracket idiom's cost is stated where the idiom lives (§ Verification): the spellchecker exemption scoped to +# this one file. The pattern brackets its own letter so this line never matches itself. +grep -q 'extend-glo[b] = \["cbk-conventions-reference.md"\]' .claude/rules/cbk-conventions-reference.md || { echo "§ Verification lacks the bracket idiom's file-scoped spellchecker exemption"; exit 1; } + +echo "verification: kit sub-block complete" +```` + +- [ ] **Step 2: Run the block — the pin must be red** + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | tail -2; echo "exit=${PIPESTATUS[0]}"` + +Expected: +``` +§ Verification lacks the bracket idiom's file-scoped spellchecker exemption +verification: block exited 1 +exit=1 +``` +Record the first line in the red-first table. + +- [ ] **Step 3: The paragraph** + +**Replace** in `.claude/rules/cbk-conventions-reference.md` *(inline; from the end of Run it's last sentence, +through the blank line, to the block's opening fence)*: +````text +a "recorded" red cannot reach the sentinels. + +```bash +```` +with: +````text +a "recorded" red cannot reach the sentinels. + +**The bracket idiom has a cost.** An `absent` check writes the phrase it hunts with one letter bracketed (`opinionate[d] profile`), so the grep never matches its own line. A target that spellchecks `.claude/` reads each bracketed fragment as a typo. Exempt the idiom in this one file, never repo-wide: a blanket ignore pattern would also hide a real misspelling written the same way. In `typos` that is a `[type.]` table whose `extend-glob` names this file and whose `extend-ignore-re` matches the idiom: + +```toml +[type.cbk-block] +extend-glob = ["cbk-conventions-reference.md"] +extend-ignore-re = ["[A-Za-z_-]*\\[[A-Za-z]\\][A-Za-z]*"] +``` + +`extend-glob` is "File globs for matching `NAME`. This is required when defining new file types.", and a type table takes the `[default]` keys, `extend-ignore-re` among them (`https://github.com/crate-ci/typos/blob/master/docs/reference.md`, read 2026-09-30; the table above exercised against typos 1.50.3, which then still reports an idiom-shaped misspelling in any other file). The skills' other bracket uses (`R[i]`, `M[n]`) are index notation, not the idiom, and need no exemption (context-builder-kit#58, the declined spellchecker item's promised note). + +```bash +```` + +- [ ] **Step 4: Green; the toml fence is not extracted; the table does what the paragraph says** + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | tail -2; echo "exit=${PIPESTATUS[0]}"` + +Expected: +``` +verification: kit sub-block complete +verification: done +exit=0 +``` + +Run: `awk '/^## Verification/{p=1} p' .claude/rules/cbk-conventions-reference.md | awk '/^```bash/{c=1;next} /^```/{c=0} c' | grep -c 'type.cbk-block'` +Expected: `0`. The runner's extraction does not carry the toml fence. + +Probe, an optional tool. It exercises the documented table against a real `typos`, with the file at its real path, +plus a second file whose idiom-shaped misspelling must still be reported: + +```bash +if command -v typos >/dev/null; then + t=$(mktemp -d); git -C "$t" init -q + awk '/^```toml/{c=1;next} /^```/{c=0} c' .claude/rules/cbk-conventions-reference.md > "$t/_typos.toml" + mkdir -p "$t/.claude/rules" "$t/docs"; cp .claude/rules/cbk-conventions-reference.md "$t/.claude/rules/" + printf 'github-onl[y] recieve[d]\n' > "$t/docs/other.md" + ( cd "$t" && typos --version && typos --format brief .claude docs; echo "rc=$?" ); rm -rf "$t" +else echo "SKIP: typos probe (typos unavailable)"; fi +``` +Expected with typos installed (observed with typos-cli 1.50.3): +``` +typos-cli 1.50.3 +docs/other.md:1:8: error: `onl` should be `only` +docs/other.md:1:15: error: `recieve` should be `receive` +rc=2 +``` +No line names `.claude/rules/cbk-conventions-reference.md`; without the table the same run reports four bracketed +fragments there (`onl`, `summar`, `chec`, `doub`). Without typos, record the SKIP line in the PR body. + +- [ ] **Step 5: Commit** + +```bash +git add .claude/rules/cbk-conventions-reference.md +git commit -F - <<'EOF' +docs(verification): V1 — name the bracket idiom's cost for a spellchecker, scoped to one file + +An absent check brackets one letter of the phrase it hunts so the grep never +matches its own line; a target that spellchecks .claude/ reads each fragment +as a typo. § Verification now says to exempt the idiom in this one file, never +repo-wide, and shows the typos form: a [type.] table whose extend-glob +names cbk-conventions-reference.md and whose extend-ignore-re matches the +idiom, quoted from typos' reference (read 2026-09-30) and exercised against +typos 1.50.3. A kit-sub-block pin keeps the note. + +Trace: #58/c5901493591/R5 (D51). + +Co-Authored-By: Claude Opus 5.5 +EOF +``` + +--- + +### Task V1.7: The kit's verify job runs an explicit bash on a named image (D52, the kit-workflows settled call) + +At this commit no workflow template carries these two pins yet. V4 adds them to the review-workflow templates and +V2 adds them to the ADR job, so the pin's comment gives its own reasons and does not lean on the templates. + +**Files:** +- Modify: `.github/workflows/verify.yml` (full content below) +- Modify: `.claude/rules/cbk-conventions-reference.md`, a new check at the kit sub-block's sentinel + +**Interfaces:** +- Consumes: the job name `Verification block`, a required check matched by name, unchanged. +- Produces: `verify.yml` with `defaults: run: shell: bash` and `runs-on: ubuntu-24.04`, each with its sourced reason. +- Produces: a kit-tree-only pin (`[ -f docs/cbk/scaffold.md ] || …`). It is red as `.github/workflows/verify.yml lacks + defaults.run.shell: bash or runs-on: ubuntu-24.04`. V2 pins `adr-immutability-check.yml` the same way when it + rewrites that job, and extends this line's file list (handed out). + +- [ ] **Step 1: Re-fetch the three facts and write the pin** + +```bash +curl -sL https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax | sed 's/<[^>]*>//g' > "${TMPDIR:-/tmp}/gh-wsyntax.txt" +grep -cF 'bash -e {0}' "${TMPDIR:-/tmp}/gh-wsyntax.txt" +grep -cF 'bash --noprofile --norc -eo pipefail {0}' "${TMPDIR:-/tmp}/gh-wsyntax.txt" +curl -sL https://raw.githubusercontent.com/actions/runner-images/main/README.md -o "${TMPDIR:-/tmp}/ri-readme.md" +grep -cF 'may see changes in the OS version' "${TMPDIR:-/tmp}/ri-readme.md" +grep -c '^#### Latest Migration Process' "${TMPDIR:-/tmp}/ri-readme.md" +curl -sL https://raw.githubusercontent.com/actions/runner-images/main/images/ubuntu/Ubuntu2404-Readme.md -o "${TMPDIR:-/tmp}/u2404.md" +for t in '- Bash 5' '- Git 2' '- jq 1' '- Node.js 2' '- Python 3'; do printf '%s: ' "$t"; grep -cF -- "$t" "${TMPDIR:-/tmp}/u2404.md"; done +``` +Expected: two non-zero counts (observed `4` and `4`: the page renders the shell table more than once), then `1`, `1`, and five +lines each ending `1`. If any count is `0`, stop and reconcile the comment. If the execution date is not 2026-09-30, +change every `read 2026-09-30` in Step 3's file to that date. + +**Replace** in `.claude/rules/cbk-conventions-reference.md` *(whole lines; the kit sub-block's sentinel)*: +````text +echo "verification: kit sub-block complete" +```` +with: +````text +# The kit's own CI pins an explicit bash (pipefail, where GitHub's unset shell runs `bash -e` without it) and a +# named runner image (a -latest label moves under the gate). Kit tree only: a target does not install verify.yml. +[ -f docs/cbk/scaffold.md ] || { grep -qx ' shell: bash' .github/workflows/verify.yml && grep -qx ' runs-on: ubuntu-24.04' .github/workflows/verify.yml; } || { echo ".github/workflows/verify.yml lacks defaults.run.shell: bash or runs-on: ubuntu-24.04"; exit 1; } + +echo "verification: kit sub-block complete" +```` + +- [ ] **Step 2: Run the block — the pin must be red** + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | tail -2; echo "exit=${PIPESTATUS[0]}"` + +Expected: +``` +.github/workflows/verify.yml lacks defaults.run.shell: bash or runs-on: ubuntu-24.04 +verification: block exited 1 +exit=1 +``` +Record the first line in the red-first table. + +- [ ] **Step 3: Rewrite `verify.yml`** + +**Overwrite** `.github/workflows/verify.yml` with this content. The trigger, job name, timeout, permissions, pinned +checkout and step are unchanged. The old header's "ubuntu-latest ships bash, jq, git, node and python3 (dated +observation, 2026-09-21 …)" sentence is replaced by the image's own readme: + +```yaml +name: Verification block + +# Runs cbk-conventions-reference.md § Verification through its runner on every pull request to +# main — no path filter, so it can be a required check (the required-checks trap in +# cbk-conventions.md § `[skip ci]` rule). + +on: + pull_request: + branches: [main] + push: + branches: [main] + +# An explicit bash runs `bash --noprofile --norc -eo pipefail {0}`; an unset shell runs `bash -e {0}`, +# with no pipefail (https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax +# § jobs..steps[*].shell, read 2026-09-30). +defaults: + run: + shell: bash + +jobs: + verify: + name: Verification block + # A named image, because a -latest label "may see changes in the OS version" while GitHub migrates it + # (https://github.com/actions/runner-images README § Latest Migration Process, read 2026-09-30); bump + # it in a reviewed PR. Ubuntu 24.04 ships bash, git, jq, node and python3 (that repository's + # images/ubuntu/Ubuntu2404-Readme.md, read 2026-09-30); a missing tool prints `command not found` + # and turns the run red. + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - name: Run the block through its runner + run: bash .claude/workflows/tests/run-verification-block.sh +``` + +- [ ] **Step 4: Green, and the workflow parses** + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | tail -2; echo "exit=${PIPESTATUS[0]}"` + +Expected: +``` +verification: kit sub-block complete +verification: done +exit=0 +``` + +Run: `python3 -c "import yaml; d=yaml.safe_load(open('.github/workflows/verify.yml')); print(d['defaults'], d['jobs']['verify']['runs-on'], d['jobs']['verify']['name'])" 2>/dev/null || echo "SKIP: YAML parse (PyYAML unavailable)"` +Expected: `{'run': {'shell': 'bash'}} ubuntu-24.04 Verification block`, or the SKIP line recorded in the PR body. On the +PR, the `Verification block` check is the live proof (Task F4 Step 3). + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | tail -20` and confirm the kit-sub-block tail of this +cluster, in order: `always-loaded total: 130628 bytes`, the six `ok:` lines, `run-verification-block-fixture: 6 cases ok`, +`verification: kit sub-block complete`, `verification: done`. The four new V1 checks print nothing when green. + +- [ ] **Step 5: Commit** + +```bash +git add .github/workflows/verify.yml .claude/rules/cbk-conventions-reference.md +git commit -F - <<'EOF' +ci: V1 — the kit's verify job runs an explicit bash on a named image + +The kit's own verify.yml ran with GitHub's unset shell (bash -e, no pipefail) +on ubuntu-latest, a label that moves under the gate; its workflow templates +take the same two pins later in this PR. verify.yml gains +defaults.run.shell: bash and runs-on: ubuntu-24.04, each with its source read +2026-09-30 (the workflow-syntax shell table; the runner-images migration note +and the 24.04 image readme). A kit-tree-only +check in the block pins both lines. The job name, the required check, is +unchanged. + +Decision: D52, the kit's-own-workflows settled call (no trace row of its own). + +Co-Authored-By: Claude Opus 5.5 +EOF +``` + +--- + +## Coverage + +Every id in the V1 work pack (items, critic; the pack has no handed-in ids and no review findings). Below them, the +other clusters' ids and the spec items this cluster also lands, so their owners' trace audit can mark them. + +| Id | Source | Lands in | What | +|---|---|---|---| +| `#73/body/1` | item | **V1.1** | The runner's third rail; `run-verification-block-fixture.sh` (six cases), run by the block; Run it says three rails and names the residual | +| `#73/body/2` | item | **V1.2**, and handed to V2 | V1.2: `stop_hook_clean`, red on exit 0 with a `WARNING`; the in-block negative case outside a checkout under a git ceiling; the preamble sentence. V2: the hook keeps the literal `WARNING` on every could-not-look path, and `hook-payloads-fixture.sh` carries the partial-scan case | +| `#58/c5901493591/R4` | item | **V1.5** (its precondition in **V1.4**) | Tooling template: the verification task (light mode included), the Rules bullet; blueprint's phase-exit item and Test 1; scaffold's matrix row and Test 1; Run it names both homes; a pin | +| `#58/c5901493591/R5` | item | **V1.6** | The bracket-idiom paragraph with the file-scoped `typos` `[type.]` table, sourced and exercised; a pin | +| `critic/22` | critic | **V1.1** | The block runs `run-verification-block-fixture.sh`; Run it says "three fail-loud rails" | +| `#73/body/2` (handed out) | handedOut → V2 | handed to V2 | See § Handed to other clusters, item 1 | +| `review/portability/68` | V9's review row (unverified) | **V1.4** | Holds at `643f7ff` (the dry run's `unfilled paths placeholder in .claude/rules/cbk-conventions-reference.md`). V1.4 adds the disposition row and the sentence count; V9 marks the row landed by V1.4 and does not re-land it | +| `#69/c5859756889/apply-h4/6` | V9's item | **V1.5** (template half) | The tooling template and checklist now name the runner. The CHANGELOG sync note ("call the runner, never an inline extraction") stays with V9/V10 | +| `#69/c5859470658/1` | V6's item | **V1.5** (the overlap it names) | R4's wiring lands here; V6 adds only its own fixture line to the block | +| `release/5` | V10's item (handed to V2, V4, V6, V7, V9) | **V1.1** (partial) | The bare `#58` in Run it becomes `context-builder-kit#58`. Four bare citations remain in § Verification's block comments, handed to V9 | +| D50 | decision | **V1.3** | The non-failing `WARN:` line above 140,000 bytes | +| D52, the kit's-own-workflows settled call | decision | **V1.7** (`verify.yml`), and handed to V2 (`adr-immutability-check.yml`) | `defaults: run: shell: bash`, `runs-on: ubuntu-24.04`, each sourced; a kit-tree pin | +| Review Focus 4 | master plan | **V1.4** | The fresh-scaffold test: three sentinels when stamped; red naming the file when not; red past blueprint without `CLAUDE.md` | +| Gate settled calls | spec | **V1.1**, **V1.4**, **V1.5** | Disposition row (V1.4). The runner fixture runs in the kit sub-block (V1.1). Light mode keeps the verification task (V1.5). No rail beyond #73's project rail (V1.1 adds exactly one). Explicit `SKIP` for a missing tool (V1.2's root probe, V1.6's typos probe, V1.7's YAML parse) | + +## Handed to other clusters + +1. **V2, `#73/body/2` (the hook's half).** `stop_hook_clean` reads `detect-forked-agent-memory.sh`'s stderr for the + literal `WARNING`. Any V2 rewrite of that hook (D61, #62's backstop wording) must keep the literal on every + could-not-look path, and keep the `Backstop:` line naming "the verification block's Stop-hook check". + `hook-payloads-fixture.sh` carries the partial-scan case (an unreadable directory: exit 0 with a `WARNING`), + printing `SKIP` as root. +2. **V2, D52 for the ADR job.** When V2 gives `adr-immutability-check.yml` `defaults: run: shell: bash` and + `runs-on: ubuntu-24.04`, extend V1.7's pin to it. The ADR job is in the drop-in set, so decide whether its pin is + kit-tree-only like `verify.yml`'s or unconditional. +3. **V2, #62's bracketed backstop slot.** The spec's Guards settled call has the project sub-block refuse an unfilled + slot, filled at scaffold's rule-file disposition pass. Add the slot to that pass (§ 4 of + `bootstrap_checklist_template.md`), and re-run V1.4's `fresh_scaffold yes` with the slot filled in the helper's + stamping block. Otherwise Review Focus 4 silently stops holding. The helper is not committed: if + `${TMPDIR:-/tmp}/v1-fresh-scaffold.sh` is gone, re-create it from V1.4 Step 1's `cat > … <<'EOF'` block first. +4. **V5, D59 and #65.** If `knowledge-backend-reference.md` ships a `paths:` block with a `<` placeholder, V1.4's pin + needs a disposition row for it. The knowledge-axis-`none` row deletes both halves. #65's unfilled-posture refusal + means a fresh scaffold passes only once `orchestration.md` is filled or deleted: extend V1.4's helper with that + disposition, and re-run it (re-creating it from V1.4 Step 1 if it is gone, as in item 3). +5. **V8, the tooling template and the checklist.** V1.5 owns line 29 (the minimum task set), the first § Rules bullet + and § Light-mode behavior's first bullet. Anchor V8's `mise` note under the `mise.toml` bullet, and its pipefail + Rules line, elsewhere. V1.4 and V1.5 edited scaffold `test_cases.md` line 22; V8's edit is line 23. +6. **V9, D53.** Four bare kit-issue citations remain in § Verification's block comments. They are `(#58, second + application, item 1)` in the retired-vocabulary comment, `Resolved pair (#37)`, `(#34)` on the + orchestration-posture row check, and `(#58 item 11)` on the launch-root guard. V1 leaves them because their lines + belong to checks other clusters may rewrite. V9's keep-out check must not pass before they are rewritten. + `review/portability/68` and the template half of `#69/c5859756889/apply-h4/6` are landed by V1.4 and V1.5. +7. **V10, README and CHANGELOG.** + - README Quick start step 2 names only the `logging.md` and `testing.md` globs. It should also name the bracketed + manifest-and-lockfile entry in `cbk-conventions-reference.md` (Customization item 4 already does). + - The v1.0.0 Sync notes should say three things. `run-verification-block.sh` is a copy row with a third rail, and + `run-verification-block-fixture.sh` is an add row. A target wires a verification task into `check`, and stamps + the reference half's glob if it never did. The block's `CLAUDE.md` check now keys on `docs/cbk/blueprint.md` + in a target. + - The `CLAUDE.md` correction (`review/release/65`) should name the runner's fixture among the kit's tests. + +## Not holding at planning time + +The pack carries no unverified review findings. These claims from the pack's exercised sources and the master plan +did not hold at `643f7ff`, and the plan does not carry them as written. Re-check each at execution. + +- **"On a fresh scaffold it is green"** (the ported tooling-template sentence, R4). It did not hold, for two causes the + dry run showed. `cbk-conventions-reference.md`'s `paths:` placeholder fails the project sub-block (`unfilled paths + placeholder in .claude/rules/cbk-conventions-reference.md`). With every glob stamped, the kit sub-block still stops + at `grep: CLAUDE.md: No such file or directory` / `verification: block exited 2`, because scaffold never commits + `CLAUDE.md`. V1.4 fixes both causes. V1.5 puts the claim on the checklist row with its precondition, not in the + tooling template. +- **Master plan, Review Focus 4's premise** that with `scaffold.md` present and the globs stamped the runner prints + all three sentinels. It fails at `643f7ff` on the `CLAUDE.md` cause above, and holds from V1.4 on. The master + plan's wording can stand after V1.4 lands. +- **"The script needs bash, git and awk, and the block needs jq"** (the ported tool list). It is incomplete. The kit + sub-block runs `node` (`review-sweep-accounting.mjs`, `finish-ab-shape.mjs`, `load-workflow-shape.mjs`) and + `python3` (`agent-cost-fixture.sh`), and the runner uses `mktemp`. V1.5 states the full set. +- **"verify.yml needs no change"** (the pack's `#73/body/1` hazard). It is true for #73's rail alone. D52's + kit's-own-workflows settled call changes the file, and V1.7 does it. diff --git a/docs/superpowers/plans/2026-09-30-harvest-5/V10-release.md b/docs/superpowers/plans/2026-09-30-harvest-5/V10-release.md new file mode 100644 index 0000000..ddb168d --- /dev/null +++ b/docs/superpowers/plans/2026-09-30-harvest-5/V10-release.md @@ -0,0 +1,1881 @@ +# Harvest 5 — V10: The release + +This cluster turns the kit into something a target installs and upgrades by version. It closes D41 (tags, +`CHANGELOG.md` with Sync notes, scaffold's Kit commit row in the `vX.Y.Z (sha)` form), D60 (a Quick start that +installs only the drop-in set from a tagged archive), R14 (the trace audit codified in this repository's +`CLAUDE.md`) and the release lens of the whole-kit review. Trace rows closed here: `release/1`–`release/4`, +`release/6`, `release/8`; the V10 parts of `release/5`; the handed-in V10 parts of `#60/c5892401033/helper`, `#60/c5881158391/status`, +`#67/c5881158070/2c`, `#69/body/F9`, `#69/body/finish-ab/N-arm`, `#69/body/finish-ab/runner`, +`#69/c5859756889/apply-h4/5` and `#69/c5901496433/sync-note`; `review/consistency/1`, `/2`, `/6`, `/7`, +`review/release/23`, `/25`, `/26`, `/27`, `/30`, `/61`, `/64`, `/65`; and, because V10 owns the file each lands in, +the § Syncing or `README.md`/`CLAUDE.md`/`CHANGELOG.md` parts of `#69/c5859756889/apply-h4/1`, `/2`, `/3`, `/6`, +`#71/body/1`, `#70/table/mise-mcp-row`, `#58/c5901493591/R14`, `review/claude-code/13`, `/14`, +`review/release/28`, `/29`, `/63`, `review/security/17`, `review/consistency/40` (`README.md`'s `finish.md` +sentence), `review/consistency/51` and `critic/9`. It runs last because it +describes what V1–V9 landed: the hook helper and fixtures (V1, V2), exec-form registrations, qualified plugin +keys and invoke-only commands (V3), the review-bot templates (V4), the recalibration and the +`knowledge-backend.md` split (V5), the N-arm harness and headless runner (V6), the review-sweep rewrite (V7), +the rebuilt `.mcp.json.example`, the `.gitignore` harness block and the formatter-scope surfaces (V8), and the +qualified citations (V9). The tags themselves and the GitHub Release are the master plan's Task F5. + +## Before you start + +- Run every task from the repository root, on `feat/harvest-5-v1.0.0`, after V9's last commit. Each task is one + commit, and the runner is green after each. +- Each task's scratch directory is `S=$(mktemp -d)`; scripts the plan gives in full are written there, never into + the tree. +- Every **Replace** below is an exact-string edit (the Edit tool). The old text is copied verbatim from the file + at `643f7ff` (identical to `74edf84` there), and each anchor sits in text that, per the ownership map, no other + cluster edits. If an anchor does not occur exactly once when the task runs, stop and reconcile; never guess. +- Every V10 block check is inserted immediately before `echo "verification: kit sub-block complete"`. The checks + on `README.md`, `CHANGELOG.md`, `CLAUDE.md`, `LICENSE` and § Syncing the kit run only on the kit tree + (`[ ! -f docs/cbk/scaffold.md ]`): in a target those files are the target's own. The block runs in a filled + target too, so a kit-tree fact must not become a target's red. +- Every red line below was produced by running the check on a scratch copy of the kit at `643f7ff` + (`cp -a`), with V10's earlier tasks applied. Where V1–V9 change which check reds first, the task says so. +- Platform quotations are re-fetched raw on the day and matched with `grep -F` after this normalisation, which + maps typographic apostrophes to `'` and a markdown link to its text: + + ```bash + norm() { sed -e "s/[’‘]/'/g" -e 's/\[\([^]]*\)\]([^)]*)/\1/g' "$1"; } + ``` + + If the execution day is not 2026-09-30, each task's quotation step says how to restamp the dates it wrote. +- Sanitization: nothing any task writes into `.claude/`, `.github/`, `README.md`, `CLAUDE.md` or `CHANGELOG.md` + names a target. Task V10.2 clones `j4th/echosphere` into `$S` with the operator's `gh` + credentials for its simulation; no sibling checkout is touched, and nothing is written outside `$S`. + +### Task V10.1: § Syncing the kit speaks in versions; scaffold records the kit release as `vX.Y.Z (sha)` (release/1, release/2, #69/c5901496433/sync-note, #69/c5859756889/apply-h4/2, #69/c5859756889/apply-h4/3, #71/body/1, release/5; D41, D52, D53) + +**Files:** +- Modify: `.claude/rules/cbk-conventions-reference.md` — § Syncing the kit (the whole section body), and one + check at the kit sub-block's sentinel +- Modify: `.claude/rules/cbk-conventions.md` — the one-line pointer under `## Syncing the kit` +- Modify: `.claude/skills/scaffold/references/scaffold_output_template.md` — the Kit commit row (line 24), the + Cascade metadata guidance (line 106), the worked example's row (line 130) +- Modify: `.claude/skills/scaffold/SKILL.md` — the Cascade metadata item of the output-doc list (line 213) +- Modify: `.claude/skills/scaffold/references/bootstrap_checklist_template.md` — one verification-matrix row, + after `Scaffold output readable` +- Modify: `.claude/skills/scaffold/references/test_cases.md` — Test 1's two-axis Cascade metadata criterion + +**Interfaces:** +- Consumes: V8's `.claude/workflows/` skip floor in `.claude/hooks/format-on-edit.sh` and its formatter-scope row + in the bootstrap checklist (the new bullet names both as the rule's other locations). V8 hands `#71/body/1`'s + § Syncing clause to this task and does not write it. V1's verification-matrix row and V2's corpus rows sit + elsewhere in the same matrix. V9 may already have rewritten this section's citation to + `context-builder-kit#58`; the section replacement accepts either spelling. +- Produces: the Kit commit form `vX.Y.Z (sha)`, with the template row text ``. + Task V10.3's install step prints `Kit commit: vX.Y.Z (sha)`, and V10.2's Sync notes cite this section. +- Produces: the § Syncing the kit bullets `**Files run ahead of the kit merge against the release that lands + them.**`, `**After resolving `settings.json`, diff its hook event keys against the pre-merge copy**`, `**Kit-owned + code stays out of the target's formatter and linter scope.**` and `**No Kit commit row to read.**`. +- Produces: the block sentences `scaffold_output_template.md's Kit commit row does not take the vX.Y.Z (sha) form` + and `cbk-conventions-reference.md § Syncing the kit does not name CHANGELOG.md's Sync notes`. +- The contract pointer grows the always-loaded total by 59 bytes (130,628 → 130,687 on the planning copy). + +- [ ] **Step 0: Confirm the surfaces this task points at** + +Run: +```bash +grep -n '\.claude/workflows' .claude/hooks/format-on-edit.sh | head -3 +grep -n -i 'formatter' .claude/skills/scaffold/references/bootstrap_checklist_template.md | head -3 +``` +Expected: each prints at least one line (V8's skip floor; V8's formatter-scope row). If either prints nothing, +stop: the new bullet's last sentence names both as the rule's other locations (§ Multi-surface facts). + +- [ ] **Step 1: Add the failing checks to the block** + +**Replace** in `.claude/rules/cbk-conventions-reference.md` *(whole line; the kit sub-block's sentinel)*: +````text +echo "verification: kit sub-block complete" +```` +with: +````text +# Releases (V10): scaffold records the kit release it installed as `vX.Y.Z (sha)`, and on the kit tree +# § Syncing the kit reads CHANGELOG.md's Sync notes before its file-by-file table. +grep -qF '| **Kit commit** | &1 | tail -2; echo "exit=${PIPESTATUS[0]}"` + +Expected: +``` +scaffold_output_template.md's Kit commit row does not take the vX.Y.Z (sha) form +verification: block exited 1 +exit=1 +``` +Record the first line in the PR body's red-first table. + +- [ ] **Step 3: The scaffold row, its guidance, its example, the SKILL list, the checklist row, the test case** + +**Replace** in `.claude/skills/scaffold/references/scaffold_output_template.md` *(whole line 24)*: +````text +| **Kit commit** | | +```` +with: +````text +| **Kit commit** | | +```` + +**Replace** in the same file *(inline, the end of the Cascade metadata guidance paragraph, line 106)*: +````text +Include only the axis-conditional rows the chosen axes need. Keep the design-doc-mode note for in-repo-markdown planning as a one-liner. +```` +with: +````text +Include only the axis-conditional rows the chosen axes need. Keep the design-doc-mode note for in-repo-markdown planning as a one-liner. The `Kit commit` row reads `vX.Y.Z (sha)`: the kit release this `.claude/` came from and its commit, which the kit's install step (its `README.md` § Quick start) prints as `Kit commit: vX.Y.Z (sha)`. From a clone of the kit rather than a release archive, `git describe --tags --exact-match` and `git rev-parse --short HEAD` in that clone give the two halves; a clone on no tag records the newest tag before it (`git describe --tags --abbrev=0`) with the clone's own sha, which is the merge base. +```` + +**Replace** in the same file *(whole line 130, the worked example)*: +````text +| **Kit commit** | e92e9c4 | +```` +with: +````text +| **Kit commit** | v0.4.0 (e92e9c4) | +```` + +**Replace** in `.claude/skills/scaffold/SKILL.md` *(inline, line 213)*: +````text +Notion hub URL (if knowledge = `notion`), provisioned date. For Claude in future sessions. +```` +with: +````text +Notion hub URL (if knowledge = `notion`), the kit release `.claude/` was installed from (`vX.Y.Z (sha)`, the base of the next sync), provisioned date. For Claude in future sessions. +```` + +**Replace** in `.claude/skills/scaffold/references/bootstrap_checklist_template.md` *(whole line, the +verification matrix)*: +````text +| Scaffold output readable | View /blob/main/docs/cbk/scaffold.md | File renders, Cascade metadata reads `Planning backend: github-issues` | ☐ | +```` +with: +````text +| Scaffold output readable | View /blob/main/docs/cbk/scaffold.md | File renders, Cascade metadata reads `Planning backend: github-issues` | ☐ | +| Kit release recorded | Read the **Kit commit** row of docs/cbk/scaffold.md, then run `git ls-remote --tags 'vX.Y.Z^{}'` with its tag | The row reads `vX.Y.Z (sha)`, and the peeled tag the command prints begins with that sha | ☐ | +```` + +**Replace** in `.claude/skills/scaffold/references/test_cases.md` *(inline, Test 1)*: +````text +the committed scaffold.md carries the two-axis Cascade metadata rows +```` +with: +````text +the committed scaffold.md carries the two-axis Cascade metadata rows and a **Kit commit** row reading `vX.Y.Z (sha)`, which the bootstrap checklist's verification matrix confirms against the kit repository's tag +```` + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | tail -2; echo "exit=${PIPESTATUS[0]}"` + +Expected: the second check is now the red one. +``` +cbk-conventions-reference.md § Syncing the kit does not name CHANGELOG.md's Sync notes +verification: block exited 1 +exit=1 +``` + +- [ ] **Step 4: Rewrite § Syncing the kit** + +The section is one paragraph at `74edf84`, and V9 may have qualified its `#58` citation first. Write the new body +to `$S/sync-section.md`, then replace the body between `## Syncing the kit` and `## Verification` with a script +that refuses any body other than the two expected spellings. + +`$S/sync-section.md` (full content): +````markdown +A kit release is an annotated tag `vX.Y.Z` on the kit repository's `main`, and the kit's `CHANGELOG.md` carries one section per release whose **Sync notes** say what a target does by hand. The versioning is informal SemVer: a major bump means a sync needs hand reconciliation beyond `git merge-file`, a minor bump is a harvest, and a patch is fixes only. A target records the release it installed as scaffold's **Kit commit** row, in the form `vX.Y.Z (sha)`: the tag is the readable name, and the sha is authoritative. + +A sync to a newer release is a three-way merge, not a hand-reconciliation. First read the Sync notes of every release after the recorded one, up to and including the release being synced to. Then write the file-by-file table: classify each file as copy / add / merge / keep, and note what each merge must preserve. The table is the reusable artifact; write it before the branch. Then merge each file with `git merge-file `: the base is the kit at the recorded release, ours is the project's copy, and theirs is the new release. On a real application 47 of 48 files the project had customized auto-resolved (pure kit drift, derivable from the base); the conflict hunks were the two big rule files, where the kit's generalized text and the project's filled text both had to survive (context-builder-kit#58, second application, item 10). When the sync merges, record the new release and its sha where the old one was recorded. + +- **Files run ahead of the kit merge against the release that lands them.** A target running files ahead of the kit records them in its own copy of this section, grouped by the kit issue each carries. Its next sync three-way-merges those files against the kit release that lands those issues, not against the recorded install. Where the kit's own application differs from the target's, the kit wins, and the difference is reconciled in that sync. Byte-identity for `copy` rows is asserted against the new release, so a fix a project needs ahead of the kit is filed upstream and carried here as a named exception, never silently patched into a copy. +- **After resolving `settings.json`, diff its hook event keys against the pre-merge copy** (`jq -r '.hooks | keys[]'` on both). A registration that sat inside a conflict hunk is dropped when the kit's side of the hunk is taken, and the diff names it before a session runs without it. +- **Kit-owned code stays out of the target's formatter and linter scope.** `.claude/workflows/**` is the kit's: a `copy` row, byte-identical to its release, so a repo-wide formatter or linter that rewrites it breaks the next sync's byte check. Exclude the tree from every repo-wide formatter and linter, and make the exclusion hold for a path handed over explicitly, as a format-on-edit hook, a pre-commit hook or an editor does. For ruff that is `extend-exclude` plus `force-exclude = true`, because otherwise "Files that are passed to `ruff` directly are always analyzed, regardless of the above criteria" (`https://raw.githubusercontent.com/astral-sh/ruff/main/docs/configuration.md` § Python file discovery, read 2026-09-30; the sentence goes on to except `force-exclude`). Exclusion is not exemption (`cbk-conventions.md` § `[skip ci]` rule): the tree's own gate is the verification block, which runs its fixtures, together with the sync's byte check on `copy` rows. The same rule is stated in `format-on-edit.sh`'s skip floor and in the bootstrap checklist. +- **A project's own hooks sync as `keep` rows, and are held to the kit's contract.** The hook fixture reads every `.claude/hooks/*.sh`, so § Hook authoring's stdin/exit contract binds a target's own guards as it binds the kit's. +- **A project sub-block re-homed into this file from a pre-split contract must split its own retired-vocabulary literals**, or the block's `absent` check matches it. +- **No Kit commit row to read.** A target whose `docs/cbk/scaffold.md` predates the row, and is immutable after its commit, records `vX.Y.Z (sha)` in its own copy of this section, beside the files it runs ahead of the kit. A target installed from an untagged commit records that sha, and reads the Sync notes from the newest release at or before it (`git describe --tags --abbrev=0 ` in a kit clone). +```` + +`$S/sync-apply.py` (full content): +````python +import sys +path, newbody = sys.argv[1], open(sys.argv[2], encoding='utf-8').read() +t = open(path, encoding='utf-8').read() +head, tail = "## Syncing the kit\n\n", "\n\n## Verification\n" +i = t.index(head) + len(head); j = t.index(tail, i) +body = t[i:j] +A = "A target that recorded its **Kit commit** (scaffold's Cascade metadata table) syncs to a newer kit as a three-way merge, not a hand-reconciliation: for every file, `git merge-file ` — base is the kit at the install sha, ours the project's copy, theirs the new kit. On a real application 47 of 48 files the project had customized auto-resolved (pure kit drift, derivable from the base); the conflict hunks were the two big rule files, where the kit's generalized text and the project's filled text both had to survive ({cite}, second application, item 10). The file-by-file table — classify each file as copy / add / merge / keep, then note what each merge must preserve — is the reusable artifact; write it before the branch. Two traps: a project sub-block re-homed into this file from a pre-split contract must split its own retired-vocabulary literals (or the block's `absent` check matches it), and byte-identity for `copy` rows is asserted against the target sha, so a fix a project needs ahead of the kit is filed upstream and carried as a named exception, never silently patched into a copy. Record the new sha in the table when the sync merges." +ok = [A.format(cite=c) for c in ("#58", "context-builder-kit#58")] +if body not in ok: + sys.exit("STOP: § Syncing the kit is neither form this task expects; reconcile before editing") +open(path, 'w', encoding='utf-8').write(t[:i] + newbody.rstrip('\n') + t[j:]) +print("§ Syncing the kit replaced") +```` + +Run: `python3 "$S/sync-apply.py" .claude/rules/cbk-conventions-reference.md "$S/sync-section.md"` +Expected: `§ Syncing the kit replaced`. A `STOP:` line means the section changed under another cluster: +reconcile, never force. + +**Replace** in `.claude/rules/cbk-conventions.md` *(whole line; the pointer under `## Syncing the kit`)*: +````text +Three-way `git merge-file` against the recorded **Kit commit**; the file-by-file table first. → `cbk-conventions-reference.md` § Syncing the kit. +```` +with: +````text +Three-way `git merge-file` against the recorded **Kit commit**, `vX.Y.Z (sha)`; the kit `CHANGELOG.md`'s Sync notes, then the file-by-file table, first. → `cbk-conventions-reference.md` § Syncing the kit. +```` + +- [ ] **Step 5: Verify the ruff quotation raw** + +Run: +```bash +curl -sL https://raw.githubusercontent.com/astral-sh/ruff/main/docs/configuration.md -o "$S/ruff-configuration.md" +grep -cF 'Files that are passed to `ruff` directly are always analyzed, regardless of the above criteria' "$S/ruff-configuration.md" +grep -cx '## Python file discovery' "$S/ruff-configuration.md" +grep -cF 'unless [`force-exclude`](settings.md#force-exclude) is also enabled (via CLI or settings file).' "$S/ruff-configuration.md" +grep -cF 'Files that are passed to `ruff` directly are always analyzed, regardless of the above criteria' .claude/rules/cbk-conventions-reference.md +``` +Expected: `1`, `1`, `1`, `1`. The citation names the URL the quotation matches byte for byte: the raw markdown +source, re-fetched on 2026-09-30, where the sentence is one line under `## Python file discovery` (line 344 that +day) that ends `criteria, ` and continues on the next line as ``unless [`force-exclude`](settings.md#force-exclude) +is also enabled (via CLI or settings file).`` The quotation is therefore the sentence's first clause only: the whole +sentence crosses a line break and a link, and never matches the raw source under `grep -F`. The bullet states the +`force-exclude` exception in its own words, outside the quotation marks. (The rendered page, +`https://docs.astral.sh/ruff/configuration/`, carries the same sentence with the backticks around `ruff` dropped, +so the backticked quotation does not match it.) If the third count is `0`, the exception moved: re-read the section +and correct the bullet's last clause. If the day is not 2026-09-30, restamp this one citation: +`today=$(date -u +%F); sed -i "s|(\`https://raw.githubusercontent.com/astral-sh/ruff/main/docs/configuration.md\` § Python file discovery, read 2026-09-30;|(\`https://raw.githubusercontent.com/astral-sh/ruff/main/docs/configuration.md\` § Python file discovery, read $today;|" .claude/rules/cbk-conventions-reference.md` + +- [ ] **Step 6: Run the gate** + +Run: +```bash +for h in .claude/hooks/*.sh .claude/hooks/lib/*.sh; do bash -n "$h" || echo "bash -n failed: $h"; done +bash .claude/workflows/tests/run-verification-block.sh 2>&1 | grep -E 'always-loaded total|^verification: '; echo "exit=${PIPESTATUS[0]}" +wc -l < .claude/skills/scaffold/SKILL.md +``` +Expected: no `bash -n failed` line; `always-loaded total: bytes` with N exactly 59 above the total the previous +commit printed; `verification: kit sub-block complete`; `verification: done`; `exit=0`; the SKILL.md line count +under 500 (337 at `74edf84`). + +- [ ] **Step 7: Commit** + +```bash +git add .claude/rules/cbk-conventions-reference.md .claude/rules/cbk-conventions.md \ + .claude/skills/scaffold/SKILL.md .claude/skills/scaffold/references/scaffold_output_template.md \ + .claude/skills/scaffold/references/bootstrap_checklist_template.md .claude/skills/scaffold/references/test_cases.md +git commit -F - <<'EOF' +feat(V10): § Syncing the kit speaks in versions; scaffold records the kit release as vX.Y.Z (sha) + +A kit release is an annotated tag, and a sync reads CHANGELOG.md's Sync notes, writes the +file-by-file table, then merges every file against the recorded release. The section gains the +rules the applications taught: files run ahead of the kit merge against the release that lands +them, the settings.json hook-event diff, kit-owned code kept out of a target's formatter and +linter, a project's own hooks held to the kit's contract, and the fallback for a scaffold.md +that predates the Kit commit row. Scaffold's row, its guidance, its SKILL list, the bootstrap +checklist and Test 1 take the vX.Y.Z (sha) form; the block pins the row and the section. + +Trace: release/1, release/2, #69/c5901496433/sync-note, #69/c5859756889/apply-h4/2, +#69/c5859756889/apply-h4/3, #71/body/1 (the § Syncing clause, handed from V8), release/5 (this +section's own citation). + +Co-Authored-By: Claude Opus 5.5 +EOF +``` + +### Task V10.2: `CHANGELOG.md` — v0.1.0 to v1.0.0, each with its Sync notes (release/6, #67/c5881158070/2c, #69/c5859756889/apply-h4/5, #69/c5901496433/sync-note, #69/c5859756889/apply-h4/1, apply-h4/2, apply-h4/3, apply-h4/6, #70/table/mise-mcp-row, #60/c5881158391/status; D41, D50; Review Focus 1) + +**Files:** +- Create: `CHANGELOG.md` +- Modify: `.claude/rules/cbk-conventions-reference.md` — one check at the kit sub-block's sentinel + +**Interfaces:** +- Consumes: every V1–V9 landing the v1.0.0 section describes (Step 0 checks each one it names), and Task V10.1's + § Syncing the kit, which the Sync notes cite. +- Produces: headings of the form `## [X.Y.Z] — — (PR #)`, newest first, each followed by + `### What landed` and `### Sync notes`, under an `## [Unreleased]` heading. The master plan's Task F4 previews + the `[1.0.0]` Sync notes in the PR body; Task F5 tags `v0.1.0`–`v1.0.0` with these headings as messages and + publishes the `[1.0.0]` section with `awk '/^## \[1\.0\.0\]/{p=1;next} /^## \[/{p=0} p' CHANGELOG.md`. +- The `[1.0.0]` heading's date and PR number are predictions: this task writes the execution day's UTC date and + the next free number. The master plan's Task F4 owns both: it confirms the PR number `gh pr create` returns, and + re-dates the heading to the merge's UTC date (§ Handed to other clusters). +- Produces: the v1.0.0 always-loaded figure, measured after V10.1 (the last V10 change to an always-loaded rule). +- Produces: the block sentence `CHANGELOG.md has no [] section with a ### Sync notes heading`. +- Numbers such as `#59` in this file are this repository's pull requests and issues, written bare on purpose: + `CHANGELOG.md` is outside the drop-in set, and its `[1.0.0]` section is the GitHub Release text, where a bare + number links to this repository. D53's hazard, a bare number linking to a target's own issue, cannot arise here. + +- [ ] **Step 0: Confirm what the v1.0.0 section says landed** + +Run: +```bash +t=.claude/workflows/tests +for f in $t/run-verification-block-fixture.sh $t/hook-guards-fixture.sh $t/hook-payloads-fixture.sh \ + $t/protected-paths-hook-fixture.sh $t/adr-ci-body-fixture.sh $t/review-assert-fixture.sh $t/review-trigger-fixture.py \ + $t/run-arms-headless-fixture.sh .claude/hooks/lib/resolve-path.sh .claude/rules/knowledge-backend-reference.md \ + .claude/workflows/finish-ab/run-arms-headless.py; do [ -f "$f" ] || echo "missing: $f"; done +grep -q 'project sub-block complete' .claude/workflows/tests/run-verification-block.sh || echo "no third rail" +grep -q 'stop_hook_clean' .claude/rules/cbk-conventions-reference.md || echo "no stop_hook_clean" +grep -q '140000' .claude/rules/cbk-conventions-reference.md || echo "no budget warning" +[ "$(jq '[.hooks[][] | .hooks[] | select(has("args") | not)] | length' .claude/settings.json)" = 0 ] || echo "a registration is not exec form" +jq -r '.enabledPlugins | keys | join(" ")' .claude/settings.json +[ "$(grep -l '^disable-model-invocation: true' .claude/commands/*.md | wc -l | tr -d ' ')" = 5 ] || echo "not all five commands are invoke-only" +grep -q 'silently replaces' .claude/rules/tooling.md && echo "tooling.md still says a local list replaces" +for f in .claude/skills/blueprint/references/templates/claude-review.yml .claude/skills/blueprint/references/templates/claude.yml; do grep -q 'Record the resolved model' "$f" || echo "no record-model step: $f"; done +grep -rn 'three surfaces + resolution order' .claude/ && echo "the old heading is still cited" +grep -q 'dispatch surfaces + resolution order' .claude/rules/orchestration.md || echo "no renamed heading" +grep -q 'Opus 5.5' .claude/rules/orchestration.md || echo "orchestration.md does not name Opus 5.5" +jq -r '.mcpServers | keys | join(" ")' .mcp.json.example +grep -q 'uvx' .mcp.json.example || echo "time is not pinned through uvx" +grep -q '"type": "http"' .mcp.json.example || echo "no hosted server carries type http" +grep -q 'READ_ONLY' .claude/workflows/review-sweep.js || echo "review-sweep has no read-only clause" +grep -q ':(glob)' .github/workflows/adr-immutability-check.yml || echo "the ADR job has no :(glob) body" +grep -q '^## `.gitignore`' .claude/skills/scaffold/references/github-starter-templates.md || echo "no .gitignore starter section" +grep -rqF '!/.claude/hooks/lib/' .claude/ || echo "the lib/ gitignore trap is not named" +for f in .claude/hooks/format-on-edit.sh .claude/hooks/analyze-on-edit.sh; do grep -q '"args"' "$f" || echo "Register: stanza not exec form: $f"; done +grep -q 'CONTRIBUTING.md` § Branches' .claude/commands/finish-procedure.md && echo "finish-procedure still cites CONTRIBUTING.md § Branches" +grep -qF '[task_config]' .claude/skills/blueprint/references/templates/tooling.md || echo "blueprint's tooling template has no [task_config] block" +grep -qF "'claude-fable-5'" .claude/workflows/agent-cost.py || echo "agent-cost.py does not key legacy Fable 5 by version" +for k in WRITE_SERVERS worktree_setup; do grep -q "$k" .claude/workflows/finish-ab/run-arms-headless.py 2>/dev/null || echo "run-arms-headless.py has no $k"; done +grep -q 'resolve-path\.sh' .claude/hooks/protect-immutable-adrs.sh || echo "the ADR guard does not source the helper" +grep -q 'Hook backstop slots' .claude/skills/scaffold/references/bootstrap_checklist_template.md || echo "no Hook backstop slots row" +grep -qF "[the project's CI lockfile check" .claude/hooks/protect-lock-files.sh || echo "no lockfile backstop slot" +[ "$(grep -cF "[the base branch's ruleset" .claude/hooks/protect-main-branch.sh)" -ge 2 ] || echo "protect-main-branch.sh lacks a ruleset slot in both warnings" +grep -qF '[ -f docs/cbk/blueprint.md ]' .claude/rules/cbk-conventions-reference.md || echo "the CLAUDE.md check does not key on docs/cbk/blueprint.md" +grep -q "That field follows the Bash tool's" .claude/hooks/require-repo-root-for-agents.sh || echo "P1 landed as variant A: Step 5 swaps the cd note" +grep -q '^## Relation grains$' .claude/skills/adr-new/SKILL.md || echo "adr-new has no ## Relation grains" +grep -q 'Any issue a PR closes' .claude/rules/cbk-conventions.md || echo "cbk-conventions.md has no D54 branch rule" +grep -qF '[actions skip]' .claude/rules/cbk-conventions.md || echo "the CI-skip trap does not name every token" +grep -qF '.github/ISSUE_TEMPLATE/cascade-meta.md' .claude/rules/cbk-conventions-reference.md || echo "no project check on the meta issue template" +for f in cascade-meta.md cascade-rough-in.md; do git diff --quiet 74edf84 -- ".claude/skills/scaffold/references/issue-templates/$f" && echo "issue template unchanged since 74edf84: $f"; done +``` +Expected: exactly two lines of output, `commit-commands@claude-plugins-official pr-review-toolkit@claude-plugins-official` +and `context7 linear notion time`, and nothing else, when V2 applied probe P1's variant B (the planning dry runs' +outcome). Under variant A a third line reads `P1 landed as variant A: Step 5 swaps the cd note`, and Step 5 handles +it. Any other line names a sentence of the v1.0.0 section below that does not hold: stop, and reconcile that +sentence with what the cluster actually landed before writing it. + +- [ ] **Step 1: Add the failing check to the block** + +**Replace** in `.claude/rules/cbk-conventions-reference.md` *(whole line; the kit sub-block's sentinel)*: +````text +echo "verification: kit sub-block complete" +```` +with: +````text +# Releases (V10): on the kit tree, CHANGELOG.md has a section for every tagged release, each with its Sync notes +# (a target's CHANGELOG, if it has one, is its own). +if [ ! -f docs/cbk/scaffold.md ]; then for v in 0.1.0 0.2.0 0.3.0 0.4.0 0.5.0 1.0.0; do awk -v v="$v" 'index($0, "## [" v "] ")==1{p=1;next} /^## \[/{p=0} p&&/^### Sync notes$/{f=1} END{exit !f}' CHANGELOG.md || { echo "CHANGELOG.md has no [$v] section with a ### Sync notes heading"; exit 1; }; done; fi + +echo "verification: kit sub-block complete" +```` + +- [ ] **Step 2: Run the block — red, no changelog** + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | tail -3; echo "exit=${PIPESTATUS[0]}"` + +Expected (the first line is the awk's own error, and its wording depends on the awk installed; this is mawk's): +``` +awk: cannot open "CHANGELOG.md" (No such file or directory) +CHANGELOG.md has no [0.1.0] section with a ### Sync notes heading +verification: block exited 1 +exit=1 +``` +Record the second line in the red-first table. + +- [ ] **Step 3: The simulated sync (Review Focus 1) — red** + +A filled target's copies are merged three ways, from the kit at harvest 4 (`74edf84`) to this branch's +tree, and every file that conflicts must be named in the v1.0.0 Sync notes. The target is `j4th/echosphere`, a +Linear-axis target that synced `74edf84` and then ran ahead of it. Besides every tracked `.claude/` file, the +simulation maps the target's review workflows onto blueprint's templates, its two committed issue templates +(`.github/ISSUE_TEMPLATE/cascade-meta.md`, `cascade-rough-in.md`) onto scaffold's, and its `.mcp.json` onto +`.mcp.json.example`. A mapped file the target does not carry is skipped, as an unmapped one is. + +`$S/sim-sync.sh` (full content): +````bash +#!/usr/bin/env bash +# Simulated sync (the master plan's Review Focus 1). A filled target's copies of the kit's +# files are merged three ways, from the kit at harvest 4 (74edf84) to this branch's working tree. +# Every file that conflicts must be named, as a backticked path, in CHANGELOG.md's [1.0.0] Sync notes. +# Usage: bash sim-sync.sh . Writes only under mktemp -d. +set -uo pipefail +kit=$1 tgt=$2 ref=origin/main base=74edf84 +work=$(mktemp -d) || exit 2 +trap 'rm -rf "$work"' EXIT +v1=$(awk '/^## \[1\.0\.0\] /{p=1;next} /^## \[/{p=0} p' "$kit/CHANGELOG.md" 2>/dev/null) +notes=$(awk '/^### Sync notes$/{p=1;next} /^### /{p=0} p' <<<"$v1") +[ -n "$notes" ] || { echo "FAIL: CHANGELOG.md has no [1.0.0] section with Sync notes"; exit 1; } +{ git -C "$kit" ls-files .claude | awk '{print $0" "$0}' + printf '%s\n' '.github/workflows/adr-immutability-check.yml .github/workflows/adr-immutability-check.yml' \ + '.github/workflows/claude-review.yml .claude/skills/blueprint/references/templates/claude-review.yml' \ + '.github/workflows/claude.yml .claude/skills/blueprint/references/templates/claude.yml' \ + '.github/ISSUE_TEMPLATE/cascade-meta.md .claude/skills/scaffold/references/issue-templates/cascade-meta.md' \ + '.github/ISSUE_TEMPLATE/cascade-rough-in.md .claude/skills/scaffold/references/issue-templates/cascade-rough-in.md' \ + '.mcp.json .mcp.json.example'; } > "$work/pairs" +merged=0 conf=0 unnamed=0 +while read -r tp kp; do + git -C "$tgt" cat-file -e "$ref:$tp" 2>/dev/null || continue # the target lacks it: an add row, no merge + [ -f "$kit/$kp" ] || continue # the kit no longer ships it: no merge + git -C "$tgt" show "$ref:$tp" > "$work/ours" + cp "$kit/$kp" "$work/theirs" + if git -C "$kit" cat-file -e "$base:$kp" 2>/dev/null; then git -C "$kit" show "$base:$kp" > "$work/base"; kind=merge + else : > "$work/base"; kind=ahead-of-kit; fi + merged=$((merged+1)) + rc=0; git merge-file -q -p "$work/ours" "$work/base" "$work/theirs" > "$work/out" || rc=$? + [ "$rc" -eq 0 ] && continue + [ "$rc" -le 127 ] || { echo "ERROR: git merge-file exited $rc on $tp"; exit 2; } + conf=$((conf+1)) + if grep -qF "\`$tp\`" <<<"$notes"; then echo "CONFLICT ($kind, named): $tp" + else echo "UNNAMED ($kind): $tp"; unnamed=$((unnamed+1)); fi +done < "$work/pairs" +echo "simulated sync: $merged files merged, $conf conflicted, $unnamed not named in the v1.0.0 Sync notes" +[ "$unnamed" -eq 0 ] +```` + +Run: `gh repo clone j4th/echosphere "$S/echosphere" -- -q && bash "$S/sim-sync.sh" "$PWD" "$S/echosphere"; echo "exit=$?"` + +Expected: +``` +FAIL: CHANGELOG.md has no [1.0.0] section with Sync notes +exit=1 +``` +The script was mutation-tested while planning: with a hunk of `.claude/rules/logging.md` changed under the +target's stamped glob and the file left unnamed, it printed `UNNAMED (merge): .claude/rules/logging.md` and +exited 1. + +- [ ] **Step 4: Verify the alias and action-release facts raw** + +Run: +```bash +curl -sL https://code.claude.com/docs/en/model-config.md -o "$S/model-config.md" +norm "$S/model-config.md" | grep -cF '| v2.1.284 | `sonnet` resolves to Sonnet 5.5 on the Anthropic API |' +norm "$S/model-config.md" | grep -cF '| v2.1.280 | `opus` resolves to Opus 5.5 on the Anthropic API' +for v in v1.0.231 v1.0.232 v1.0.235 v1.0.236; do printf '%s ' "$v"; curl -sL "https://raw.githubusercontent.com/anthropics/claude-code-action/$v/src/entrypoints/run.ts" | grep -F 'const claudeCodeVersion'; done +``` +Expected: `1`, `1`, then +``` +v1.0.231 const claudeCodeVersion = "2.1.278"; +v1.0.232 const claudeCodeVersion = "2.1.280"; +v1.0.235 const claudeCodeVersion = "2.1.283"; +v1.0.236 const claudeCodeVersion = "2.1.284"; +``` +(v1.0.232 is the first release that installs 2.1.280, and v1.0.236 the first that installs 2.1.284.) If any line +differs, correct the `**The review model is the action's pin.**` note to what the fetch shows. + +- [ ] **Step 5: Write `CHANGELOG.md`** + +Full content: +````markdown +# Changelog + +Every release of the kit, newest first. A release is an annotated tag `vX.Y.Z` on `main`. The versioning is informal SemVer: a major bump means a sync needs hand reconciliation beyond `git merge-file`, a minor bump is a harvest, and a patch is fixes only. Numbers such as #59 are this repository's pull requests and issues. + +Each release's **Sync notes** say what a target does by hand when its sync crosses that release. Read the notes of every release after the one your **Kit commit** row records, then follow `.claude/rules/cbk-conventions-reference.md` § Syncing the kit. Each harvest's design is under `docs/superpowers/specs/`. + +## [Unreleased] + +Nothing yet. + +## [1.0.0] — 2026-09-30 — harvest 5 (PR #75) + +The first release to be tagged; the five before it are tagged on their merge commits after the fact. It closes #58 and #60–#74, and the findings of a review of the whole kit. Design: `docs/superpowers/specs/2026-09-30-cascade-kit-harvest-5-design.md`, with one trace row per ask beside it. + +### What landed + +- **The verification block as a gate.** `run-verification-block.sh` gains a third rail: in a filled target, where `docs/cbk/scaffold.md` exists, the output must also carry `verification: project sub-block complete`. The Stop-hook check reds a hook that could not look. Blueprint's tooling template gives `check` a verification task, light mode included. The budget line warns above 140,000 bytes. +- **Guards that resolve the path they guard.** `.claude/hooks/lib/resolve-path.sh`, a sourced helper, resolves a path lexically and physically and compares device and inode. The ADR guard is rewritten onto it. A hard-deny guard refuses a payload `jq` cannot parse, and an ask-gate asks. The main-branch and PR-state guards match a command behind `bash -c`, quoting, a full path or a chained verb. Every fail-open warning names its surviving backstop. The ADR CI job reads a `:(glob)` pathspec, diffs three-dot from the merge base and fails closed on any git error. Three behavioural hook fixtures run in the block. +- **Claude Code conformance.** The commands declare named `arguments:` and carry `disable-model-invocation: true`. Every hook registration is exec form (`"args": []`). `enabledPlugins` keys read `@claude-plugins-official`. `tooling.md` states how settings lists merge across scopes. `/intake` and `/pr-respond` treat report and comment text as data. +- **Review-bot templates.** The family aliases `opus` and `sonnet`, with an explicit `--effort` on every branch. A step records the model that ran. The "Assert the review posted" step is rebuilt and has a fixture. A `paths:` trigger still reviews markdown under `.claude/` and `docs/adr/`, and has a fixture. A fork guard, concurrency at job level, a prompt that applies the base branch's rules, and label checks fed as here-strings. +- **Recalibration for Opus 5.5 and Sonnet 5.5.** `orchestration.md` carries effort defaults per model and surface, the ladder from Opus 5.5, and current prices and cache-read rates. `§ The three surfaces + resolution order` is now `§ The dispatch surfaces + resolution order` in both halves. `knowledge-backend.md` splits into a contract and a path-scoped `knowledge-backend-reference.md`. `workflows.md` records why `/finish` runs inline. +- **Harness.** `finish-ab.js` runs two to four arms, each with its own model and effort, under a balanced judge panel. `run-arms-headless.py` runs each arm as a headless session in its own worktree. `agent-cost.py` keys its prices and cache-read rates by model version. +- **Review conventions.** `review-sweep.js` deduplicates on file and line and carries every title, takes finders that bring their own prompt, and tells every find and verify agent to stay read-only. +- **Target hygiene.** A `.gitignore` harness block for targets. What Dependabot covers for container images, and its three gaps. mise tasks run under bash with `pipefail`. Kit-owned code stays out of a target's formatter and linter. `.mcp.json.example` is rebuilt: `${VAR}` references, `type: "http"` on hosted servers, Linear's hosted endpoint, `time` pinned through `uvx`, and no GitHub server, because `gh` is the kit's GitHub interface. #70's mise and MCP rows land in blueprint's tooling template and in `.mcp.json.example`; no `mise.toml` template ships. +- **Consistency.** Citations point at headings every target has. Kit issues are cited `context-builder-kit#N`. `/finish` Step 1 admits five title forms. `adr-new` reads the index's rows in both of their forms. +- **The release.** This changelog. `README.md`'s Quick start installs the drop-in set from a tagged archive and prints the release to record. § Syncing the kit speaks in versions, and scaffold's **Kit commit** row reads `vX.Y.Z (sha)`. +- Always-loaded rules: 130,628 bytes at v0.5.0, @@ALWAYS_LOADED@@ bytes at v1.0.0. Checked on Claude Code 2.1.285. + +### Sync notes + +- **Hand-merge these files.** This release rewrites text a filled target changes in each of them. Merge each three ways from the release your Kit commit row records, and keep what the project filled: + - `.github/workflows/claude-review.yml` and `.github/workflows/claude.yml`, your copies of blueprint's review templates. Keep your label names, turn cap, sizing, allowed tools and the filled `REVIEW_LOGIN`. Take the model and effort lines (`claude.yml` now runs `--effort high`), the `Record the resolved model` step, the rebuilt "Assert the review posted" step, the fork guard, `concurrency` moved into the job, `defaults: run: shell: bash`, `runs-on: ubuntu-24.04`, the step ids `review` and `claude`, the prompt's restore section with its two new bracketed fills, and the notes above `claude_args`. The `paths:` filter replaces `paths-ignore`: put your own prose-only negations above the re-includes of `.claude/**` and `docs/adr/**`. The kit sub-block now runs `review-trigger-fixture.py` and `review-assert-fixture.sh` against your filled `.github/workflows/claude-review.yml`, so a target that syncs the rules before merging this workflow is red until it does. + - The CI workflow blueprint generated from its skeleton (usually `.github/workflows/ci.yml`). It was never a kit copy, so there is nothing to merge: edit it to match the skeleton. That means `defaults: run: shell: bash`, `runs-on: ubuntu-24.04`, and every `uses:` pinned to a full commit SHA with a trailing version comment, in place of a tag such as `actions/checkout@v4`. + - `.mcp.json`, your copy of `.mcp.json.example`. Keep the servers your axes use. The kit ships no GitHub entry, hosted entries carry `"type": "http"`, and `time` runs pinned through `uvx`. Turn every literal credential into a `${VAR}` reference, export the variable before launching `claude`, and commit the file. The kit sub-block now checks the committed file's shape: a url entry without a type, an unpinned `npx`, `uvx` or `bunx` server, or a literal credential turns it red. + - `.claude/rules/orchestration.md` and `.claude/rules/orchestration-reference.md`. Keep your posture row and your dated applied instances. Re-point any citation of `§ The three surfaces + resolution order` to `§ The dispatch surfaces + resolution order`. + - `.claude/rules/tooling.md`. Keep your filled stack sections; take § MCP configuration's merge rule and § Automated review on the git host. + - `.claude/settings.json`. Keep your own hooks' registrations, rewritten in exec form, and re-add their names and tiers to `_comment_hooks`. Then diff the hook event keys against your pre-merge copy (§ Syncing the kit). + - `.claude/rules/cbk-conventions.md` and `.claude/rules/cbk-conventions-reference.md`. Keep your filled values and your project sub-block; take everything in the block above it. In `cbk-conventions.md`, take § Branch naming's rule that any issue a PR closes, cascade or not, puts its key in the branch (the bare issue number on the `github-issues` axis; `/` only for work no issue tracks) with its Quick reference row, and the reworded substring trap in the CI-skip rule's section, which names all five skip tokens and GitHub's skip trailer and cites GitHub's page. The kit sub-block pins both, so a copy that keeps the old wording is red. + - `.claude/rules/pr-review.md` and `.claude/rules/pr-review-reference.md`. Keep your roster entries; take the floor, the sweep and the invariants. + - `.claude/rules/simplification.md` and `.claude/rules/workflows.md`. Take the kit's text; keep a filled § Cost+scope-explicit. + - `.claude/rules/knowledge-backend.md`. It is now two halves. On the `notion` axis, take both, `knowledge-backend.md` and `.claude/rules/knowledge-backend-reference.md`, and move each filled value under its heading. On the `none` axis, where the rule was deleted, add neither half. + - `.claude/commands/finish-procedure.md` and `.claude/skills/rough-in/references/finish-procedure.md`. Their citations now point at the conventions: a copy whose `CONTRIBUTING.md` citation was filled by hand takes the kit's text. + - `.claude/skills/adr-new/SKILL.md`. Take the kit's text: it reads the index's rows in both forms, and its `## Refines vs Supersedes vs Extends` section is now `## Relation grains`, a pointer to `.claude/rules/cbk-conventions-reference.md` § ADR relation grains. Repoint any citation of "adr-new § Refines vs Supersedes" in your filled rules, reviewers or docs at `adr-new` § Relation grains or § ADR relation grains; an ADR that cites it stays as written, and the correction goes in `docs/adr/corrections.md`. + - `.claude/hooks/format-on-edit.sh` and `.claude/hooks/analyze-on-edit.sh`. Keep your filled case arms; take the skip floor, which now names `.claude/workflows/`, and the exec-form `Register:` stanza. + - `.github/workflows/adr-immutability-check.yml`. Re-copy it from the kit, then restore only your comments and the pinned job name: the job body is replaced. It reads a `:(glob)` pathspec, diffs three-dot from the merge base with `--no-renames`, checks out blobless, runs under `shell: bash` on a named runner image, and fails closed on any git error. +- **Files a target may already run ahead of the kit.** These carry this release's harness and guard ports: `.claude/workflows/agent-cost.py`, `.claude/workflows/tests/agent-cost-fixture.sh`, `.claude/workflows/finish-ab/finish-ab.js`, `.claude/workflows/tests/finish-ab-shape.mjs`, `.claude/workflows/finish-ab/run-arms-headless.py`, `.claude/workflows/tests/run-arms-headless-fixture.sh`, `.claude/workflows/review-sweep.js`, `.claude/workflows/tests/review-sweep-accounting.mjs`, `.claude/hooks/protect-immutable-adrs.sh` and `.claude/hooks/lib/resolve-path.sh`. For each one your target already carries, diff your copy against v1.0.0's. Where the kit's application differs from yours, take the kit's, and keep only your project's own fills. From this sync on, v1.0.0 is these files' merge base, not your install (§ Syncing the kit). +- **New files.** Add `.claude/hooks/lib/resolve-path.sh`, which is sourced, never run. A `.gitignore` with an unanchored `lib/` line hides it from `git add` without a word: add `!/.claude/hooks/lib/` after that line, and after the sync commits check that `git ls-files .claude/hooks/lib/` names the helper (if it does not, `git check-ignore -v .claude/hooks/lib/resolve-path.sh` names the line that hides it). Add the new fixtures under `.claude/workflows/tests/`, each an `add` row: the three hook fixtures `protected-paths-hook-fixture.sh`, `hook-guards-fixture.sh` and `hook-payloads-fixture.sh`; `adr-ci-body-fixture.sh`; `review-assert-fixture.sh` and `review-trigger-fixture.py`; `run-verification-block-fixture.sh`; and `extract-run-block.sh`, which the ADR and review-assert fixtures source. The block runs every one of them but the sourced helper. Append the harness block from scaffold's `references/github-starter-templates.md` § `.gitignore` to your `.gitignore`, below every stack section, and state its pin assertions in the commit body; the kit's own `.gitignore` is not in the drop-in set. +- **Kit-owned code stays out of your formatters.** Exclude `.claude/workflows/**` from every repo-wide formatter and linter, forced for explicit paths (§ Syncing the kit). A wired `format-on-edit.sh` merges the new skip-floor line. +- **mise runs tasks under bash with `pipefail`.** A target on mise adds the `[task_config]` block from blueprint's `templates/tooling.md` step 1 to its `mise.toml` and pins mise ≥ 2026.7.15 wherever tasks run, CI's setup action included. Nothing in the kit checks a target's `mise.toml`. +- **Dependabot covers more than the settle-window said.** `rust-toolchain.toml` is covered by Dependabot's `rust-toolchain` ecosystem, and so is a container base-image tag; a target whose filled § Dependency settle-window lists either as uncovered corrects that section, which now names the three image gaps. +- **The cost reader keys by model version.** A target synced at v0.5.0 prices legacy Fable 5 cache reads at Fable 5.1's 0.025x until it takes v1.0.0's `agent-cost.py`, which reads them at the standard 0.1x. A target that copies `run-arms-headless.py` sets its config's `worktree_setup` to its own per-worktree step, and adds any write-capable MCP server it wires beyond the kit's `github`, `linear` and `notion` to `WRITE_SERVERS`, which makes the file a `merge` row in its sync table. +- **A filled target owes both sentinels.** `.claude/workflows/tests/run-verification-block.sh` is a `copy` row with a third rail: it fails a target whose output lacks `verification: project sub-block complete`. `.claude/workflows/tests/run-verification-block-fixture.sh`, which the block runs against the runner, is an `add` row. Wire the runner as a verification task your `check` depends on, per blueprint's `templates/tooling.md`. If you never stamped the bracketed manifest-and-lockfile globs in `cbk-conventions-reference.md`'s `paths:`, stamp them now (the bootstrap checklist's disposition pass has the row), or the project sub-block is red. The block's `CLAUDE.md` check now fires in a target only once `docs/cbk/blueprint.md` exists: from then on your `CLAUDE.md` must mention `cbk-conventions` as a backticked path, never an `@` import. +- **Fill the three hook backstop slots.** A guard's fail-open warning names the backstop that still stands, and three of them are the project's to name: `[the project's CI lockfile check — …]` in `protect-lock-files.sh`, and `[the base branch's ruleset — pull requests only — where one exists]` in both warnings of `protect-main-branch.sh`. The bootstrap checklist's disposition pass gains a **Hook backstop slots** row: replace each bracket with the real check or ruleset, or with `none` and the reason. The project sub-block refuses a slot left bracketed. A filled slot makes those two hooks `merge` rows in your sync table. +- **The commands no longer start from a description.** `/finish`, `/intake`, `/enrich`, `/pr-respond` and `finish-procedure` carry `disable-model-invocation: true`: type them. +- **Kit issue citations are qualified.** Kit text cites its own issues as `context-builder-kit#N`, because a bare number links to the target's own issue. The rewrite touches about thirteen files a target copies byte for byte, among them the hooks, the workflow scripts and their tests, the executor pair and `adr-new`: take the kit's side of every such hunk. A copy you patched by hand takes the kit's form. The kit sub-block's bare-citation check runs on the kit's own tree only; in yours a bare number is your own issue. +- **Re-copy the issue templates.** `.github/ISSUE_TEMPLATE/cascade-meta.md` and `.github/ISSUE_TEMPLATE/cascade-rough-in.md` are byte copies of scaffold's `references/issue-templates/`, and both changed: re-copy them. The project sub-block is red while your `cascade-meta.md` still cites the retired § Deferred meta-issues. +- **An unreadable payload now denies.** A hard-deny guard refuses a payload `jq` cannot parse, and an ask-gate asks. A missing `jq` still fails open, naming its backstop. +- **The ADR guard judges what a path is, not how it is spelled.** `protect-immutable-adrs.sh` sources `lib/resolve-path.sh`, resolves the path lexically and physically, and denies an edit to an existing numbered ADR in any checkout, however the path reaches it. A target that customized its ADR hook takes the kit's hook (a `copy` row) plus the helper (an `add` row), carries anything it still needs as a named exception (§ Syncing the kit), and checks the helper is tracked (**New files**, above). +- **A dispatch after a `cd` is judged where the shell is.** A hook payload's `cwd` follows the Bash tool's `cd` (the probe is recorded in `require-repo-root-for-agents.sh`'s `Timing:` paragraph). So the launch-root guard now denies an Agent, Task or Workflow dispatch made after a `cd` into a subdirectory, and `protect-main-branch.sh` judges a commit against the checkout the shell is in. Return to the repository root, as its own command, before dispatching. +- **The review model is the action's pin.** The templates name the family aliases, so a review runs whatever model the pinned `anthropics/claude-code-action` release's Claude Code resolves them to. `opus` resolves to Opus 5.5 on the Anthropic API from Claude Code v2.1.280, and `sonnet` to Sonnet 5.5 from v2.1.284 (`https://code.claude.com/docs/en/model-config` § Version history, read 2026-09-30). The action installs Claude Code 2.1.280 from its v1.0.232 and 2.1.284 from its v1.0.236 (`src/entrypoints/run.ts`, `claudeCodeVersion`, read 2026-09-30). A workflow pinned below v1.0.232 still reviews on the previous model under `opus` (Opus 5 from Claude Code v2.1.219), and below v1.0.236 on Sonnet 5 under `sonnet`: bump the pinned SHA. + +## [0.5.0] — 2026-09-22 — harvest 4 (PR #59) + +The kit as applied: what two targets' syncs of v0.4.0 found. It closes #33 and lands every finding on #58 that had a proven fix; #58 stays open for its residue, which v1.0.0 closes. Design: `docs/superpowers/specs/2026-09-21-cascade-kit-harvest-4-design.md`. + +### What landed + +- **Settings liveness.** The two `_example_PostToolUse_*` objects are deleted from `settings.json`; an advisory hook's registration stanza lives in its header's `Register:` line. The block asserts that no top-level key holds a hook-shaped object. +- **The hook stdin/exit contract.** Every hook drains stdin first and decides on here-strings, never on a pipeline whose reader can exit first. `hook-contract-fixture.sh` checks both structurally over every hook and probes the decision sites past the pipe buffer. +- **The exercised fork detector**, with its prune rules, its staging-copy prune and its degrade path under a fixture probe. +- **The verification block's runner**, `run-verification-block.sh`, with two fail-loud rails, and the kit's own CI, `verify.yml`, which runs it on every pull request. `ADVISORY_WIRED` declares a target's wired advisory hooks. +- **Harness fixes.** Per-tier cache reads in `agent-cost.py`; `review-sweep.js`'s boundary-safe hint match, roster-read `.catch` and retry reindex; `finish-ab.js`'s distinct arm labels. +- **Records.** `/finish` Step 1 admits `[:meta]`. `adr-new` writes the index row in the index's own form. Scaffold's **Kit commit** row and § Syncing the kit. +- **Templates.** Both review workflows grant `checks: read` twice, fetch the base ref after the head checkout, and degrade explicitly past the turn cap. The knowledge-backend matcher is widened to the live tool set. + +### Sync notes + +- Delete any `_example_*` key, and any other hook-shaped top-level key, from `settings.json`. An advisory hook's stanza is copied from its header's `Register:` line into `hooks.PostToolUse`. +- After resolving `settings.json`, diff its hook event keys against the pre-merge copy (`jq -r '.hooks | keys[]'` on both). A registration inside a conflict hunk is lost when the kit's side is taken. +- A `docs/cbk/scaffold.md` committed before this release has no Kit commit row, and it is immutable. Record the release in your copy of § Syncing the kit instead. +- Your own hooks are held to the stdin/exit contract too. `hook-contract-fixture.sh` reads every `.claude/hooks/*.sh`, so a project hook that exits before draining stdin turns the block red: make `input="$(cat)"` its first statement. +- Wire the runner, not an inline copy of the extraction: the task your `check` depends on runs `bash .claude/workflows/tests/run-verification-block.sh`. +- Name every advisory hook you wired in the project sub-block's `ADVISORY_WIRED`. +- A review workflow that already grants `checks: read` (to the job and to the action's token) and already runs `gh pr diff` merges only the degrade clause. Size its N against `--max-turns`. + +## [0.4.0] — 2026-09-07 — harvest 3 (PRs #54–#57) + +Four PRs: the kit stops being wrong about itself (#54); the review gate is a floor (#55); the producing phases become contract-first (#56); the github-issues axis, the records and the hygiene (#57). Design: `docs/superpowers/specs/2026-09-06-cascade-kit-harvest-3-design.md`. + +### What landed + +- **The instruction budget.** `cbk-conventions.md`, `orchestration.md` and `pr-review.md` each split into an always-loaded contract and a path-scoped `-reference.md` half, and every moved section keeps a pointer heading. `logging.md` and `testing.md` carry `paths:` globs to stamp. Scaffold's bootstrap checklist gains a rule-file disposition pass. +- **The review floor.** `/simplify` and `pr-review-toolkit:review-pr` must both run, once, as skills, recorded in the PR body's `## Review gate` block. The sweep beside them is bounded and reads its reviewer roster at run time. +- **Reviewer memory does not fork.** A hard-deny hook refuses an agent dispatch from outside the repository root, and a Stop hook blocks the hand-off while a stray memory tree exists. Every hook registration takes the `${CLAUDE_PROJECT_DIR}` form. +- **Contract-first phases.** Framing, rough-in and `/finish` each split into a contract and a procedure. `/finish` gains `finish-procedure.md`, and rough-in bundles the pair. `orchestration.md` states its default tier and effort. `finish-ab` and `agent-cost.py` land with stub tests. +- **The github-issues axis and the records.** The `.github` starter bodies, blueprint's two review-workflow templates, `docs/cbk/ROADMAP.md`, `docs/adr/corrections.md`, the `Extends:` grain, § Multi-surface facts, frozen-corpus ingestion, the issue-less branch, licensing, and `.github/dependabot.yml.example`. + +### Sync notes + +- The three split rules move sections. Text you filled under a heading that moved goes into the `-reference.md` half, under the same heading; the contract keeps only the pointer. +- Stamp the `paths:` globs in `logging.md` and `testing.md`: a placeholder glob loads nothing. Then run the bootstrap checklist's rule-file disposition pass. +- `/finish` is now a contract and a procedure: install `finish-procedure.md` beside `finish.md`, and rough-in's bundled pair beside them. +- Take the kit's `hooks` block: every command becomes `${CLAUDE_PROJECT_DIR}/.claude/hooks/.sh`, the launch-root guard registers on `PreToolUse`, and the fork detector registers on `Stop`. +- Record the reviewer agent-memory choice, `project` or `local`, in § Surface inventory. +- Do not take the two `_example_PostToolUse_*` keys this release adds to `settings.json`. A hook-shaped top-level key makes Claude Code discard the whole settings file (`.claude/rules/cbk-conventions-reference.md` § Hook authoring), and v0.5.0 removes them. + +## [0.3.0] — 2026-08-09 — axis parity (PRs #6, #7) + +The kit reconciled with its exercised reality, one configuration at a time. Design: `docs/superpowers/specs/2026-08-09-axis-parity-pass-design.md`. + +### What landed + +- **An axis-aware `/finish`.** Step 1 reads the planning backend from `docs/cbk/scaffold.md` § Cascade metadata, falling back to `.cascade/backends.toml`, and reads each axis its own way. +- **Two-axis producers.** Scaffold's and blueprint's templates emit `Planning backend` and `Knowledge backend` rows, the axis-conditional rows, and the `.cascade/backends.toml` mirror. +- **One Linear model.** Initiative, then a project shell, then the workstream as a parent issue, then F, then R; the milestones field stays unused. +- **Markdown honesty.** In-repo-markdown planning is design-doc mode, disclosed at scaffold's confirmation gate, with a defined markdown issue record. +- **Wiring and vocabulary.** A `notion` stanza in `.mcp.json.example`. The Projects v2 board contract, marked designed-unexercised. Frame headings of the form `### F<#> — M<#>: `, with F continuing across a workstream's frames. +- The kit repository ignores its own reviewer memory (#6). + +### Sync notes + +- `/finish` reads the planning backend from `docs/cbk/scaffold.md` § Cascade metadata. A target whose scaffold.md has no `Planning backend` row writes `.cascade/backends.toml` as the mirror it falls back to. +- The Notion server's key in `.mcp.json` must be `notion`: the knowledge-backend ask-gate's matcher names that server's tools. +- Frames already committed are immutable and keep their headings. The `### F<#> — M<#>: ` form starts at the next frame, with F continuing the workstream's sequence. +- On Linear, the workstream is a parent issue under the project shell, and the milestones field is not used. + +## [0.2.0] — 2026-08-09 — harvest 2 (PR #5) + +A second harvest of real-run learnings. Design: `docs/superpowers/specs/2026-08-09-cascade-kit-harvest-2-design.md`. + +### What landed + +- **`/finish` Step 5 split.** Research runs executably before plan mode, which then gates the plan. +- **Research grounding.** Existence and absence claims are verified repo-wide. +- **The review sweep**, `.claude/workflows/review-sweep.js`: find, then verify adversarially. +- **Three hooks.** `protect-main-branch.sh` hard-denies a commit on `main`; `guard-pr-state.sh` and `require-knowledge-backend-ok.sh` are ask-gates. +- **Reviewer memory.** `memory: project` on the three shipped reviewers. +- **Invocation posture.** `scaffold`, `blueprint`, `framing` and `rough-in` carry `disable-model-invocation: true`; `consultation` stays model-invocable. +- **Rule templates** `workflows.md`, `tooling.md` and `orchestration.md`, and the `Explore` exemplar. +- Frame lifecycle, decision governance, testing cadence and CI supply-chain discipline. + +### Sync notes + +- Register the three new hooks: take the kit's entries in `settings.json`'s `hooks`. +- `tooling.md` and `orchestration.md` are templates: fill them or delete them. `workflows.md` is kept as shipped. +- `scaffold`, `blueprint`, `framing` and `rough-in` no longer start from a description: type `/scaffold`, `/blueprint`, `/framing` or `/rough-in`. +- The reviewers write precedent memory under `.claude/agent-memory//`. Decide whether your repository commits it. +- `.claude/agents/Explore.md` overrides the built-in Explore agent. Delete it to keep the built-in. + +## [0.1.0] — 2026-08-09 — harvest 1 (PR #4) and everything before it + +The kit as first exported (#1–#3) and its first harvest of real-run learnings (#4). Design: `docs/superpowers/specs/2026-07-04-cascade-kit-harvest-design.md`. + +### What landed + +- **The first export.** The cascade skills, `/finish`, the rules and the ADR scaffolding, with the README (#1). +- **Calibration** (#2). `## Assumptions` becomes the eighth issue section, and the Apply class takes small, low-risk changes. +- **The constant plus two independent axes** (#3). A planning backend and a knowledge backend replace the bundled profiles; `knowledge-backend.md` is new. +- **Harvest 1** (#4). `/finish` in eleven gated steps, reading its break-glass marker from the issue body. The bottom-up lane: `/intake`, `/enrich`, `/pr-respond` and § Contribution intake. The `cascade-rule-reviewer`. The lock-file guard. The `format-on-edit.sh` exemplar. ADR `Refines:`. Additive-increment frames. The dependency settle-window. + +### Sync notes + +- This is the first tagged release. A copy of the kit taken before any tag records the commit it copied, as § Syncing the kit says, and reads the Sync notes from this release on. +- The bundled profiles are gone. `profile_selection.md` and `opinionated_profile.md` are removed from scaffold, and each skill's `github-only-vs-opinionated.md` is replaced by `planning-backend-matrix.md`. Record the planning and knowledge axes instead. +- The break-glass marker `` goes in the issue body or in the operator's instructions to `/finish`, never in the PR body. +- `format-on-edit.sh` ships unregistered. Wire it only after its case arms are filled. +```` + +Then fill the values only the execution day knows, and prove none is left: +```bash +n=$(bash .claude/workflows/tests/run-verification-block.sh | sed -n 's/^always-loaded total: \([0-9][0-9]*\) bytes$/\1/p') +sed -i "s/@@ALWAYS_LOADED@@/$(python3 -c 'import sys; print(f"{int(sys.argv[1]):,}")' "$n")/" CHANGELOG.md +cc=$(claude --version | awk '{print $1}'); [ "$cc" = 2.1.285 ] || sed -i "s/Checked on Claude Code 2\.1\.285\./Checked on Claude Code $cc./" CHANGELOG.md +today=$(date -u +%F); [ "$today" = 2026-09-30 ] || sed -i -e "s/^## \[1\.0\.0\] — 2026-09-30 — /## [1.0.0] — $today — /" -e "s/read 2026-09-30/read $today/g" CHANGELOG.md +last=$(gh api 'repos/j4th/context-builder-kit/issues?state=all&per_page=1&sort=created&direction=desc' -q '.[0].number') +if [ -z "$last" ]; then echo "STOP: gh api returned no issue number; fix gh auth, then re-run this line" +elif [ "$last" != 74 ]; then sed -i "s/^\(## \[1\.0\.0\] — .*\) (PR #75)$/\1 (PR #$((last + 1)))/" CHANGELOG.md; fi +grep -q "That field follows the Bash tool's" .claude/hooks/require-repo-root-for-agents.sh || python3 - <<'PY' +import re +p = "CHANGELOG.md"; t = open(p, encoding="utf-8").read() +a = ("- **A dispatch after a `cd` is still judged at the launch directory.** A hook payload's `cwd` stays where the" + " session was launched after the Bash tool's `cd`, whatever the hooks page says (the probe is recorded in" + " `require-repo-root-for-agents.sh`'s `Timing:` paragraph). The launch-root guard denies a session launched" + " outside the repository root: relaunch it from the root.") +t2, n = re.subn(r"^- \*\*A dispatch after a `cd` is judged where the shell is\.\*\*.*$", lambda m: a, t, flags=re.M) +assert n == 1, "the P1 note" +open(p, "w", encoding="utf-8").write(t2) +print("P1 note: variant A") +PY +grep -c '@@' CHANGELOG.md; grep -n '^## \[1\.0\.0\]\|^- Always-loaded\|^- \*\*A dispatch after' CHANGELOG.md +``` +Expected: `0`, then the `[1.0.0]` heading (with `(PR #75)` if `last` printed 74, the case at planning time), the +always-loaded line with both figures filled, and the P1 note: `…judged where the shell is.**` under variant B, or, +after a `P1 note: variant A` line, `…still judged at the launch directory.**`. The heading's date and PR number are both predictions: the date +is this execution day's, not the merge's, and the number is the next free one, not the PR's. The master plan's +Task F4 confirms the PR number when it opens the PR, and re-dates the heading to the merge's UTC date +(§ Handed to other clusters gives it the commands). + +- [ ] **Step 6: Re-run the simulated sync — green** + +Run: `bash "$S/sim-sync.sh" "$PWD" "$S/echosphere"; echo "exit=$?"` + +Expected: zero or more `CONFLICT (…, named): ` lines, then +`simulated sync: files merged, conflicted, 0 not named in the v1.0.0 Sync notes` and `exit=0`. (On the +planning copy, where V1–V9 had not landed, against the target's `origin/main` at `a64f7f6`, it printed `simulated sync: 131 files merged, 0 conflicted, 0 not +named in the v1.0.0 Sync notes`; that run predates the issue-template mapping, and the target carries both +templates, so the merged count is now two higher.) The target's `cascade-meta.md` is a byte copy of the kit's at +`74edf84`, and its `cascade-rough-in.md` is an older copy without `## Assumptions`; both are named under **Re-copy +the issue templates**, so either may conflict and still count as named. The target changed 21 of the kit's `.claude/` files against `74edf84`. The 18 of +them that a V1–V9 pack names as a target are already named under **Hand-merge these files** or **Files a target +may already run ahead of the kit**, and so are the four mapped files it changed (the two review workflows, the ADR +job and `.mcp.json`) and `run-arms-headless.py`. The other three, `.claude/agents/adr-conformance-reviewer.md`, `.claude/rules/logging.md` and +`.claude/rules/testing.md`, merge clean unless a cluster changed them after all. +If a line reads `UNNAMED (): `, add one sub-bullet under **Hand-merge these files**, directly above +the `.github/workflows/adr-immutability-check.yml` sub-bullet, of the form +`` - ``. Keep ; take the rest of the kit's text.``, +with the kept part read from that file's conflict hunk (`git merge-file -p` on the three versions the script +names). Re-run until the unnamed count is 0. Record the final summary line in the PR body. + +- [ ] **Step 7: Run the gate** + +Run: +```bash +for h in .claude/hooks/*.sh .claude/hooks/lib/*.sh; do bash -n "$h" || echo "bash -n failed: $h"; done +bash .claude/workflows/tests/run-verification-block.sh 2>&1 | grep -E '^verification: '; echo "exit=${PIPESTATUS[0]}" +git grep --untracked -n -i -E '\bcr[e]ase\b|CR[E]-[0-9]|you-are-he[a]r|echospher[e]' -- CHANGELOG.md; echo "names=$?" +``` +Expected: no `bash -n failed` line; `verification: kit sub-block complete`; `verification: done`; `exit=0`; +`names=1`. (`--untracked` because `CHANGELOG.md` is not added until Step 8, and a plain `git grep` skips an +untracked file, so it would print `names=1` whatever the file said.) + +- [ ] **Step 8: Commit** + +```bash +git add CHANGELOG.md .claude/rules/cbk-conventions-reference.md +git commit -F - <<'EOF' +docs(V10): CHANGELOG.md — v0.1.0 to v1.0.0, each release with its Sync notes + +One section per tag: harvest 1 with everything before it, harvest 2, axis parity, harvest 3's +four PRs, harvest 4, and v1.0.0. Each section records what landed and the Sync notes a target +follows by hand. v1.0.0's notes name every file a filled target must hand-merge, and say that +files run ahead of the kit take v1.0.0 as their merge base. They also record which action +release first resolves each family alias to the 5.5 models. A simulated sync of a named +target's filled .claude/ from 74edf84 to this branch leaves no conflicting file unnamed. The +block pins a section per tag. + +Trace: release/6, #67/c5881158070/2c, #69/c5859756889/apply-h4/5, #69/c5901496433/sync-note +(the v1.0.0 note), #69/c5859756889/apply-h4/1, #69/c5859756889/apply-h4/2, +#69/c5859756889/apply-h4/3 and #69/c5859756889/apply-h4/6 (the v0.5.0 notes), +#70/table/mise-mcp-row (the release-notes line), #60/c5881158391/status (the note for a target +that customized its ADR hook); the v1.0.0 notes handed in by V1 (the runner a copy row with a +third rail, its fixture an add row, the check task, the reference glob, the CLAUDE.md check keyed +on blueprint.md), V2 (the path-independent ADR deny and its helper, the three backstop slots, +the refused payload, the cd behaviour probe P1 settled, the re-copied ADR job, the new +fixtures), V4 (the review workflows), V5 (the orchestration pair, the knowledge-backend split +and its none-axis disposition, the heading rename), V6 (#69/c5881157875/2-fable5-misprice, the +runner's fills), V8 (#70/body/4, #70/body/5, #70/body/6a, #70/body/6b, #71/body/3) and V9 (the +issue-template re-copy, adr-new's Relation grains for #66/body/1, the qualified citations, the +D54 branch rule for #68/body/1a, the CI-skip wording for review/portability/36). + +Co-Authored-By: Claude Opus 5.5 +EOF +``` + +### Task V10.3: `README.md` — the tagged drop-in install, prerequisites, how a phase starts, and an inventory from the tree (release/3, release/4, review/consistency/1, /2, /6, /7, review/release/23, /25, /26, /27, /30, /61; the README parts of #60/c5892401033/helper, #69/body/F9, #69/body/finish-ab/N-arm, #69/body/finish-ab/runner, review/release/28, review/security/17, review/claude-code/13, review/release/29, review/release/63, review/consistency/40 (the `finish.md` sentence), review/consistency/51, and the License paragraph of review/release/64; D41, D60) + +**Files:** +- Modify: `README.md` — thirteen replacements, each anchored on text only V10 edits +- Modify: `.claude/rules/cbk-conventions-reference.md` — one check at the kit sub-block's sentinel + +**Interfaces:** +- Consumes: the tree after V1–V9 (Step 0 lists it); `CHANGELOG.md`'s newest release (Task V10.2); § Syncing the + kit (Task V10.1); V5's `.claude/rules/simplification.md` § Plugin, which the plugin list points at instead of + restating a Claude Code version; V3's `omitClaudeMd: true` on `Explore.md`; V8's `.mcp.json.example` server set + and its `.gitignore` starter section. +- Produces: the install block under `### 1. Install the drop-in set from a tagged release`, with the line + `KIT_VERSION=v1.0.0` and a single-line `curl -fsSL -o "$kit_tmp/kit.tar.gz" …` download. The master plan's + Task F5 Step 5 runs that block against the real tag. It prints `Kit commit: v1.0.0 (<7-char sha>)`, the value + scaffold's row records (Task V10.1). +- Produces: `## Upgrading`, which the install block's refusal message and the Gotchas line point at. +- Produces: the block sentences `README.md's inventory tree does not name ` and `README.md's install does not + pin v, the newest release in CHANGELOG.md`. + +- [ ] **Step 0: Confirm the tree the README describes** + +Run: +```bash +for d in commands agents hooks hooks/lib rules; do printf '%s: %s\n' "$d" "$(cd .claude/$d && LC_ALL=C ls | tr '\n' ' ')"; done +jq -r '.mcpServers | keys | join(" ")' .mcp.json.example +grep -c 'uvx' .mcp.json.example +jq -r '.enabledPlugins | keys | join(" ")' .claude/settings.json +grep -c '^## Plugin' .claude/rules/simplification.md +grep -c '2\.1\.154' .claude/rules/simplification.md +grep -c '^omitClaudeMd: true$' .claude/agents/Explore.md +grep -c '^model: haiku$' .claude/agents/Explore.md +grep -c 'run-verification-block\.sh' .claude/skills/blueprint/references/templates/tooling.md +grep -c 'CLAUDE_CODE_OAUTH_TOKEN' .claude/skills/blueprint/references/templates/claude-review.yml +grep -c '^## `.gitignore`' .claude/skills/scaffold/references/github-starter-templates.md +for s in scaffold blueprint framing rough-in adr-new consultation; do printf '%s=%s ' "$s" "$(grep -c '^disable-model-invocation: true' .claude/skills/$s/SKILL.md)"; done; echo +``` +Expected: +``` +commands: enrich.md finish-procedure.md finish.md intake.md pr-respond.md +agents: Explore.md adr-conformance-reviewer.md cascade-rule-reviewer.md logging-discipline-reviewer.md +hooks: analyze-on-edit.sh detect-forked-agent-memory.sh format-on-edit.sh guard-pr-state.sh lib protect-immutable-adrs.sh protect-lock-files.sh protect-main-branch.sh require-knowledge-backend-ok.sh require-repo-root-for-agents.sh +hooks/lib: resolve-path.sh +rules: cbk-conventions-reference.md cbk-conventions.md knowledge-backend-reference.md knowledge-backend.md logging.md orchestration-reference.md orchestration.md pr-review-reference.md pr-review.md simplification.md testing.md tooling.md workflows.md +context7 linear notion time + +commit-commands@claude-plugins-official pr-review-toolkit@claude-plugins-official + +scaffold=1 blueprint=1 framing=1 rough-in=1 adr-new=1 consultation=0 +``` +The listing is five commands, four agents, nine hooks and `lib/resolve-path.sh`, and thirteen rules. Every +`` line must be at least `1`. A `0`, or a different set, means a sentence below describes something that +did not land: stop and reconcile that sentence first. + +- [ ] **Step 1: Add the failing checks to the block** + +**Replace** in `.claude/rules/cbk-conventions-reference.md` *(whole line; the kit sub-block's sentinel)*: +````text +echo "verification: kit sub-block complete" +```` +with: +````text +# Releases (V10): on the kit tree, README.md's inventory names every command, agent, hook, sourced hook helper and rule +# the kit ships, and its install pins the newest release CHANGELOG.md records. +if [ ! -f docs/cbk/scaffold.md ]; then + for f in .claude/commands/*.md .claude/agents/*.md .claude/hooks/*.sh .claude/hooks/lib/*.sh .claude/rules/*.md; do [ -e "$f" ] || continue; grep -qF -- "── $(basename "$f") " README.md || { echo "README.md's inventory tree does not name $f"; exit 1; }; done + newest=$(awk 'match($0, /^## \[[0-9]+\.[0-9]+\.[0-9]+\]/){print substr($0, 5, RLENGTH-5); exit}' CHANGELOG.md) + grep -qx "KIT_VERSION=v$newest" README.md || { echo "README.md's install does not pin v$newest, the newest release in CHANGELOG.md"; exit 1; } +fi + +echo "verification: kit sub-block complete" +```` + +- [ ] **Step 2: Run the block — red** + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | tail -2; echo "exit=${PIPESTATUS[0]}"` + +Expected at execution, with V2's helper in the tree: +``` +README.md's inventory tree does not name .claude/hooks/lib/resolve-path.sh +verification: block exited 1 +exit=1 +``` +(On the planning copy, which has no `lib/` and twelve rules, every shipped name was already in the tree, so the +version pin reded instead: `README.md's install does not pin v1.0.0, the newest release in CHANGELOG.md`. The +planning copy with a stub `lib/resolve-path.sh` added reded on the line above.) Record the first line. + +- [ ] **Step 3: D60's install test — red** + +`$S/install-test.sh` (full content): +````bash +#!/usr/bin/env bash +# D60's local test: archive the branch head the way GitHub archives a tag, run README.md's install +# block against that archive in a scratch repository, and check that only the drop-in set lands, +# that nothing the target already had is touched, and that the printed Kit commit names the head. +# Usage: bash install-test.sh . Writes only under mktemp -d. +set -uo pipefail +kit=$(cd "$1" && pwd) || exit 2 +t=$(mktemp -d) || exit 2 +trap 'rm -rf "$t"' EXIT +fail() { echo "FAIL: $*"; exit 1; } +git -C "$kit" archive --format=tar.gz --prefix=context-builder-kit-1.0.0/ -o "$t/archive.tar.gz" HEAD || exit 2 +awk '/^### 1\. Install the drop-in set/{s=1;next} s&&/^```bash$/{c=1;next} c&&/^```$/{exit} c' "$kit/README.md" > "$t/install.sh" +grep -q '^curl -fsSL -o "\$kit_tmp/kit.tar.gz" ' "$t/install.sh" || fail "README.md's install block has no single-line curl download to swap for the local archive" +sed -i.bak 's|^curl -fsSL -o "\$kit_tmp/kit.tar.gz" .*|cp "'"$t"'/archive.tar.gz" "$kit_tmp/kit.tar.gz"|' "$t/install.sh" +mkdir "$t/target" && cd "$t/target" && git init -q . || exit 2 +for f in README.md LICENSE CLAUDE.md .gitignore; do printf 'the target'"'"'s own %s\n' "$f" > "$f"; done +out=$(bash "$t/install.sh" 2>&1) || fail "the install block exited non-zero: $out" +for f in README.md LICENSE CLAUDE.md .gitignore; do grep -qx "the target's own $f" "$f" || fail "the install overwrote the target's $f"; done +find . -path ./.git -prune -o -type f -print | sed 's|^\./||' | sort > "$t/landed" +{ git -C "$kit" ls-tree -r --name-only HEAD .claude; printf '%s\n' .mcp.json.example .github/dependabot.yml.example .github/workflows/adr-immutability-check.yml README.md LICENSE CLAUDE.md .gitignore; } | sort > "$t/expected" +diff "$t/expected" "$t/landed" > "$t/diff" || { cat "$t/diff"; fail "the files that landed are not exactly the drop-in set plus the target's own four"; } +want="Kit commit: v1.0.0 ($(git -C "$kit" rev-parse --short=7 HEAD))" +grep -qxF "$want" <<<"$out" || fail "the install printed '$out', not '$want'" +[ -e .claude ] && { out2=$(bash "$t/install.sh" 2>&1); grep -q 'already exists' <<<"$out2" || fail "a second run over an existing .claude/ did not refuse"; } +echo "install test: $(wc -l < "$t/landed") files, only the drop-in set landed; printed '$want'" +```` + +Run: `bash "$S/install-test.sh" "$PWD"; echo "exit=$?"` + +Expected: +``` +FAIL: README.md's install block has no single-line curl download to swap for the local archive +exit=1 +``` + +- [ ] **Step 4: Verify the README's quotations raw** + +Run: +```bash +for p in model-config mcp discover-plugins skills sub-agents; do curl -sL "https://code.claude.com/docs/en/$p.md" -o "$S/$p.md"; done +norm "$S/model-config.md" | grep -cF 'Sonnet 5.5 requires Claude Code v2.1.284 or later, and Opus 5.5 requires v2.1.280 or later' +norm "$S/mcp.md" | grep -cF '`${VAR}`: expands to the value of environment variable `VAR`' +norm "$S/mcp.md" | grep -cF 'Check `.mcp.json` into version control so everyone on your team gets the same MCP tools and services' +norm "$S/mcp.md" | grep -cF "A cloned repository can't approve its own servers" +norm "$S/mcp.md" | grep -cF '⏸ Pending approval' +norm "$S/mcp.md" | grep -cF 'Use `/mcp` to authenticate with remote servers that require OAuth 2.0 authentication' +norm "$S/discover-plugins.md" | grep -cF '/plugin install commit-commands@claude-plugins-official' +norm "$S/skills.md" | grep -cF 'Use for workflows you want to trigger manually with `/name`' +norm "$S/sub-agents.md" | grep -cF 'A user or project subagent named `Explore` overrides the built-in and keeps its own `model` field' +norm "$S/sub-agents.md" | grep -cF 'Set to `true` to launch this subagent without the user, project, and local CLAUDE.md files' +tr -s ' \n' ' ' < LICENSE | grep -cF 'You must give any other recipients of the Work or Derivative Works a copy of this License' +``` +Expected: eleven lines, each a count of at least `1`. A `0` means the page changed: re-read it, and correct or +drop that sentence before Step 5 writes it. + +- [ ] **Step 5: Rewrite the README** + +Make these thirteen replacements in `README.md`, in order. Every old text is verbatim from `README.md` at +`74edf84`, which no other cluster edits. + +**1. The tagline.** **Replace** *(whole line 3)*: +````text +> Take an idea from *"I want to make X"* to a merged PR. Six Claude Code skills, one slash command, two reviewer agents, and a small set of rules — drop into any repo and pick the entry point that fits your moment. +```` +with: +````text +> Take an idea from *"I want to make X"* to a merged PR. Claude Code skills for each phase, slash commands that execute and feed them, reviewer agents, guard hooks and a small set of rules — install them into any repo and pick the entry point that fits your moment. +```` + +**2. The Quick start, and a new `## Upgrading`.** **Replace** *(whole lines 27–54)*: +````markdown +## Quick start + +The kit assumes [Claude Code](https://claude.com/claude-code), `git`, and ideally [`mise`](https://mise.jdx.dev/) installed. + +```bash +# 1. Drop the kit into your repo +cd your-project +curl -L https://github.com//context-builder-kit/archive/main.tar.gz \ + | tar xz --strip-components=1 +# (Or git submodule, or just `cp -r` from a clone — pick what fits your repo.) + +# 2. Customize the conventions file with your project's specifics +$EDITOR .claude/rules/cbk-conventions.md +# Fill in , placeholders; stamp the paths: globs in +# logging.md and testing.md; delete the template callouts once you're done. + +# 3. Configure MCP servers (cascade reads from .mcp.json) +cp .mcp.json.example .mcp.json +$EDITOR .mcp.json # fill in PATs, API keys + +# 4. Install the Claude Code plugins the kit depends on +# (See "Required dependencies" below for the canonical list.) + +# 5. Open Claude Code in the repo and start. Pick an entry point — see below. +claude +``` + +That's it. The kit is in place; what you do next depends on where you are in the project. +```` +with: +````markdown +## Quick start + +### Prerequisites + +| Tool | Used by | Without it | +|---|---|---| +| [Claude Code](https://claude.com/claude-code) 2.1.284 or later | Everything. The kit's model defaults name Opus 5.5 and Sonnet 5.5, and "Sonnet 5.5 requires Claude Code v2.1.284 or later, and Opus 5.5 requires v2.1.280 or later" (`https://code.claude.com/docs/en/model-config`, read 2026-09-30) | A request for either model fails | +| `git` | Every phase, every hook, the install and the sync | Nothing runs | +| `bash` 3.2 or later | The hooks, the fixtures and the verification block; the hooks use no bash-4 builtins, so a stock macOS bash runs them | The hooks cannot run | +| `jq` | Every hook whose header's `Depends:` line names it, and the verification block | Each of those guards fails open, allowing the action with a warning that names its backstop, and the block exits non-zero | +| `gh`, authenticated | `/finish`, `/intake`, `/enrich`, `/pr-respond` and the phases' issue writes on the `github-issues` axis; `gh` is the kit's GitHub interface | The `github-issues` axis cannot write issues or pull requests | +| `node` | The verification block's `.mjs` fixtures, the workflow scripts, and any `npx`-launched server in `.mcp.json` | The block exits non-zero | +| `python3` | `agent-cost.py`, `run-arms-headless.py`, and the fixtures the block runs for them | The block exits non-zero | +| `uv` | The `time` MCP server, which `.mcp.json.example` launches through `uvx` | The optional `time` server does not start | +| [`mise`](https://mise.jdx.dev/), optional | The task runner blueprint's tooling template defaults to | Nothing: any runner that defines a `check` task works | +| A `CLAUDE_CODE_OAUTH_TOKEN` Actions secret | The review workflows blueprint emits when scaffold's PR question chose automated review | Those workflows fail at their action step | + +v1.0.0 was checked on Claude Code 2.1.285. To list what is missing on a machine: `for t in git jq gh node python3 uv; do command -v "$t" >/dev/null || echo "missing: $t"; done`. + +### 1. Install the drop-in set from a tagged release + +Run this at your repository's root. It copies only the drop-in set — `.claude/`, `.mcp.json.example`, `.github/dependabot.yml.example` and `.github/workflows/adr-immutability-check.yml` — and never touches your `README.md`, `LICENSE`, `CLAUDE.md`, `.gitignore` or `docs/`. It prints the release to record. To install from a fork, change the owner in the URL. + +```bash +KIT_VERSION=v1.0.0 +kit_tmp=$(mktemp -d) +curl -fsSL -o "$kit_tmp/kit.tar.gz" "https://github.com/j4th/context-builder-kit/archive/refs/tags/$KIT_VERSION.tar.gz" +tar -xzf "$kit_tmp/kit.tar.gz" -C "$kit_tmp" --strip-components=1 +if [ -e .claude ]; then + echo "A .claude/ already exists here, so nothing was copied: upgrade it instead (see Upgrading)." +else + cp -R "$kit_tmp/.claude" . + cp "$kit_tmp/.mcp.json.example" . + mkdir -p .github/workflows + cp "$kit_tmp/.github/dependabot.yml.example" .github/ + cp "$kit_tmp/.github/workflows/adr-immutability-check.yml" .github/workflows/ + echo "Kit commit: $KIT_VERSION ($(gunzip -c "$kit_tmp/kit.tar.gz" | git get-tar-commit-id | cut -c1-7))" +fi +rm -rf "$kit_tmp" +``` + +The printed `Kit commit:` line is the release you installed, and the base of every later sync. Scaffold records it as the **Kit commit** row of `docs/cbk/scaffold.md`. If you start at blueprint or later, record it yourself in `.claude/rules/cbk-conventions-reference.md` § Syncing the kit, and create `docs/adr/` from the starters that scaffold would have copied: `mkdir -p docs/adr && cp .claude/skills/scaffold/references/adr-starters/*.md docs/adr/`. Then fill ADR-0000's `Date:` and `Deciders:` by hand, because the ADR guard denies the agent's edits to an existing ADR. + +The install leaves your `.gitignore` alone: add the harness block from `.claude/skills/scaffold/references/github-starter-templates.md` § `.gitignore` to it yourself. + +### 2. Fill the conventions + +```bash +$EDITOR .claude/rules/cbk-conventions.md +``` + +Fill in the `` and `` placeholders, stamp the `paths:` globs in `logging.md` and `testing.md` and the bracketed manifest-and-lockfile entry in `cbk-conventions-reference.md`'s `paths:`, and delete the template callouts once you're done. Scaffold's bootstrap checklist walks the rest of the rule files; in a filled target, an unstamped glob turns the verification block red. + +### 3. Configure the MCP servers + +```bash +cp .mcp.json.example .mcp.json +$EDITOR .mcp.json # delete the servers your axes do not use +git add .mcp.json +``` + +`.mcp.json` is committed, because it holds `${VAR}` references and never a secret. The mcp page says "`${VAR}`: expands to the value of environment variable `VAR`", and "Check `.mcp.json` into version control so everyone on your team gets the same MCP tools and services" (`https://code.claude.com/docs/en/mcp`, read 2026-09-30). Export each variable the file references before launching `claude`, and name them in a committed `.env.example`. A hosted server that requires OAuth signs in through `/mcp`. + +### 4. Install the plugins + +In a Claude Code session: + +``` +/plugin install pr-review-toolkit@claude-plugins-official +/plugin install commit-commands@claude-plugins-official +``` + +These are the two `enabledPlugins` keys in `.claude/settings.json` (`https://code.claude.com/docs/en/discover-plugins`, read 2026-09-30). `/simplify` ships with Claude Code and needs no install. + +### 5. Open Claude Code and start a phase + +```bash +claude +``` + +On the first launch, accept the workspace trust dialog. "A cloned repository can't approve its own servers": until the workspace is trusted, the `enabledMcpjsonServers` list committed in `.claude/settings.json` is ignored and each server waits at `⏸ Pending approval` (`https://code.claude.com/docs/en/mcp`, read 2026-09-30). Once it is trusted, the servers that list names are approved; approve any other server in `.mcp.json` when Claude Code asks. Then start from where you are (next section). + +That's it. The kit is in place; what you do next depends on where you are in the project. + +## Upgrading + +A repository that carries the kit upgrades by release, never by re-extracting over its filled files. Read `CHANGELOG.md` from the release your **Kit commit** row names up to the one you are moving to: each release's **Sync notes** say what to do by hand. Then follow `.claude/rules/cbk-conventions-reference.md` § Syncing the kit — a file-by-file table first, then a three-way `git merge-file` per file, with the kit at your recorded release as the base. Record the new release when the sync merges. +```` + +**3. The entry-point table gains a Run column, and how a phase starts.** **Replace** *(whole lines 60–69)*: +````markdown +| Where you are | Start at | Skip the cascade above? | +|---|---|---| +| **Vague idea, no repo** ("I want to build something that does X") | `consultation` | No — full cascade | +| **Clear-ish idea, no repo** (problem is shaped, you can describe it in a paragraph) | `scaffold` | Skip consultation; provide a verbal brief or paste one | +| **Existing repo, ready to set up workspace + architecture** | `blueprint` | Skip consultation + scaffold; commit a brief manually if you don't have one | +| **Architecture decided, ready to plan a specific workstream** | `framing` | Skip everything above; **needs `blueprint.md` § Workstreams** | +| **Small project — milestones obvious, just want issues** | `rough-in` ⚠️ *experimental* | Skip framing too; **brittle without a `frame-NN.md`** | +| **One concrete issue ready to implement** | `/finish ` | Issue must already have rough-in's eight-section body shape; backend planning axes only | + +**Most users start at `blueprint`.** Consultation is HITL-heavy and works fine in plain Claude.ai chat; scaffold is mostly provisioning that's faster to do in a browser tab. The cascade's value compounds from `blueprint` forward, where the artifacts start versioning into your repo and the next phase actually inherits from disk. +```` +with: +````markdown +| Where you are | Start at | Run | Skip the cascade above? | +|---|---|---|---| +| **Vague idea, no repo** ("I want to build something that does X") | `consultation` | Describe the idea in chat | No — full cascade | +| **Clear-ish idea, no repo** (problem is shaped, you can describe it in a paragraph) | `scaffold` | `/scaffold` | Skip consultation; provide a verbal brief or paste one | +| **Existing repo, ready to set up workspace + architecture** | `blueprint` | `/blueprint` | Skip consultation + scaffold; commit a brief manually if you don't have one | +| **Architecture decided, ready to plan a specific workstream** | `framing` | `/framing` | Skip everything above; **needs `blueprint.md` § Workstreams** | +| **Small project — milestones obvious, just want issues** | `rough-in` ⚠️ *experimental* | `/rough-in` | Skip framing too; **brittle without a `frame-NN.md`** | +| **One concrete issue ready to implement** | `/finish ` | `/finish ` | Issue must already have rough-in's eight-section body shape; backend planning axes only | + +**How a phase starts.** `consultation` is the one skill Claude invokes on its own: describing an idea is enough. `scaffold`, `blueprint`, `framing`, `rough-in` and `adr-new`, and the commands `/finish`, `/intake`, `/enrich` and `/pr-respond`, carry `disable-model-invocation: true`. The skills page describes that setting as "Use for workflows you want to trigger manually with `/name`" (`https://code.claude.com/docs/en/skills`, read 2026-09-30): describing the intent does not start them, so type the command. + +**Most users start at `blueprint`.** Consultation is HITL-heavy and works fine in plain Claude.ai chat; scaffold is mostly provisioning that's faster to do in a browser tab. The cascade's value compounds from `blueprint` forward, where the artifacts start versioning into your repo and the next phase actually inherits from disk. +```` + +**4. Exercised vs designed, restamped.** The one real run on the `github-issues` axis used issues, native +sub-issues and labels and recorded `**Project board** | **None.** Deliberate` in its `docs/cbk/scaffold.md` +(`j4th/you-are-hear` at `1d4d52a`), so the board contract stays designed-unexercised. **Replace** *(whole line 119)*: +````markdown +**Exercised vs designed** (recorded 2026-08-09 — the honest status per configuration, per the kit's exercised-not-provisional principle): `Linear` planning is the **exercised reference configuration** — a full real cascade ran on it end-to-end. `GitHub Issues` planning has the kit's deepest documentation and is designed first-class, but no real cascade run has exercised it yet (its Projects v2 board contract is marked designed-unexercised inline). `In-repo markdown` is design-doc mode by design. On the knowledge axis, `none` is effectively exercised daily; `Notion` is a complete contract, configured in the reference run and lightly exercised. +```` +with: +````markdown +**Exercised vs designed** (restamped 2026-09-30 — the honest status per configuration, per the kit's exercised-not-provisional principle): `Linear` planning is exercised end-to-end by more than one real cascade run. `GitHub Issues` planning is exercised by a real run through issues, native sub-issues, labels and the review workflows; that run chose no Projects v2 board, so the board contract stays marked designed-unexercised inline. `In-repo markdown` is design-doc mode by design. On the knowledge axis, `none` is effectively exercised daily; `Notion` is a complete contract, configured in a real run and lightly exercised. +```` + +**5. The scaffold example shows the Kit commit row.** **Replace** *(whole lines 129–130)*: +````markdown +**Repo**: github.com/you/tuitor +**Project board**: github.com/you/tuitor/projects/4 +```` +with: +````markdown +**Repo**: github.com/you/tuitor +**Project board**: github.com/you/tuitor/projects/4 +**Kit commit**: v0.5.0 (74edf84) +```` + +**6. Blueprint names the roadmap it writes.** **Replace** *(whole line 158)*: +````markdown +**Phase 3** — most complex of the cascade. Inherits the brief and the scaffold output, then makes the load-bearing calls: stack (language, framework, storage, testing), methodology (Shape Up, Kanban, Scrum), and produces six prose foundation docs plus tooling configs. +```` +with: +````markdown +**Phase 3** — most complex of the cascade. Inherits the brief and the scaffold output, then makes the load-bearing calls: stack (language, framework, storage, testing), methodology (Shape Up, Kanban, Scrum), and produces six prose foundation docs — seven on the `github-issues` and `in-repo-markdown` axes, where `docs/cbk/ROADMAP.md` joins them — plus tooling configs. `ROADMAP.md` is the freely mutable status surface that framing, rough-in and `/finish` update. +```` + +**7. The bottom-up lane gets a paragraph.** **Replace** *(whole lines 281–283)*: +````markdown +**`/finish` does NOT**: modify the issue body; handle re-rough-in; bypass dependencies; skip either half of the review floor (`/simplify`, `pr-review-toolkit:review-pr` — both as skills, recorded in the `## Review gate` block); mark the PR ready; merge. When the spec is wrong or something is missing, `/finish` surfaces and aborts rather than improvising. + +## What the kit ships +```` +with: +````markdown +**`/finish` does NOT**: modify the issue body; handle re-rough-in; bypass dependencies; skip either half of the review floor (`/simplify`, `pr-review-toolkit:review-pr` — both as skills, recorded in the `## Review gate` block); mark the PR ready; merge. When the spec is wrong or something is missing, `/finish` surfaces and aborts rather than improvising. + +### The bottom-up lane — `/intake`, `/enrich`, `/pr-respond` + +The cascade runs top-down; externally sourced work comes in from the side. `/intake ` turns a bug report or feature request into a shaped, `/finish`-able issue: it investigates, reproduces, classifies and shapes, and never lands code. `/enrich ` is rough-in for a single small capability, skipping the framing milestone. `/pr-respond ` closes the review loop on a PR: it triages every comment, applies what the rubric says to apply, and answers every thread. Which work skips framing and which stays framed is `cbk-conventions.md` § Contribution intake. + +## What the kit ships +```` + +**8. What the kit ships: the tree from the drop-in set, and what is not installed.** **Replace** *(whole lines +285–347)*: +`````markdown +``` +.claude/ +├── commands/ +│ ├── finish.md ← Phase 6 executor slash command (the contract) +│ ├── finish-procedure.md ← its procedure, read on demand +│ ├── intake.md ← bottom-up entry: external report → /finish-able issue +│ ├── enrich.md ← rough-in for one small capability +│ └── pr-respond.md ← the PR feedback-loop executor +├── skills/ +│ ├── consultation/ ← Phase 1 +│ ├── scaffold/ ← Phase 2 (+ references/adr-starters/ incl. corrections.md, references/issue-templates/, the .github starter bodies, the cascade-events index template) +│ ├── blueprint/ ← Phase 3 (+ references/templates/: the foundation docs, roadmap.md, the review workflows claude-review.yml + claude.yml) +│ ├── framing/ ← Phase 4 (+ references/contract.md, references/procedure.md) +│ ├── rough-in/ ← Phase 5 (+ references/contract.md, references/procedure.md; references/finish-command.md + finish-procedure.md, the bundled executor pair) +│ └── adr-new/ ← ADR scaffolder (used by blueprint and onward) +├── agents/ +│ ├── adr-conformance-reviewer.md ← dispatched on every review pass +│ ├── logging-discipline-reviewer.md ← same +│ ├── cascade-rule-reviewer.md ← same +│ └── Explore.md ← cheap-tier search exemplar +├── hooks/ +│ ├── protect-immutable-adrs.sh ← hard-deny: edits to existing ADRs +│ ├── protect-lock-files.sh ← hard-deny: hand edits to lock files +│ ├── protect-main-branch.sh ← hard-deny: git commit on main +│ ├── require-repo-root-for-agents.sh ← hard-deny: Task/Agent/Workflow dispatch outside the repo root +│ ├── guard-pr-state.sh ← ask-gate: gh pr ready/merge/close/reopen +│ ├── require-knowledge-backend-ok.sh ← ask-gate: knowledge-backend MCP writes +│ ├── detect-forked-agent-memory.sh ← stop: a reviewer-memory tree outside the root blocks the hand-off +│ ├── format-on-edit.sh ← advisory exemplar (unregistered; stanza in settings.json) +│ └── analyze-on-edit.sh ← advisory exemplar (unregistered; stanza in settings.json) +├── rules/ +│ ├── cbk-conventions.md ← project conventions — contract half (template; you fill this) +│ ├── cbk-conventions-reference.md ← its path-scoped reference half +│ ├── orchestration.md ← model × effort tiering — contract half (template) +│ ├── orchestration-reference.md ← its path-scoped reference half +│ ├── pr-review.md ← review floor, roster, rubric — contract half +│ ├── pr-review-reference.md ← its path-scoped reference half (calibration tables) +│ ├── testing.md ← three-regime testing (path-scoped; stamp the globs) +│ ├── logging.md ← structured logging (path-scoped; stamp the glob) +│ ├── simplification.md ← /simplify contract +│ ├── workflows.md ← agent workflow patterns (portable) +│ ├── tooling.md ← tool-selection skeleton (template) +│ └── knowledge-backend.md ← Notion-axis contract (delete with its hook when the axis is none) +├── workflows/ +│ ├── review-sweep.js ← find-then-verify review orchestration +│ ├── finish-ab/ ← two-arm A/B harness exemplar (worktree-isolated arms, balanced blind judges) +│ ├── agent-cost.py ← per-agent cost reader for a run's transcripts +│ └── tests/ ← stub harnesses for the workflows (no agent dispatched) +└── settings.json ← hook registration + plugin/MCP manifest + +docs/adr/ +├── README.md ← ADR index (starter — just ADR-0000) +├── template.md ← ADR template +└── 0000-record-architecture-decisions.md ← meta-ADR establishing immutability (scaffold fills its header) + +.github/workflows/ +└── adr-immutability-check.yml ← CI gate enforcing ADR-0000 at raw-git level + +CLAUDE.md ← kit-level instructions for Claude Code +.mcp.json.example ← MCP server config template +``` + +Each skill follows the same pattern: a `SKILL.md` entrypoint plus a `references/` directory with templates and operational reference docs (failure modes, question banks, axis-specific behavior, inheritance discipline). The three producing phases — framing, rough-in and `/finish` — are contract-first: `references/contract.md` (for the executor, `commands/finish.md` itself) is the drafting read, `references/procedure.md` (`commands/finish-procedure.md`) the step-by-step on demand, and `SKILL.md` routes. Skills load `references/*.md` lazily on demand. +````` +with: +`````markdown +``` +.claude/ +├── commands/ +│ ├── finish.md ← Phase 6 executor slash command (the contract) +│ ├── finish-procedure.md ← its procedure, read on demand +│ ├── intake.md ← bottom-up entry: external report → /finish-able issue +│ ├── enrich.md ← rough-in for one small capability +│ └── pr-respond.md ← the PR feedback-loop executor +├── skills/ +│ ├── consultation/ ← Phase 1 +│ ├── scaffold/ ← Phase 2 (+ references/adr-starters/, the docs/adr/ starters incl. corrections.md; references/issue-templates/, the .github starter bodies, the cascade-events index template) +│ ├── blueprint/ ← Phase 3 (+ references/templates/: the foundation docs, roadmap.md, the review workflows claude-review.yml + claude.yml) +│ ├── framing/ ← Phase 4 (+ references/contract.md, references/procedure.md) +│ ├── rough-in/ ← Phase 5 (+ references/contract.md, references/procedure.md; references/finish-command.md + finish-procedure.md, the bundled executor pair) +│ └── adr-new/ ← ADR scaffolder (used by blueprint and onward) +├── agents/ +│ ├── adr-conformance-reviewer.md ← dispatched on every review pass +│ ├── logging-discipline-reviewer.md ← same +│ ├── cascade-rule-reviewer.md ← same +│ └── Explore.md ← a Haiku-pinned override of the built-in Explore, with omitClaudeMd (delete it to keep the built-in) +├── hooks/ +│ ├── protect-immutable-adrs.sh ← hard-deny: edits to existing ADRs, however the path is spelled +│ ├── protect-lock-files.sh ← hard-deny: hand edits to lock files +│ ├── protect-main-branch.sh ← hard-deny: git commit on main +│ ├── require-repo-root-for-agents.sh ← hard-deny: Task/Agent/Workflow dispatch outside the repo root +│ ├── guard-pr-state.sh ← ask-gate: gh pr ready/merge/close/reopen +│ ├── require-knowledge-backend-ok.sh ← ask-gate: knowledge-backend MCP writes +│ ├── detect-forked-agent-memory.sh ← stop: a reviewer-memory tree outside the root blocks the hand-off +│ ├── format-on-edit.sh ← advisory exemplar (unregistered; its Register: stanza is in its header) +│ ├── analyze-on-edit.sh ← advisory exemplar (unregistered; its Register: stanza is in its header) +│ └── lib/ +│ └── resolve-path.sh ← the path resolver the ADR guard sources (sourced, never run) +├── rules/ +│ ├── cbk-conventions.md ← project conventions — contract half (template; you fill this) +│ ├── cbk-conventions-reference.md ← its path-scoped reference half +│ ├── orchestration.md ← model × effort tiering — contract half (template) +│ ├── orchestration-reference.md ← its path-scoped reference half +│ ├── pr-review.md ← review floor, roster, rubric — contract half +│ ├── pr-review-reference.md ← its path-scoped reference half (calibration tables) +│ ├── knowledge-backend.md ← Notion-axis contract — contract half (delete both halves with its hook when the axis is none) +│ ├── knowledge-backend-reference.md ← its path-scoped reference half +│ ├── testing.md ← three-regime testing (path-scoped; stamp the globs) +│ ├── logging.md ← structured logging (path-scoped; stamp the glob) +│ ├── simplification.md ← /simplify contract +│ ├── workflows.md ← agent workflow patterns (portable) +│ └── tooling.md ← tool-selection skeleton (template) +├── workflows/ +│ ├── review-sweep.js ← find-then-verify review orchestration +│ ├── finish-ab/ ← A/B harness exemplar: two to four worktree-isolated arms, a balanced blind judge panel, and run-arms-headless.py, which runs each arm as a headless session +│ ├── agent-cost.py ← per-agent cost reader for a run's transcripts +│ └── tests/ ← the verification runner and every fixture the block runs (hooks, CI job bodies, review-bot templates, workflow scripts, run-arms-headless-fixture.sh among them) +└── settings.json ← hook registration + plugin/MCP manifest + +.github/ +├── dependabot.yml.example ← Dependabot configuration to copy, with the settle-window floor +└── workflows/ + └── adr-immutability-check.yml ← CI gate enforcing ADR-0000 at raw-git level + +.mcp.json.example ← MCP server config template +``` + +That is the drop-in set the Quick start installs; scaffold then writes `docs/adr/` from `references/adr-starters/`. The rest of this repository is the kit's own and is never installed: `README.md`, `CHANGELOG.md`, `CLAUDE.md` (instructions for working on the kit), `LICENSE`, `.gitignore`, `.github/workflows/verify.yml` (the kit's CI) and `docs/` (the kit's own decision records and harvest designs). + +Each skill follows the same pattern: a `SKILL.md` entrypoint plus a `references/` directory with templates and operational reference docs (failure modes, question banks, axis-specific behavior, inheritance discipline). The three producing phases — framing, rough-in and `/finish` — are contract-first: `references/contract.md` (for the executor, `commands/finish.md` itself) is the drafting read, `references/procedure.md` (`commands/finish-procedure.md`) the step-by-step on demand, and `SKILL.md` routes. Skills load `references/*.md` lazily on demand. +````` + +**9. Plugins by their install ids, the MCP servers without GitHub, the conventions without the `mise run +check` claim.** **Replace** *(whole lines 353–384)*: +````markdown +### Claude Code plugins + +Listed declaratively in `.claude/settings.json` under `enabledPlugins`. Install each via Claude Code's plugin system before running `/finish`: + +- **`pr-review-toolkit`** — `/finish`'s review pass dispatches `pr-review-toolkit:review-pr` for the auto-triage sweep +- **`/simplify`** — `/finish`'s simplify pass invokes it before the review pass. It is a **bundled Claude Code skill**, not a plugin (verified against the installed CLI bundle, Claude Code 2.1.263, 2026-09-06 — re-verify after harness upgrades), so it is deliberately absent from `enabledPlugins`; add `"simplify": true` there only if your install ships it as a plugin +- **`commit-commands`** — convenient wrappers for staging + committing (used by examples in this kit's docs) + +The slash commands and skills these provide are referenced by name in `/finish` and the rules files; if your install uses different identifiers, edit the references. + +### MCP servers + +Listed declaratively in `.claude/settings.json` under `enabledMcpjsonServers`. Configure in `.mcp.json` (copy from `.mcp.json.example`): + +- **`github`** — required by every cascade phase that writes to a GitHub repo. PAT with repo + read:project scopes (classic) or fine-grained PAT with Contents/Metadata write + Issues/Pull-requests write per repo. +- **`linear`** — required only when the planning backend is Linear. +- **`notion`** (Notion's official MCP, `notion.com/help/notion-mcp`) — required only when the knowledge backend is Notion. Used by every phase that reads from or writes to the knowledge backend. +- **`context7`** — used by framing and rough-in research phases for library/framework doc lookups. Reduces stale-knowledge errors when the cascade picks dependencies. +- **`time`** — optional, used by skills that need ISO-8601 conversions or timezone math. + +If an expected MCP is missing, the cascade falls back to manual / web-search paths and surfaces the gap. + +### Project conventions + +The kit's load-bearing assumption is that your project has — or will have, after `blueprint` runs — these surfaces: + +- A task runner (`mise.toml` is what `/finish` references in `mise run check`; if you use `Makefile` or `justfile`, edit `/finish` to match) +- `docs/STANDARDS.md` with a quality bar and CI Pipeline table +- The twelve files under `.claude/rules/`: the contract + reference pairs for conventions, orchestration and review; `testing.md` and `logging.md` (path-scoped — stamp their globs); `simplification.md`; `workflows.md` (portable); `tooling.md` (a template); and `knowledge-backend.md` for the Notion axis. `/finish` and the reviewer agents reference them by name. +- ADR scaffolding under `docs/adr/` (we ship the starter; `adr-new` skill maintains it) + +If your project uses a different layout, the kit still works but `/finish` becomes the friction point — edit it after dropping the kit in. See "Customization" below. +```` +with: +````markdown +### Claude Code plugins + +Listed in `.claude/settings.json` under `enabledPlugins`, keyed `@`. Install each in a Claude Code session before running `/finish` (Quick start, step 4): + +- **`pr-review-toolkit@claude-plugins-official`** — `/finish`'s review pass invokes `pr-review-toolkit:review-pr`, one half of the review floor +- **`/simplify`** — `/finish`'s simplify pass invokes it before the review pass; it is the other half of the floor. It is a skill bundled with Claude Code, not a plugin, so it is deliberately absent from `enabledPlugins`. Its version history is recorded once, in `.claude/rules/simplification.md` § Plugin +- **`commit-commands@claude-plugins-official`** — convenient wrappers for staging and committing (used by examples in this kit's docs) + +The slash commands and skills these provide are referenced by name in `/finish` and the rules files; if your install uses different identifiers, edit the references. + +### MCP servers + +`.mcp.json.example` lists each server with a comment on what uses it; `enabledMcpjsonServers` in `.claude/settings.json` names the ones approved for the project. Copy the example to `.mcp.json` and commit it (Quick start, step 3): + +- **`linear`** (hosted; sign in through `/mcp`, OAuth in the browser) — required only when the planning backend is Linear. +- **`notion`** (Notion's official MCP, `notion.com/help/notion-mcp`) — required only when the knowledge backend is Notion. Used by every phase that reads from or writes to the knowledge backend. Keep the key `notion`: the knowledge-backend ask-gate matches that server's tool names. +- **`context7`** — used by framing and rough-in research phases for library/framework doc lookups. Reduces stale-knowledge errors when the cascade picks dependencies. +- **`time`** (launched through `uvx` at the version `.mcp.json.example` pins; needs `uv`) — optional, used by skills that need ISO-8601 conversions or timezone math. + +There is no GitHub server: the kit's GitHub interface is the `gh` CLI (`.claude/rules/tooling.md` § Planning backend). If an expected MCP is missing, the cascade falls back to manual / web-search paths and surfaces the gap. + +### Project conventions + +The kit's load-bearing assumption is that your project has — or will have, after `blueprint` runs — these surfaces: + +- A `check` task in whatever task runner the project uses: `/finish` runs "the project's `check` task", and blueprint's tooling template defaults to `mise` and makes the verification runner a task `check` depends on +- `docs/STANDARDS.md` with a quality bar and CI Pipeline table +- The files under `.claude/rules/`: the contract and reference pairs for conventions, orchestration, review and the knowledge backend; `testing.md` and `logging.md` (path-scoped — stamp their globs); `simplification.md`; `workflows.md` (portable); and `tooling.md` (a template). `/finish` and the reviewer agents reference them by name. +- ADR scaffolding under `docs/adr/` (scaffold writes it from the starters; the `adr-new` skill maintains it) + +If your project uses a different layout, record it in `cbk-conventions.md`: `/finish` reads the conventions and needs no edit of its own. See "Customization" below. +```` + +**10. Customization: `finish.md` needs no edit, the budget figure is the block's, the Explore override.** +**Replace** *(whole lines 392–401)*: +````markdown +2. **`.claude/commands/finish.md`** — bakes in the project's `check` task, `docs/STANDARDS.md § Step 4`, `.claude/rules/testing.md`, `.claude/rules/logging.md`, `pr-review-toolkit:review-pr`, `/simplify`. If your stack doesn't have one of these, edit the file. The eight-section spec contract that `/finish` reads from issue bodies is the stable interface; the tooling assumptions are the swap-out point. + +3. **`.claude/settings.json`** — adjust `enabledPlugins` if your installed identifiers differ; adjust `enabledMcpjsonServers` if you don't use one of the five defaults or want to add others. Never add a hook-shaped object as a top-level key while doing so — the advisory hooks' registration stanzas live in their headers (see `.claude/rules/cbk-conventions-reference.md` § Hook authoring). + +4. **`.claude/rules/logging.md` and `.claude/rules/testing.md`** — stamp the `paths:` globs at the top with your project's real extensions; they ship as placeholders and a placeholder glob loads nothing. The always-loaded rule set at the kit's current sha totals the figure the verification block prints (`always-loaded total: … bytes`; 130,628 at harvest 4) — the number a fresh target starts from before path-scoping its own rules. Then settle a disposition for each template rule (`orchestration.md`, `tooling.md`, and the bracketed entry in `cbk-conventions-reference.md`'s `paths:`): fill, path-scope, or delete — see `cbk-conventions.md` § Rule loading and the instruction budget. + +Optional further customization: +- Add project-local reviewer agents under `.claude/agents/` (the kit ships `adr-conformance-reviewer`, `logging-discipline-reviewer` and `cascade-rule-reviewer`; add your own for project-specific concerns). +- Tune `pr-review.md`'s Apply/Surface calibration as you learn what's noisy in your project's PRs. +- If you don't use ADRs, delete `docs/adr/` and remove the hook registration from `settings.json`. +```` +with: +````markdown +2. **`.claude/commands/finish.md`** — needs no edit. It runs the project's `check` task in whatever runner defines it, and reads the foundation docs and the rules that bind the diff. What a project customizes is what those name: the `check` task, the foundation docs blueprint writes, and the `paths:` globs in `testing.md` and `logging.md`. The eight-section spec contract that `/finish` reads from issue bodies is the stable interface. A project that must diverge carries the change as a named exception (`.claude/rules/cbk-conventions-reference.md` § Syncing the kit) and edits the bundled copy, `.claude/skills/rough-in/references/finish-command.md`, in the same commit, because the verification block diffs the pair. + +3. **`.claude/settings.json`** — adjust `enabledPlugins` if your installed identifiers differ (keys are `@`); adjust `enabledMcpjsonServers` to the servers your `.mcp.json` keeps. Never add a hook-shaped object as a top-level key while doing so — the advisory hooks' registration stanzas live in their headers (see `.claude/rules/cbk-conventions-reference.md` § Hook authoring). + +4. **`.claude/rules/logging.md` and `.claude/rules/testing.md`** — stamp the `paths:` globs at the top with your project's real extensions; they ship as placeholders and a placeholder glob loads nothing. The verification block prints the always-loaded rule set and its total (`always-loaded total: … bytes`): the number a fresh target starts from before path-scoping its own rules, which `CHANGELOG.md` records per release. Then settle a disposition for each template rule (`orchestration.md`, `tooling.md`, and the bracketed entry in `cbk-conventions-reference.md`'s `paths:`): fill, path-scope, or delete — see `cbk-conventions.md` § Rule loading and the instruction budget. + +Optional further customization: +- Add project-local reviewer agents under `.claude/agents/` (the kit ships `adr-conformance-reviewer`, `logging-discipline-reviewer` and `cascade-rule-reviewer`; add your own for project-specific concerns). +- Keep or delete `.claude/agents/Explore.md`. It overrides Claude Code's built-in Explore agent — "A user or project subagent named `Explore` overrides the built-in and keeps its own `model` field" — with one pinned to `model: haiku` and set to `omitClaudeMd: true`, which launches it "without the user, project, and local CLAUDE.md files" (`https://code.claude.com/docs/en/sub-agents`, read 2026-09-30). Delete it to keep the built-in. +- Tune `pr-review.md`'s Apply/Surface calibration as you learn what's noisy in your project's PRs. +- If you don't use ADRs, delete `docs/adr/` and remove the hook registration from `settings.json`. +```` + +**11. The upgrade gotcha.** **Replace** *(whole line 430)*: +````markdown +**No upgrade mechanism yet.** When the kit revises, re-pull manually. A future addition might be a `mise run cbk:update` task that fetches from the public repo, but that's down the road. +```` +with: +````markdown +**Upgrade by release, never by re-extracting.** Re-running the install over a repository that carries the kit would replace your filled files; the sync keeps them. See Upgrading. +```` + +**12. The version statement.** **Replace** *(whole line 437)*: +````markdown +- **Not finished.** Some operations along the Linear-planning and Notion-knowledge paths have documentation gaps (the kit falls back to manual where MCP-based ops aren't fully documented). The kit is at evergreen v0 — usable, but expect rough edges and surface them. +```` +with: +````markdown +- **Not finished.** Some operations along the Linear-planning and Notion-knowledge paths have documentation gaps (the kit falls back to manual where MCP-based ops aren't fully documented). The kit is at v1.0.0 — versioned, with a sync path between releases in `CHANGELOG.md` — and still has rough edges; surface them. +```` + +**13. License and Contributing.** **Replace** *(whole lines 452–458)*: +````markdown +## License + +See [LICENSE](LICENSE). + +## Contributing + +Issues and PRs welcome. The kit is itself a work-in-progress and rough edges are expected — surface them. +```` +with: +````markdown +## License + +Apache License 2.0 — see [LICENSE](LICENSE). The files you install from the kit stay under it; its section 4 sets what redistributing them requires, starting with "You must give any other recipients of the Work or Derivative Works a copy of this License". The kit's licence does not choose your project's own: scaffold asks for that separately (`.claude/rules/cbk-conventions-reference.md` § Licensing). + +## Contributing + +Issues and PRs welcome. The kit is itself a work-in-progress and rough edges are expected — surface them. A change that alters what a target syncs adds its entry, with its Sync notes, under `CHANGELOG.md`'s `## [Unreleased]` heading. +```` + +Then restamp what only the execution day knows: +```bash +cc=$(claude --version | awk '{print $1}'); [ "$cc" = 2.1.285 ] || sed -i "s/^v1\.0\.0 was checked on Claude Code 2\.1\.285\./v1.0.0 was checked on Claude Code $cc./" README.md +today=$(date -u +%F); [ "$today" = 2026-09-30 ] || sed -i -e "s/read 2026-09-30/read $today/g" -e "s/(restamped 2026-09-30 /(restamped $today /" README.md +``` + +- [ ] **Step 6: Run the install test, the gate and the sanitization greps — green** + +Run: +```bash +bash "$S/install-test.sh" "$PWD"; echo "install=$?" +for h in .claude/hooks/*.sh .claude/hooks/lib/*.sh; do bash -n "$h" || echo "bash -n failed: $h"; done +bash .claude/workflows/tests/run-verification-block.sh 2>&1 | grep -E '^verification: '; echo "exit=${PIPESTATUS[0]}" +git grep -n -i -E '\bcr[e]ase\b|CR[E]-[0-9]|you-are-he[a]r|echospher[e]' -- README.md; echo "names=$?" +grep -nE '(^|[[:space:](,;])#[0-9]{1,3}([^0-9]|$)' README.md; echo "bare=$?" +``` +Expected: `install test: files, only the drop-in set landed; printed 'Kit commit: v1.0.0 ()'` +(134 files on the planning copy; more once V1–V9's files are in), `install=0`; no `bash -n failed` line; +`verification: kit sub-block complete`, `verification: done`, `exit=0`; `names=1`; `bare=1`. + +- [ ] **Step 7: Commit** + +```bash +git add README.md .claude/rules/cbk-conventions-reference.md +git commit -F - <<'EOF' +docs(V10): README — install the drop-in set from a tagged release, and say how to start a phase + +The Quick start no longer extracts the whole repository over a target. It copies only the +drop-in set (.claude/, .mcp.json.example, .github/dependabot.yml.example and the ADR job) from +a tagged archive, refuses when a .claude/ already exists, and prints the Kit commit to record. +A new Upgrading section replaces "no upgrade mechanism". Prerequisites name what each absence +costs, and the entry table says how each phase starts. The inventory is recounted from the +tree, with the helper, the knowledge-backend pair, the N-arm harness and the kit-only files +named. The finish.md customisation paragraph, the advisory-stanza line, the plugin ids, the +MCP servers and the secret handling match the tree. The block pins the inventory and the pinned +version, and a local archive of the branch head proves only the drop-in set lands. + +Trace: release/3, release/4, review/consistency/1, review/consistency/2, review/consistency/6, +review/consistency/7, review/release/23, review/release/25, review/release/26, +review/release/27, review/release/30, review/release/61, review/release/64 (the License +paragraph); README parts of #60/c5892401033/helper, #69/body/F9, #69/body/finish-ab/N-arm, +#69/body/finish-ab/runner, review/release/28, review/security/17, review/claude-code/13, +review/release/29, review/release/63, review/consistency/40 (the finish.md sentence, handed from +V3), review/consistency/51 (V9.18's count wording); the README lines V8 handed in for +#70/body/6a and #70/body/6b (the linear and time notes), and V1's reference-glob line in step 2. + +Co-Authored-By: Claude Opus 5.5 +EOF +``` + +### Task V10.4: This repository's `CLAUDE.md` — the gate, the release rule, the R14 audit, and the commands' posture (release/8, review/release/65, #58/c5901493591/R14, review/claude-code/14, critic/9, the CLAUDE.md parts of #60/c5892401033/helper, #69/body/finish-ab/N-arm and review/release/63, release/5 for V10's own files; D41, D53, D57) + +**Files:** +- Modify: `CLAUDE.md` — eight replacements +- Modify: `.claude/rules/cbk-conventions-reference.md` — one check at the kit sub-block's sentinel + +**Interfaces:** +- Consumes: V3's `disable-model-invocation: true` and `arguments:` frontmatter on all five commands (V3 hands the + posture sentence to this task); V2's `lib/resolve-path.sh`; V5's `knowledge-backend-reference.md`; V6's + `run-arms-headless.py`; V3's `omitClaudeMd: true` on `Explore.md`; `CHANGELOG.md` (Task V10.2). +- Produces: `CLAUDE.md` § Working in this repo's bullets `**Releases are tags, and `CHANGELOG.md` is the upgrade + contract.**` and `**A harvest closes by its trace, not by impression.**`, the gate every future harvest runs. +- Produces: the block sentence `CLAUDE.md does not name (its gate, its release record and its harvest + audit)`, and a check that `README.md` and `CLAUDE.md` cite no issue by a bare number. V9's bare-citation check + covers the shipped kit files; these two are V10's, and `CHANGELOG.md` is exempt by design (Task V10.2). +- `CLAUDE.md` stays well under the ~200-line bloat line (102 lines at `74edf84`, 113 after; the layout keeps its code fence). + +- [ ] **Step 0: Confirm what the text names** + +Run: +```bash +grep -l '^disable-model-invocation: true' .claude/commands/*.md | wc -l +grep -l '^arguments:' .claude/commands/*.md | wc -l +ls .claude/hooks/lib/resolve-path.sh .claude/rules/knowledge-backend-reference.md .claude/workflows/finish-ab/run-arms-headless.py CHANGELOG.md +grep -c '^omitClaudeMd: true$' .claude/agents/Explore.md +grep -c '^model: haiku$' .claude/agents/Explore.md +curl -sL https://code.claude.com/docs/en/skills.md -o "$S/skills.md" +norm "$S/skills.md" | grep -cF 'such as `$0` for the first argument or `$1` for the second' +``` +Expected: `5`, `5`, the four paths listed without error, `1`, `1`, `1`. Anything else: stop and reconcile the +sentence that names it (a `0` on the last line means the skills page changed: re-read it, and correct or drop the +quotation in passage 8 before writing it). If the day is not 2026-09-30, restamp the one date passage 8 writes, after +Step 3: `today=$(date -u +%F); sed -i "s|(\`https://code.claude.com/docs/en/skills\`, read 2026-09-30)|(\`https://code.claude.com/docs/en/skills\`, read $today)|" CLAUDE.md`. + +- [ ] **Step 1: Add the failing checks to the block** + +**Replace** in `.claude/rules/cbk-conventions-reference.md` *(whole line; the kit sub-block's sentinel)*: +````text +echo "verification: kit sub-block complete" +```` +with: +````text +# Releases (V10): on the kit tree, CLAUDE.md names the gate, the release record and the harvest audit, and the +# kit-only front door (README.md, CLAUDE.md) cites no issue by a bare number. +if [ ! -f docs/cbk/scaffold.md ]; then + for w in 'run-verification-block.sh' 'CHANGELOG.md' 'refute-by-default'; do grep -qF -- "$w" CLAUDE.md || { echo "CLAUDE.md does not name $w (its gate, its release record and its harvest audit)"; exit 1; }; done + absent grep -nE '(^|[[:space:](,;])#[0-9]{1,3}([^0-9]|$)' README.md CLAUDE.md +fi + +echo "verification: kit sub-block complete" +```` + +- [ ] **Step 2: Run the block — red** + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | tail -2; echo "exit=${PIPESTATUS[0]}"` + +Expected: +``` +CLAUDE.md does not name run-verification-block.sh (its gate, its release record and its harvest audit) +verification: block exited 1 +exit=1 +``` +Record the first line. (The bare-citation half was mutation-tested while planning: a line `see #58 here` +appended to `README.md` printed `README.md:526:see #58 here` and `VIOLATION (matched above): grep -nE …`.) + +- [ ] **Step 3: Rewrite the eight passages** + +**1. What this repo is.** **Replace** *(whole line 7)*: +````markdown +`context-builder-kit` is **not a software project** — it is a kit that exports a set of Claude Code skills, commands, and rule files used to walk an idea from "raw thought" through to "automatically executed code issue." Everything in this repo is markdown content consumed by Claude Code itself; there is no code to build, no test suite to run, no application to start. +```` +with: +````markdown +`context-builder-kit` is **not a software project** — it is a kit that exports a set of Claude Code skills, commands, and rule files used to walk an idea from "raw thought" through to "automatically executed code issue." Most of it is markdown consumed by Claude Code itself. The rest is the code that enforces it — bash hooks, JavaScript workflow scripts and Python harness scripts, each with a fixture — and the verification block that runs every fixture. There is nothing to build and no application to start. +```` + +**2. The deliverable is the drop-in set.** **Replace** *(whole line 9)*: +````markdown +The deliverable is the contents of `.claude/` — the cascade skills (`consultation`, `scaffold`, `blueprint`, `framing`, `rough-in`), the `/finish` slash command, the `adr-new` skill, and the example project-rules file. They are designed to be installed (copied or symlinked) into target projects' `.claude/` directories. +```` +with: +````markdown +The deliverable is the drop-in set: `.claude/` — the cascade skills (`consultation`, `scaffold`, `blueprint`, `framing`, `rough-in`), the `adr-new` skill, the `/finish` executor and the contribution-lane commands, the reviewer agents, the hooks, the rules and the workflows — plus `.mcp.json.example`, `.github/dependabot.yml.example` and `.github/workflows/adr-immutability-check.yml`. A target installs it from a tagged release (`README.md` § Quick start) and syncs it release by release (`CHANGELOG.md`, and `.claude/rules/cbk-conventions-reference.md` § Syncing the kit). Everything else here — `README.md`, `CHANGELOG.md`, this file, `LICENSE`, `.gitignore`, `.github/workflows/verify.yml` and `docs/` — is the kit's own and is never installed. +```` + +**3. The layout, with the helper, the split rules, the harness, `.github/`, `docs/` and the changelog.** +**Replace** *(whole lines 32–51, inside the layout's code fence)*: +````text +``` +.claude/ +├── commands/ +│ ├── finish.md ← the executor slash-command (phase 6) — the contract +│ ├── finish-procedure.md ← the executor's procedure, read on demand +│ ├── intake.md ← bottom-up entry: externally-sourced report → /finish-able issue +│ ├── enrich.md ← rough-in for one small capability (enhancement lane, skips framing) +│ └── pr-respond.md ← the PR feedback-loop executor (inverse of /finish) +├── agents/ ← project-local PR reviewers (adr-conformance, logging-discipline, cascade-rule; memory-enabled) + Explore (cheap-tier search exemplar) +├── hooks/ ← guards in four tiers: hard-deny (protect-immutable-adrs, protect-lock-files, protect-main-branch, require-repo-root-for-agents) + ask-gate (guard-pr-state, require-knowledge-backend-ok) + advisory exemplars, unregistered (format-on-edit, analyze-on-edit) + stop (detect-forked-agent-memory) +├── rules/ ← operational contracts (cbk-conventions, pr-review — each split into an always-loaded contract and a path-scoped `-reference.md` half — plus testing, logging, simplification, knowledge-backend) and rule templates (tooling; orchestration, itself also split); workflows.md is portable +├── workflows/ ← saved orchestrations (review-sweep: find-then-adversarially-verify review pass) and harness exemplars (finish-ab/ two-arm A/B, agent-cost.py) with their stub tests +└── skills/ + ├── consultation/SKILL.md + references/ (notion_ingestion, frozen_corpus_ingestion, …) ← phase 1 + ├── scaffold/SKILL.md + references/ (adr-starters/ incl. corrections.md, issue-templates/, github-starter-templates.md, templates/cascade-events-index-template.md, …) ← phase 2 + ├── blueprint/SKILL.md + references/ (templates/ incl. roadmap.md, claude-review.yml, claude.yml, …) ← phase 3 + ├── framing/SKILL.md + references/ (contract.md, procedure.md, …) ← phase 4 + ├── rough-in/SKILL.md + references/ (contract.md, procedure.md, …) ← phase 5 + └── adr-new/SKILL.md ← ADR scaffolder (used by target projects) +``` +```` +with: +````text +``` +.claude/ +├── commands/ +│ ├── finish.md ← the executor slash-command (phase 6) — the contract +│ ├── finish-procedure.md ← the executor's procedure, read on demand +│ ├── intake.md ← bottom-up entry: externally-sourced report → /finish-able issue +│ ├── enrich.md ← rough-in for one small capability (enhancement lane, skips framing) +│ └── pr-respond.md ← the PR feedback-loop executor (inverse of /finish) +├── agents/ ← project-local PR reviewers (adr-conformance, logging-discipline, cascade-rule; memory-enabled) + Explore (a Haiku-pinned override of the built-in Explore, with omitClaudeMd; delete it to keep the built-in) +├── hooks/ ← guards in four tiers: hard-deny (protect-immutable-adrs, protect-lock-files, protect-main-branch, require-repo-root-for-agents) + ask-gate (guard-pr-state, require-knowledge-backend-ok) + advisory exemplars, unregistered (format-on-edit, analyze-on-edit) + stop (detect-forked-agent-memory); lib/resolve-path.sh is the path resolver the ADR guard sources +├── rules/ ← operational contracts (cbk-conventions, pr-review, knowledge-backend — each split into an always-loaded contract and a path-scoped `-reference.md` half — plus testing, logging, simplification) and rule templates (tooling; orchestration, itself also split); workflows.md is portable +├── workflows/ ← saved orchestrations (review-sweep: find-then-adversarially-verify review pass), harness exemplars (finish-ab/: a two-to-four-arm A/B workflow and run-arms-headless.py, its headless runner; agent-cost.py), and tests/: the verification runner and every fixture the block runs +└── skills/ + ├── consultation/SKILL.md + references/ (notion_ingestion, frozen_corpus_ingestion, …) ← phase 1 + ├── scaffold/SKILL.md + references/ (adr-starters/ incl. corrections.md, issue-templates/, github-starter-templates.md, templates/cascade-events-index-template.md, …) ← phase 2 + ├── blueprint/SKILL.md + references/ (templates/ incl. roadmap.md, claude-review.yml, claude.yml, …) ← phase 3 + ├── framing/SKILL.md + references/ (contract.md, procedure.md, …) ← phase 4 + ├── rough-in/SKILL.md + references/ (contract.md, procedure.md, …) ← phase 5 + └── adr-new/SKILL.md ← ADR scaffolder (used by target projects) +.github/ +├── dependabot.yml.example ← installed: the Dependabot configuration a target copies +└── workflows/ + ├── adr-immutability-check.yml ← installed: the ADR lint at raw-git level + └── verify.yml ← the kit's own CI: runs the verification block on every pull request +docs/ +├── adr/ ← the ADR starters, byte-identical to scaffold's references/adr-starters/ (the block diffs them) +└── superpowers/ ← each harvest's design spec, trace and implementation plans +CHANGELOG.md ← one section per release, each with the Sync notes a target follows +``` +```` + +**4. There is a gate.** **Replace** *(whole line 77)*: +````markdown +Because there is no build/test/lint, the verification surface is editorial: +```` +with: +````markdown +There is no build, but there is a gate. `bash .claude/workflows/tests/run-verification-block.sh` runs the verification block (`.claude/rules/cbk-conventions-reference.md` § Verification), which runs every fixture, `run-verification-block-fixture.sh` among them, the one that drives the runner's own rails; `.github/workflows/verify.yml` runs it on every pull request. It needs `bash`, `git`, `jq`, `node` and `python3`, and it is green after every commit. Beyond it, the verification surface is editorial: +```` + +**5. The release rule and the R14 audit, after the templates bullet.** **Replace** *(whole line 82)*: +````markdown +- **Templates live in `references/templates/`** and are quoted verbatim in skill output. Edits to a template change every future cascade artifact — treat them as the contract. +```` +with: +````markdown +- **Templates live in `references/templates/`** and are quoted verbatim in skill output. Edits to a template change every future cascade artifact — treat them as the contract. +- **Releases are tags, and `CHANGELOG.md` is the upgrade contract.** A change that alters what a target syncs adds its entry under the changelog's `## [Unreleased]` heading, with a **Sync notes** line for anything a target must do by hand. A release renames that heading `## [X.Y.Z] — — (PR #)`, where the date is the merge's in UTC and the number is the PR's, both confirmed just before the merge, and after the merge, with the operator's go-ahead, the merge commit is tagged `vX.Y.Z`, annotated. A major bump means a sync needs hand reconciliation beyond `git merge-file`, a minor bump is a harvest, and a patch is fixes only. +- **A harvest closes by its trace, not by impression.** Every ask a harvest takes in, an issue's body item or one row of a comment, gets one row in a trace file beside the design spec (`docs/superpowers/specs/-harvest--trace.md`). The row carries the ask's location as `#/body/` or `#/c/`, its cluster, whether it is live at the base commit, the decision it rests on, and a status that starts `open`. Before merge every row reads `landed ` (the commit that names it), `non-goal` (named in the spec's § Non-goals) or `not holding` (re-checked at its commit and found not to hold, with a one-line reason). Then one fresh-context verifier, refute-by-default and read-only, tries to show that each row not marked `landed` was required and is still live at the branch head, and every row it refutes is landed. An issue closes only when every row for it is landed or a named non-goal (context-builder-kit#58, residue row R14). +```` + +**6. The knowledge-backend rule is split.** **Replace** *(whole line 95)*: +````markdown +- `knowledge-backend.md` — the operational contract for the knowledge-backend axis (read patterns, write tiering, HITL discipline, brownfield detection, lazy provisioning) +```` +with: +````markdown +- `knowledge-backend.md` — the operational contract for the knowledge-backend axis (read patterns, write tiering, HITL discipline, brownfield detection, lazy provisioning), split like the conventions into a contract and a path-scoped half +```` + +**7. The path-scoped halves.** **Replace** *(whole line 98)*: +````markdown +- `cbk-conventions-reference.md`, `orchestration-reference.md`, `pr-review-reference.md` — the path-scoped halves; see `cbk-conventions.md` § Rule loading and the instruction budget +```` +with: +````markdown +- `cbk-conventions-reference.md`, `orchestration-reference.md`, `pr-review-reference.md`, `knowledge-backend-reference.md` — the path-scoped halves; see `cbk-conventions.md` § Rule loading and the instruction budget +```` + +**8. The commands' invocation posture (handed from V3).** **Replace** *(inline, the end of line 102)*: +````markdown +(`adr-new` follows the same deliberate posture). +```` +with: +````markdown +(`adr-new` follows the same deliberate posture). The five commands — `/finish`, `/intake`, `/enrich`, `/pr-respond` and `finish-procedure` — carry `disable-model-invocation: true` too, for the same reason: the four executors open branches, issues and pull requests, and `finish-procedure` is the executor's reference, not a step to run. A command that takes an argument declares it in `arguments:` frontmatter and reads it by name, because positional placeholders count from zero: "such as `$0` for the first argument or `$1` for the second" (`https://code.claude.com/docs/en/skills`, read 2026-09-30). +```` + +- [ ] **Step 4: Run the gate — green** + +Run: +```bash +for h in .claude/hooks/*.sh .claude/hooks/lib/*.sh; do bash -n "$h" || echo "bash -n failed: $h"; done +bash .claude/workflows/tests/run-verification-block.sh 2>&1 | grep -E '^verification: '; echo "exit=${PIPESTATUS[0]}" +git grep -n -i -E '\bcr[e]ase\b|CR[E]-[0-9]|you-are-he[a]r|echospher[e]' -- CLAUDE.md; echo "names=$?" +wc -l < CLAUDE.md +``` +Expected: no `bash -n failed` line; `verification: kit sub-block complete`, `verification: done`, `exit=0`; +`names=1`; `113`. + +- [ ] **Step 5: Commit** + +```bash +git add CLAUDE.md .claude/rules/cbk-conventions-reference.md +git commit -F - <<'EOF' +docs(V10): CLAUDE.md — the kit has a gate, releases are tags, and a harvest closes by its trace + +"No code to build, no test suite" no longer holds: the kit ships hooks, JavaScript and Python, +each with a fixture, and the verification block runs them all. CLAUDE.md now names the runner, +the fixture that drives its rails, its tools and verify.yml. The deliverable is the drop-in set, and the layout adds the path +helper, the knowledge-backend pair, the N-arm harness, .github/, docs/ and CHANGELOG.md. Two +bullets make the release rule and the R14 trace audit the gate for every future harvest, and +the invocation-posture paragraph names the five invoke-only commands and why each reads its +argument by name. The block pins these names and keeps bare issue numbers out of README.md and +CLAUDE.md. + +Trace: release/8, review/release/65 (with the runner's fixture, handed from V1), +#58/c5901493591/R14 and critic/9 (the CLAUDE.md codification of the audit, handed from V9), +review/claude-code/14 (the posture sentence, handed from V3), the CLAUDE.md parts of +#60/c5892401033/helper, #69/body/finish-ab/N-arm and review/release/63, release/5 (the check +on V10's own files). + +Co-Authored-By: Claude Opus 5.5 +EOF +``` + +### Task V10.5: `LICENSE` names its copyright holder (review/release/64; D41) + +**Files:** +- Modify: `LICENSE` — the appendix's copyright line +- Modify: `.claude/rules/cbk-conventions-reference.md` — one check at the kit sub-block's sentinel + +**Interfaces:** +- Consumes: the operator's confirmation of the holder's name, and `README.md` § License (Task V10.3), which + already tells a target what the licence asks of a redistributed copy. +- Produces: `Copyright 2026 ` in place of `Copyright [yyyy] [name of copyright owner]`. 2026 is the year + of the kit's first commit (`git log --reverse --format=%ad --date=format:%Y | head -1` prints `2026`). +- Produces: the block check `absent grep -nF '[name of copyright owner]' LICENSE` on the kit tree. +- No `NOTICE` file ships: the licence's section 4(d) binds a redistributor only to a NOTICE that exists, and the + finding's other asks are the copyright line and the README paragraph. + +- [ ] **Step 1: Add the failing check to the block** + +**Replace** in `.claude/rules/cbk-conventions-reference.md` *(whole line; the kit sub-block's sentinel)*: +````text +echo "verification: kit sub-block complete" +```` +with: +````text +# Releases (V10): on the kit tree, LICENSE names its copyright holder instead of the appendix's placeholder. +[ -f docs/cbk/scaffold.md ] || absent grep -nF '[name of copyright owner]' LICENSE + +echo "verification: kit sub-block complete" +```` + +- [ ] **Step 2: Run the block — red** + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | tail -3; echo "exit=${PIPESTATUS[0]}"` + +Expected: +``` +189: Copyright [yyyy] [name of copyright owner] +VIOLATION (matched above): grep -nF [name of copyright owner] LICENSE +verification: block exited 1 +exit=1 +``` +Record the second line. + +- [ ] **Step 3: Confirm the holder with the operator** + +Run: `git config user.name` +Expected: one line, the operator's configured name. Show the operator that name and the proposed line +` Copyright 2026 `, and wait for an explicit yes. A different holder the operator gives replaces the +name. Nothing is written before the answer. + +- [ ] **Step 4: Write the line** + +**Replace** in `LICENSE` *(whole line 189, the appendix; three leading spaces)*: +````text + Copyright [yyyy] [name of copyright owner] +```` +with the confirmed line, written by: +```bash +holder=$(git config user.name) # or the holder the operator named in Step 3 +python3 - "$holder" <<'PY' +import sys +p = "LICENSE" +t = open(p, encoding="utf-8").read() +old = " Copyright [yyyy] [name of copyright owner]\n" +assert t.count(old) == 1, "anchor" +open(p, "w", encoding="utf-8").write(t.replace(old, f" Copyright 2026 {sys.argv[1]}\n")) +PY +git diff --stat LICENSE +``` +Expected: ` LICENSE | 2 +-` and `1 file changed, 1 insertion(+), 1 deletion(-)`. + +- [ ] **Step 5: Run the gate — green** + +Run: +```bash +for h in .claude/hooks/*.sh .claude/hooks/lib/*.sh; do bash -n "$h" || echo "bash -n failed: $h"; done +bash .claude/workflows/tests/run-verification-block.sh 2>&1 | grep -E '^verification: '; echo "exit=${PIPESTATUS[0]}" +``` +Expected: no `bash -n failed` line; `verification: kit sub-block complete`, `verification: done`, `exit=0`. + +- [ ] **Step 6: Commit** + +```bash +git add LICENSE .claude/rules/cbk-conventions-reference.md +git commit -F - <<'EOF' +docs(V10): LICENSE names its copyright holder + +The appendix carried the licence's unfilled placeholder. The kit's files are copied into +other projects, and the licence those copies keep should say whose it is. README.md § License +already says what section 4 asks of a redistributed copy. The block keeps the placeholder out. + +Trace: review/release/64. + +Co-Authored-By: Claude Opus 5.5 +EOF +``` +After the master plan's Task F4 pushes, confirm GitHub still detects the licence: +`gh api repos/j4th/context-builder-kit/license -q .license.spdx_id` prints `Apache-2.0`. If it prints anything +else, revert this commit's `LICENSE` hunk, restore the appendix placeholder, and put the copyright line in a +`NOTICE` file instead, in one follow-up commit that also removes the block check. + +## Coverage + +Every id in the V10 pack, then the rows of other clusters whose part lands in a file V10 owns. + +| Id | Kind | Lands in | +|---|---|---| +| `release/1` | item | V10.1 — § Syncing the kit in versions, and the contract pointer | +| `release/2` | item | V10.1 — the Kit commit row, its guidance, the example, `SKILL.md`, the checklist row, Test 1; V10.3 shows the row in the README example | +| `release/3` | item | V10.3 — the README inventory from the tree (hooks/lib, the knowledge-backend pair, the N-arm harness, the fixtures, `.github/`, the kit-only files); V10.4 — `CLAUDE.md`'s layout | +| `release/4` | item | V10.3 — the tagged, drop-in-only install that prints the release to record | +| `release/5` | item | V10.1 — § Syncing the kit's own `#58`; V10.4 — the check that keeps bare numbers out of `README.md` and `CLAUDE.md`. Handed to V2, V4, V6, V7 and V9 for the citations in their files, and to V9 for the kit-wide check (see below) | +| `release/6` | item | V10.2 — `CHANGELOG.md` | +| `release/7` | item | Master plan Task F5 — the six annotated tags after merge, with the operator's go-ahead; V10.2's headings are their messages | +| `release/8` | item | V10.4 — `CLAUDE.md`'s layout and the Releases bullet. No Surface inventory row is added to the always-loaded contract (D50): § Syncing the kit's fallback (V10.1) covers an older target | +| `release/9` | item | Handed to V9 — the `docs/STANDARDS.md` citations in the executor procedure pair (spec § V9 Citations, D53) | +| `release/10` | item | Handed to V9 — `review/portability/31` removes the sibling name at § Hook authoring and adds the absent check; V10.3 and V10.4 prove `README.md` and `CLAUDE.md` clean (`names=1`) | +| `#60/c5892401033/helper` | handed in | V10.3 — `lib/resolve-path.sh` in the README tree; V10.4 — in `CLAUDE.md`'s hooks line | +| `#67/c5881158070/2c` | handed in | V10.2 — the v1.0.0 Sync note "The review model is the action's pin", verified raw in Step 4 | +| `#69/body/F9` | handed in | V10.3 — the README's `/simplify` bullet points at `simplification.md` § Plugin instead of restating 2.1.263 | +| `#69/body/finish-ab/N-arm` | handed in | V10.3 — the README's `finish-ab/` line; V10.4 — `CLAUDE.md`'s workflows line | +| `#69/body/finish-ab/runner` | handed in | V10.3 — `run-arms-headless.py` in the README tree | +| `#69/c5859756889/apply-h4/5` | handed in | V10.2 — the v0.5.0 Sync note on a review workflow that already had `checks: read` | +| `#69/c5901496433/sync-note` | handed in | V10.1 — the § Syncing bullet; V10.2 — the v1.0.0 note on files run ahead of the kit | +| `review/consistency/1` | review, verified | V10.1 and V10.3 — the sync is documented, the README says Upgrading and v1.0.0; the axis restamp is V10.3 (the board stamp: see Not holding) | +| `review/consistency/2` | review, verified | V10.3 — no counts in the tagline, the full tree, the kit-only files named, the `mise run check` claim and the byte figure removed, the bottom-up lane paragraph; the block pins the tree | +| `review/consistency/6` | review, verified | V10.3 — step 3 commits `.mcp.json` with `${VAR}` references. The example's `_comment`, `tooling.md` and blueprint's table are V8's (`review/release/28`) | +| `review/consistency/7` | review, verified | V10.3 — the drop-in set only | +| `review/release/23` | review, verified | V10.3 — a pinned tag of the canonical URL, the drop-in set only, `docs/adr/` left to scaffold or copied from the starters, the fork note, no submodule line. `.gitattributes` export-ignore was not adopted: the install copies named paths, and V10.3's test proves it | +| `review/release/25` | review, verified | V10.3 — the prerequisites table with each absence's cost, the Claude Code floor with its quotation, the version it was checked on, the one-line missing-tools check | +| `review/release/26` | review, verified | V10.3 — the README restamp (the board part: see Not holding) | +| `review/release/27` | review, verified | V10.3 — the advisory hooks' tree lines say the stanza is in the header | +| `review/release/30` | review, verified | V10.3 — `finish.md` needs no edit; the named-exception route and the pair diff | +| `review/release/61` | review, unverified | Holds (no `/blueprint` or `/scaffold` anywhere in `README.md` at `74edf84`). V10.3 — the Run column, how a phase starts, and the first-launch trust and approval | +| `review/release/64` | review, unverified | Holds (`LICENSE:189` is the unfilled placeholder). V10.5 — the copyright line; V10.3 — the README License paragraph | +| `review/release/65` | review, unverified | Holds (`CLAUDE.md:7` and `:77`). V10.4 | +| `#69/c5859756889/apply-h4/1` | V9 row, V10 file | V9 or V2 lands the § Hook authoring sentence; V10.1 cross-references it in § Syncing the kit; V10.2 records the v0.5.0 Sync note | +| `#69/c5859756889/apply-h4/2` | V9 row, V10 region | V10.1 — the § Syncing bullet; V10.2 — the v0.5.0 note | +| `#69/c5859756889/apply-h4/3` | V9 row, V10 region | V10.1 — `**No Kit commit row to read.**`; V10.2 — the v0.5.0 note | +| `#69/c5859756889/apply-h4/6` | V9 row, V10 file | V1 wires the runner into the templates (R4); V10.2 records the v0.5.0 note | +| `#71/body/1` | V8 row, V10 region | V10.1 — the formatter-scope bullet in § Syncing the kit, handed from V8 by the ownership map | +| `#70/table/mise-mcp-row` | V8 row, V10 file | V10.2 — the release-notes line V8's item asks for | +| `#58/c5901493591/R14` | V9 row, V10 file | V10.4 — the audit codified in `CLAUDE.md` § Working in this repo | +| `review/claude-code/13`, `review/release/29` | V3 rows, V10 file | V10.3 — the README's plugin list and step 4 use `@claude-plugins-official` | +| `review/claude-code/14` | V3 row, V10 file | V10.4 — the posture sentence in `CLAUDE.md` | +| `review/release/63` | V3 row, V10 file | V10.3 — the Customization bullet on the Explore override, and the tree's `Explore.md` line in V3's words ("a Haiku-pinned override of the built-in Explore … delete it to keep the built-in"); V10.4 — the same words in `CLAUDE.md`'s agents line | +| `review/release/28`, `review/security/17` | V8 rows, V10 file | V10.3 — step 3's secret handling | +| `review/consistency/51` | V9 row, V10 file | V10.3 — the README's blueprint paragraph counts its docs in V9.18's words ("six prose foundation docs — seven on the `github-issues` and `in-repo-markdown` axes, where `docs/cbk/ROADMAP.md` joins them"); the count in blueprint's own files is V9's | +| `review/consistency/40` | V3 row, V10 file | V10.3 — Customization item 2: `finish.md` bakes in no `docs/STANDARDS.md § Step 4` and needs no edit (the same passage as `review/release/30`) | +| `critic/9` | V9 hand-off, V10 file | V10.4 — the R14 audit method in `CLAUDE.md` § Working in this repo (with `#58/c5901493591/R14`) | +| `#60/c5881158391/status` | V2 hand-off, V10 file | V10.2 — the Sync note for a target that customized its ADR hook: the kit's hook a `copy` row, `lib/resolve-path.sh` an `add` row, the tracked-helper check under **New files** | +| `#67/c5881158070/2c` and V4's shape changes | V4 hand-off, V10 file | V10.2 — the review-workflow hand-merge bullet: every changed line V4's § Handed to other clusters lists, the kept `REVIEW_LOGIN`, and the kit sub-block's fixtures now running against a target's filled workflow | +| `#69/c5881157875/2-fable5-misprice`, `#69/body/finish-ab/runner` (config fills) | V6 hand-off, V10 file | V10.2 — the "cost reader keys by model version" Sync note: legacy Fable 5 cache reads, `worktree_setup`, `WRITE_SERVERS` | +| `#70/body/4`, `#70/body/5`, `#70/body/6a`, `#70/body/6b`, `#71/body/3` | V8 hand-offs, V10 file | V10.2 — the Sync notes V8 wrote for V10: settle-window coverage, mise `[task_config]`, `.mcp.json`'s shape check and no GitHub entry, the `.gitignore` harness block; V10.3 — the `linear` and `time` notes in README § MCP servers. The `time` pin's release-day age check goes to Task F5 | +| V8's ruff correction (`#71/body/1`) | V8 hand-off, V10 region | V10.1 Step 5 — the quotation is the first clause of the sentence as the raw source writes it, cited at the raw URL it matches under `grep -F`, re-fetched 2026-09-30; the `force-exclude` exception is stated outside the quotation marks | +| V1 hand-off 7 (`#73/body/1`, `review/release/65`, `#69/c5859756889/apply-h4/6`) | V1 hand-off, V10 files | V10.3 — Quick start step 2 names `cbk-conventions-reference.md`'s manifest-and-lockfile glob; V10.2 — **A filled target owes both sentinels**: the runner a `copy` row with a third rail, its fixture an `add` row, the verification task in `check`, the reference glob stamped, the `CLAUDE.md` check keyed on `docs/cbk/blueprint.md`; V10.4 — passage 4 names `run-verification-block-fixture.sh` | +| V2 hand-off (`#60/c5892401033/helper`, `#60/c5881158391/status`, D61, P1) | V2 hand-off, V10 files | V10.3 and V10.4 — `hooks/lib/` in the tree and the layout; V10.2 — **The ADR guard judges what a path is**, **New files** (the tracked helper, the three hook fixtures and the others), **Fill the three hook backstop slots**, **An unreadable payload now denies**, the P1 note in the variant V2 applied (Step 0 detects it, Step 5 swaps it under variant A), and the re-copied ADR job's sub-bullet (three-dot, blobless, `shell: bash`) | +| V3 hand-off (`review/claude-code/14`, `review/release/29`, `#69/body/F9`, `review/release/63`, `review/consistency/40`) | V3 hand-off, V10 files | V10.4 passage 8 — the posture sentence, with the reason a command reads its argument by name quoted from the skills page (Step 0 verifies it); V10.3 — the plugin ids and install lines, `/simplify` pointing at `simplification.md` § Plugin, the Explore wording, the `finish.md` paragraph | +| V5 hand-off 8 (`#69/body/F9`, D59, `#67/c5881158070/2c`) | V5 hand-off, V10 files | V10.3 — `knowledge-backend-reference.md` in the tree (the count of rule files is dropped rather than restated) and the `/simplify` line; V10.4 — passages 6 and 7 name the split and the new half; V10.2 — the hand-merge sub-bullets for the orchestration pair (with the heading rename's citation sweep), `workflows.md`, `simplification.md`, `tooling.md` and the knowledge-backend pair (its `none`-axis disposition included), and the action-release note | +| V9 hand-off (Sync notes (a)–(e), `#69/c5859756889/apply-h4/2`, `/3`, `/5`, `/6`, `#69/c5901496433/sync-note`) | V9 hand-off, V10 files | V10.2 — **Re-copy the issue templates** (both copies, and the simulated sync now maps them), the `adr-new` sub-bullet's § Relation grains repoint, **Kit issue citations are qualified** (take the kit's side in the byte-copied files), and the `cbk-conventions.md` sub-bullet's D54 branch rule and CI-skip wording; V10.1 — § Syncing the kit, anchored on either citation spelling | + +The pack's own `critic` list is empty; `critic/9` is handed in from V9 (above). + +## Handed to other clusters + +- **V2, V4, V6, V7, V9 — `release/5`.** Each rewrites the bare kit-issue citations in the files it owns, in the + commit where it first edits them. V2: `detect-forked-agent-memory.sh` (six), `protect-lock-files.sh`, + `require-repo-root-for-agents.sh`, and § Hook authoring's `#58` citations. V4: `claude-review.yml` (two). V6: + `agent-cost.py`, `finish-ab.js`, `finish-ab-shape.mjs`. V7: `review-sweep.js` (three), + `review-sweep-accounting.mjs`. V9: the remaining `cbk-conventions-reference.md` lines, `finish.md:21` with + `finish-command.md:104`, `adr-new/SKILL.md:51`, and the kit-wide check. V9's check must not scan + `CHANGELOG.md`, where bare numbers are this repository's own by design, nor `README.md` and `CLAUDE.md`, which + V10.4 checks. +- **V9 — `release/9`.** The `docs/STANDARDS.md` citations in `finish-procedure.md` (lines 118, 136, 166, 221) + and in its bundled copy, re-pointed at headings every target has. +- **V9 — `release/10`.** `review/portability/31` removes `you-are-hear #81 → PR #82` from § Hook authoring's + stdin/exit bullet. V10.3 and V10.4 only prove `README.md` and `CLAUDE.md` clean. +- **V9 — the Projects v2 board stamp** (`review/release/26`, `review/consistency/1`). The four `backends.md` + copies and `cbk-conventions-reference.md` § Recommended planning-backend settings keep "designed-unexercised", + and are re-dated, not restamped: see Not holding. +- **V8 — `review/release/28`, `review/security/17`, `review/consistency/6`.** The example's `_comment`, + `tooling.md` § MCP configuration, `blueprint-output-template.md`'s `.mcp.json (gitignored)` and + `research-phase.md:85` agree with V10.3's step 3: committed, credentials as `${VAR}` references. +- **V8 — the formatter-scope surfaces.** `format-on-edit.sh`'s `.claude/workflows/` skip floor and the bootstrap + checklist's formatter-scope row must land before V10.1, whose bullet names them as the rule's other locations. + V8 hands `#71/body/1`'s § Syncing clause to V10.1 and does not write it. +- **V1.** `run-verification-block-fixture.sh` must keep running synthetic blocks: V10's kit-tree checks read + `README.md`, `CHANGELOG.md`, `CLAUDE.md` and `LICENSE`, which a throwaway checkout without + `docs/cbk/scaffold.md` would not carry. +- **Master plan, Task F2.** If a finding applied after V10.2 changes an always-loaded rule, re-run the runner and + update the v1.0.0 always-loaded figure in `CHANGELOG.md` in that commit. +- **Master plan, Task F4 — the `[1.0.0]` heading's date and PR number.** Both are predictions V10.2 wrote: the + execution day's UTC date and the next free number. F4 owns them. + - When `gh pr create` returns, confirm its number equals the `(PR #75)` in `CHANGELOG.md`'s `[1.0.0]` heading. + - On the day the operator merges, immediately before the squash-merge, re-date the heading to the merge's UTC + date, and correct the number if it differed, in one commit on the branch (no CI-skip marker, so the branch + still ends on a commit that runs CI): + ```bash + merged=$(date -u +%F); n=$(gh pr view --json number -q .number) + sed -i -e "s/^\(## \[1\.0\.0\] — \)[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\( — .*\)$/\1$merged\2/" \ + -e "s/^\(## \[1\.0\.0\] — .*\) (PR #[0-9]*)$/\1 (PR #$n)/" CHANGELOG.md + grep -c "^## \[1\.0\.0\] — $merged — harvest 5 (PR #$n)$" CHANGELOG.md + ``` + Expected: `1`. If the heading already reads that date and number, `git diff --quiet CHANGELOG.md` exits 0 and + there is nothing to commit. Otherwise commit `docs: date the v1.0.0 changelog heading to the merge`, run the + runner once more, and merge only on green. If the merge slips past UTC midnight, re-run the block above. + - After the push, run `gh api repos/j4th/context-builder-kit/license -q .license.spdx_id` and expect + `Apache-2.0` (V10.5). +- **Master plan, Task F5.** Step 5 runs `README.md`'s install block, verbatim, into a scratch directory against + the real `v1.0.0` tag, and checks that only the drop-in set lands and the printed `Kit commit:` sha equals + `git rev-parse --short=7 v1.0.0^{}`. This confirms against GitHub what V10.3's local test, which archives a commit, cannot: + that `git get-tar-commit-id` on GitHub's archive of an annotated tag returns the peeled commit. It did for + `anthropics/claude-code-action`'s annotated tag `v1.0.236` on 2026-09-30. The install strips the archive's top + directory whatever it is named. Also on release day, from V8: re-run + `curl -sL https://pypi.org/pypi/mcp-server-time/json | jq -r '.releases["2026.8.18"][0].upload_time'` and confirm + the `time` server's pin in `.mcp.json.example` is at least 7 days old. + +## Not holding at planning time + +- **Restamping the Projects v2 board contract as exercised** (a proposed fix in `review/consistency/1` and + `review/release/26`). The axis-level claim in `README.md:119` is stale and V10.3 restamps it, but the board + flag is still true. The one real `github-issues` run recorded `| **Project board** | **None.** Deliberate — + labels, native sub-issues and a saved filter instead |` in its `docs/cbk/scaffold.md` (`j4th/you-are-hear`, + `origin/main` at `1d4d52a`, read 2026-09-30), and the other two targets run Linear. The board stamp stays + designed-unexercised, re-dated by V9. Re-check at execution with + `git -C /home/j4th/Code/create/you-are-hear show origin/main:docs/cbk/scaffold.md | grep -n 'Project board'`. +- **A block check that every tracked top-level path is named in the README tree** (`review/consistency/2`'s + proposed check). Taken as written, it reds on the kit-only paths that are deliberately not installed. V10.3's + check covers what ships instead: every command, agent, hook, hook helper and rule. The README names the + kit-only paths in prose. +- **A Claude Code version floor stated as tested** (`review/release/25`). No floor is tested. The README states + the floor the documentation sets for the models the kit names (2.1.284), and the version v1.0.0 was checked + on, as its verifier advised. + +Every unverified finding in the pack (`review/release/61`, `/64`, `/65`) still held when checked against +`74edf84` while planning. Each is re-checked when its commit is made. diff --git a/docs/superpowers/plans/2026-09-30-harvest-5/V2-guards.md b/docs/superpowers/plans/2026-09-30-harvest-5/V2-guards.md new file mode 100644 index 0000000..a746ea3 --- /dev/null +++ b/docs/superpowers/plans/2026-09-30-harvest-5/V2-guards.md @@ -0,0 +1,2839 @@ +# Harvest 5 — V2: Guards and their CI backstops + +This cluster makes every guard the kit ships decide on what a tool call really touches, refuse input it cannot read, name the backstop that still stands when it fails open, and prove each branch by payload — and it makes the CI backstops behind those guards exist and fail closed. It closes #60 (the ADR guard's ten bypass spellings, the shared resolver `.claude/hooks/lib/resolve-path.sh`, the parse-nothing ADR job, the frozen-corpus recipe, the backstop-exists check), #61 (the ADR job's `mapfile` that read a git error as a pass), #62 (every fail-open warning names its backstop), #58's residue rows R1, R6, R8 and R13 and the § Hook authoring half of R11, and the whole-kit review's `review/security/19` (the two-dot ADR diff) and `review/security/20` (the unanchored main-branch and PR-state patterns); the trace rows are listed per task and in § Coverage. It implements D43, D44, D55 and D61, the backstop half of D51, and runs probes P1 and P4. It consumes V1: the runner `bash .claude/workflows/tests/run-verification-block.sh`, the two sentinels, and V1's `stop_hook_clean`, whose literal `WARNING` contract `hook-payloads-fixture.sh` pins on every could-not-look branch of the detector. It produces the shared `extract-run-block.sh` that V4's assert-step fixture sources, the `hook-guards-fixture.sh` SKIP line V5's knowledge-axis-`none` dry run expects, and the helper, slots and behaviour changes V3, V8 and V10 describe (§ Handed to other clusters). + +**How this file runs.** +- **Base.** V1 has landed. Every edit below anchors on text V1 does not change. New kit-sub-block checks go immediately before `echo "verification: kit sub-block complete"`; new project-sub-block checks go immediately before ` echo "verification: project sub-block complete"`. Because V1's lines run before them, each "run" step greps the runner's output for this task's lines and the sentinels rather than reading its tail. +- **Measured, not assumed.** Every red-first line below was produced on 2026-09-30 in a scratch clone of `643f7ff`, and the whole V2.1–V2.8 sequence was replayed there from these exact texts, with the block green after each commit. Line numbers are "at `74edf84`", for orientation only; if an anchor is missing, stop and reconcile. +- **Case counts.** `protected-paths-hook-fixture.sh` reports 48 cases on a host with `/proc` and `unshare -rm` (45 with its bind-mount SKIP line, 47 with its `/proc` SKIP line). `hook-guards-fixture.sh` reports 64 after V2.3 and 85 after V2.4 (three fewer with its knowledge-backend SKIP line). `hook-payloads-fixture.sh` reports 33 (32 with its SKIP line, when run as root). `adr-ci-body-fixture.sh` reports 13. +- **Modes.** `extract-run-block.sh` and `lib/resolve-path.sh` are sourced, never run: mode 644. Every fixture and hook is 755. `git add` records the mode; check it with `git ls-files -s `. +- **Scratch copies** are made with `S=$(mktemp -d) && cp -a . "$S/kit"` from the repository root and removed after; the real tree is changed only by the steps that say so. +- **Commits** use a quoted heredoc (`git commit -F - <<'MSG'`), so no backtick or `$` in a message is expanded. The branch is `feat/harvest-5-v1.0.0`, never `main`. +### Task V2.1: The ADR job parses no paths, diffs from the merge base and fails closed (#60/c5876324022/adr-job, #60/c5881158391/port-glob-body, #60/c5892401033/adr-job-glob-fixture, #60/c5901494943/3, #61/body/fix, #61/body/grep, review/security/19, #67/c5892401572/2 (the extractor); D55, D44) + +**Files:** +- Create: `.claude/workflows/tests/extract-run-block.sh` (mode 644) +- Create: `.claude/workflows/tests/adr-ci-body-fixture.sh` (mode 755) +- Modify: `.github/workflows/adr-immutability-check.yml` (whole file; the job name `ADR immutability` and the step name `Detect modified or deleted ADR files` are kept) +- Modify: `.claude/rules/cbk-conventions-reference.md` — § Verification, the kit sentinel + +**Interfaces:** +- Produces: `extract_run_block `, defined by sourcing `.claude/workflows/tests/extract-run-block.sh`. It prints the step's `run: |` body, dedented, or nothing when the step or its `run:` is absent. V4's `review-assert-fixture.sh` sources this file as it is; V4 does not fork or edit it. +- Produces: the step name `Detect modified or deleted ADR files` as the fixture's key. Renaming the step turns the fixture red with `FAIL: adr-immutability-check.yml has no 'Detect modified or deleted ADR files' step with a run: body`. +- Produces: block pins on `:(glob)docs/adr/`, `--diff-filter=a`, `--no-renames` and `"$BASE_SHA...$HEAD_SHA"` in the step's commands (its comments stripped: the job's comments name every flag, so a whole-file `grep -F` passes with the flag deleted), on the whole lines ` shell: bash`, ` runs-on: ubuntu-24.04` and ` filter: blob:none`, and the absence of `mapfile` fed by a process substitution under `.github/workflows` and `.claude/skills/blueprint/references/templates` (V4's template edits must keep it absent). +- Produces: `.github/workflows/adr-immutability-check.yml` as the named backstop that V2.2's hook messages and V2.6's mutation row cite. +- Decides V1's hand-off item 2 (D52 for the ADR job): the job's `shell: bash` and `runs-on: ubuntu-24.04` pins are **unconditional**, not kit-tree-only like V1.7's `verify.yml` line. The ADR job ships in the drop-in set and is the named backstop of the ADR guard, so a target keeps the pins, or narrows the check in its own copy with a comment (a self-hosted runner). V1.7's line stays `verify.yml`'s own; this task adds a separate check rather than extending V1.7's file list. + +- [ ] **Step 1: Write the shared extractor and the failing fixture** + +Create `.claude/workflows/tests/extract-run-block.sh`: +```bash +# shellcheck shell=bash +# Sourced by every fixture that runs a workflow step's own body, so the fixture runs the exact bytes CI runs +# rather than a copy that drifts. Defines one function and runs nothing on source; never reads stdin. +# +# extract_run_block +# +# Prints the step's `run: |` body, dedented. The step is found by its `- name:` value compared as a plain +# string (a name may hold regex metacharacters). The body starts after the step's `run: |` line and ends at +# the first non-blank line indented less than the body's first line — YAML's literal-block rule. Prints +# nothing when the step or its `run:` is absent; a caller treats an empty body as a failure, never as a pass. +# Needs awk only (POSIX: no gawk extension is used). +extract_run_block() { + awk -v want="$2" ' + function ind(s) { match(s, /^ */); return RLENGTH } + !found { + line = $0; sub(/^ *- name: /, "", line) + if ($0 ~ /^ *- name: / && line == want) { found = 1; step = ind($0) } + next + } + found && !inrun { + if ($0 ~ /^ *- / && ind($0) <= step) exit # the next step began: this one has no run: block + if ($0 ~ /^ *run: \|/) inrun = 1 + next + } + inrun { + if ($0 ~ /^ *$/) { blank++; next } + if (!body) body = ind($0) + if (ind($0) < body) exit + while (blank > 0) { print ""; blank-- } + print substr($0, body + 1) + } + ' "$1" +} +``` + +Create `.claude/workflows/tests/adr-ci-body-fixture.sh`: +```bash +#!/usr/bin/env bash +# Fixture for .github/workflows/adr-immutability-check.yml: runs the job's own `run:` body — extracted from the +# workflow by extract-run-block.sh, never a copy — against a throwaway repository, one head commit per case off a +# shared base (context-builder-kit#60, context-builder-kit#61). +# exit 1 — an existing numbered ADR modified (an ASCII, a non-ASCII and a spaced name), deleted, renamed, or +# given a mode change; a base SHA git cannot read; two commits with no merge base. A git error is +# never "no ADR changed". +# exit 0 — a new ADR; an edit to README.md or corrections.md; a nested docs/adr/sub/0005-x.md (not a numbered +# ADR); and a branch that touches no ADR while the base branch gained one after the branch point (the +# diff runs from the merge base, so the newer ADR is not read as deleted). +# The awk body this replaced read a name git prints quoted (non-ASCII) and a name awk splits (spaced) as "no ADR +# changed". Needs bash, git and awk; runs under mktemp, never in the checkout. Run by the verification block; also: +# bash .claude/workflows/tests/adr-ci-body-fixture.sh +set -euo pipefail +here=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$here/../../.." && pwd) +# shellcheck source=extract-run-block.sh +. "$here/extract-run-block.sh" +body=$(extract_run_block "$root/.github/workflows/adr-immutability-check.yml" "Detect modified or deleted ADR files") +[ -n "$body" ] || { echo "FAIL: adr-immutability-check.yml has no 'Detect modified or deleted ADR files' step with a run: body"; exit 1; } +command -v git >/dev/null || { echo "adr-ci-body-fixture: needs git"; exit 1; } +t=$(mktemp -d) +trap 'rm -rf "$t"' EXIT +printf '%s\n' "$body" > "$t/step.sh" +repo="$t/repo" +git -c init.defaultBranch=main init -q "$repo" +g() { git -C "$repo" -c user.email=f@x -c user.name=f -c commit.gpgsign=false "$@"; } +mkdir -p "$repo/docs/adr" +for f in 0001-plain.md "0002-café.md" "0003-two words.md" 0004-delete.md 0005-rename.md 0006-mode.md README.md corrections.md; do + printf '# %s\n' "$f" > "$repo/docs/adr/$f" +done +g add -A; g commit -q -m base +base=$(g rev-parse HEAD) + +n=0 +step() { # step — runs the job body as CI does + local rc=0 + (cd "$repo" && env BASE_SHA="$3" HEAD_SHA="$4" bash "$t/step.sh") > "$t/out" 2>&1 || rc=$? + n=$((n + 1)) + [ "$rc" -eq "$1" ] || { echo "FAIL: $2 (want exit $1, got $rc)"; sed 's/^/ | /' "$t/out"; exit 1; } +} +case_() { # case_ — applied on a fresh detached head off the base + local want=$1 desc=$2; shift 2 + g checkout -q --detach "$base" + (cd "$repo" && eval "$*") + g add -A; g commit -q -m "$desc" + step "$want" "$desc" "$base" "$(g rev-parse HEAD)" +} +case_ 1 "modify an ASCII-named ADR" "printf 'x\n' >> docs/adr/0001-plain.md" +case_ 1 "modify a non-ASCII-named ADR" "printf 'x\n' >> 'docs/adr/0002-café.md'" +case_ 1 "modify a spaced-name ADR" "printf 'x\n' >> 'docs/adr/0003-two words.md'" +case_ 1 "delete an ADR" "git rm -q docs/adr/0004-delete.md" +case_ 1 "rename an ADR" "git mv docs/adr/0005-rename.md docs/adr/0005-renamed.md" +case_ 1 "change an ADR's mode" "chmod +x docs/adr/0006-mode.md" +case_ 0 "add a new ADR" "printf '# new\n' > docs/adr/0007-new.md" +case_ 0 "edit the README index" "printf 'x\n' >> docs/adr/README.md" +case_ 0 "append to the corrections register" "printf 'x\n' >> docs/adr/corrections.md" +case_ 0 "add a nested non-ADR file" "mkdir -p docs/adr/sub && printf 'x\n' > docs/adr/sub/0005-x.md" + +# The base branch moved on: an ADR merged there after this branch was cut is not this PR's deletion. +g checkout -q --detach "$base"; printf '# later\n' > "$repo/docs/adr/0008-later.md"; g add -A; g commit -q -m later +moved=$(g rev-parse HEAD) +g checkout -q --detach "$base"; printf 'y\n' >> "$repo/docs/adr/README.md"; g add -A; g commit -q -m behind +step 0 "a branch behind a base that gained an ADR after the branch point" "$moved" "$(g rev-parse HEAD)" + +# Fail closed: a git error is never "no ADR changed" (context-builder-kit#61). +step 1 "a base SHA git cannot read fails closed" 0000000000000000000000000000000000000bad "$(g rev-parse HEAD)" +g checkout -q --orphan unrelated; g rm -rq --cached . >/dev/null; printf 'z\n' > "$repo/z.md"; g add z.md; g commit -q -m unrelated +step 1 "two commits with no merge base fail closed" "$base" "$(g rev-parse HEAD)" +echo "adr-ci-body-fixture: $n cases ok" +``` + +Run: `chmod 644 .claude/workflows/tests/extract-run-block.sh && chmod 755 .claude/workflows/tests/adr-ci-body-fixture.sh` + +- [ ] **Step 2: Run the fixture against the unfixed job, and show the two other defects** + +Run: `bash .claude/workflows/tests/adr-ci-body-fixture.sh; echo "exit=$?"` +Expected: +```text +FAIL: modify a non-ASCII-named ADR (want exit 1, got 0) + | No numbered ADR files changed in this PR. +exit=1 +``` +Record the FAIL line in the PR body's red-first table. + +The fixture stops at its first failure, so show the stale-base defect (review/security/19) and the fail-open (#61) against the unfixed body directly. Run from the repository root: +```bash +S=$(mktemp -d) +. .claude/workflows/tests/extract-run-block.sh +extract_run_block .github/workflows/adr-immutability-check.yml "Detect modified or deleted ADR files" > "$S/old.sh" +g() { git -C "$S/r" -c user.email=x@x -c user.name=x -c commit.gpgsign=false "$@"; } +git -c init.defaultBranch=main init -q "$S/r"; mkdir -p "$S/r/docs/adr"; echo a > "$S/r/docs/adr/0001-a.md" +g add -A; g commit -qm base; b=$(g rev-parse HEAD) +echo later > "$S/r/docs/adr/0002-later.md"; g add -A; g commit -qm later; m=$(g rev-parse HEAD) +g checkout -q --detach "$b"; echo y > "$S/r/notes.md"; g add -A; g commit -qm behind; h=$(g rev-parse HEAD) +(cd "$S/r" && BASE_SHA=$m HEAD_SHA=$h bash "$S/old.sh" >/dev/null 2>&1); echo "stale base: exit=$? (the unfixed body fails a PR that touched no ADR)" +rc=0; out=$(cd "$S/r" && BASE_SHA=0000000000000000000000000000000000000bad HEAD_SHA=$h bash "$S/old.sh" 2>/dev/null) || rc=$?; printf '%s\n' "$out"; echo "bad base SHA: exit=$rc (the unfixed body passes on a git error)" +rm -rf "$S" +``` +Expected: +```text +stale base: exit=1 (the unfixed body fails a PR that touched no ADR) +No numbered ADR files changed in this PR. +bad base SHA: exit=0 (the unfixed body passes on a git error) +``` +Record both lines in the red-first table. #61/body/grep needs no change of its own: record in the PR body that a repository-wide grep for `mapfile`, `readarray` and `< <(` over `*.yml`, `*.yaml`, `*.sh` and `*.md` (excluding `docs/superpowers/`) finds the ADR job as the only live `mapfile` of a process substitution under `set -e`, and that the block now pins its absence. + +- [ ] **Step 3: Verify the platform facts, rewrite the job, add the block checks** + +The new job comments state three facts. Re-read each raw today: + +Run: `curl -sL 'https://docs.github.com/api/article/body?pathname=/en/actions/reference/workflows-and-actions/workflow-syntax' | grep -F -e 'bash --noprofile --norc -eo pipefail {0}' -e 'bash -e {0}' | head -2 | cut -c1-80` +Expected: two table rows, one for `unspecified` ending in `bash -e {0}` and one for `bash` ending in `bash --noprofile --norc -eo pipefail {0}`. If either is missing, stop: the `defaults:` comment cannot be written as drafted. + +Run: `curl -sL https://raw.githubusercontent.com/actions/checkout/11d5960a326750d5838078e36cf38b85af677262/action.yml | grep -A2 '^ filter:'` +Expected: ` filter:` then ` description: >` then ` Partially clone against a given filter.` + +Run (the blobless measurement the comment dates): +```bash +S=$(mktemp -d) && g() { git -C "$S/src" -c user.email=a@b -c user.name=a -c commit.gpgsign=false "$@"; } \ +&& git -c init.defaultBranch=main init -q "$S/src" && mkdir -p "$S/src/docs/adr" \ +&& for i in 1 2 3; do echo "# $i" > "$S/src/docs/adr/000$i-a.md"; done && g add -A && g commit -qm base && b=$(g rev-parse HEAD) \ +&& echo x >> "$S/src/docs/adr/0001-a.md" && echo n > "$S/src/docs/adr/0009-n.md" && g add -A && g commit -qm head && h=$(g rev-parse HEAD) \ +&& g config uploadpack.allowFilter true && git clone -q --no-checkout --filter=blob:none "file://$S/src" "$S/c" \ +&& git -C "$S/c" rev-list --objects --missing=print --all | grep -c '^?' \ +&& git -C "$S/c" diff --no-renames --diff-filter=a --name-status "$b...$h" -- ':(glob)docs/adr/[0-9][0-9][0-9][0-9]-*.md' \ +&& git -C "$S/c" rev-list --objects --missing=print --all | grep -c '^?'; rm -rf "$S" +``` +Expected: the same missing-object count before and after (it was `5`, then `M docs/adr/0001-a.md`, then `5`). If the second count is larger, stop: the diff fetched blobs, and `filter: blob:none` must not ship. + +Replace the whole of `.github/workflows/adr-immutability-check.yml` with: +```yaml +name: ADR immutability check + +# Closes the raw-git-access gap that the PreToolUse hook can't catch. +# Per ADR-0000, ADRs are immutable once accepted — revisions go via new ADRs +# that supersede the old. This workflow fails the PR if any existing +# `docs/adr/[0-9]{4}-*.md` file has been modified (additions of new ADR +# files are allowed; deletions and modifications are not). + +# No `paths:` filter, on purpose: a required check that is skipped by a path +# filter never reports and parks the PR forever (cbk-conventions-reference.md +# § Required-checks trap, cause 2). The job exits 0 in seconds when no +# ADR changed, so always-run costs nothing. +on: + pull_request: + branches: [main] + +# `shell: bash` runs every step as `bash --noprofile --norc -eo pipefail {0}`; +# an unspecified shell runs `bash -e {0}`, without pipefail +# (https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax, +# read 2026-09-30). +defaults: + run: + shell: bash + +jobs: + immutability: + # The check-run context a ruleset requires. Pinned before promotion and never + # renamed afterwards — matching is by name (the trap's cause 3). + name: ADR immutability + # A named image, not a moving label: the runner changes only by a reviewed edit. + runs-on: ubuntu-24.04 + steps: + - name: Checkout PR branch + # SHA-pinned per cbk-conventions.md § Dependency settle-window: tag refs + # are mutable and open to tag-retag compromise. + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + # The full commit graph (the diff needs the merge base) and no blobs: + # with --no-renames, `git diff --name-status` compares tree entries, so + # no ADR blob is fetched (a blobless clone's missing-object count was + # unchanged across the diff, measured 2026-09-30). `filter` is an input + # of the pinned action (its action.yml at this SHA, read 2026-09-30). + # The property rests on --no-renames staying in the body below; the + # verification block pins the two together. + fetch-depth: 0 + filter: blob:none + + - name: Detect modified or deleted ADR files + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + + # Every change to an existing numbered ADR — docs/adr/NNNN-*.md directly under docs/adr/ — + # fails the check; an added ADR passes, and README.md, template.md, corrections.md and nested + # files are exempt. The body parses no paths: a `:(glob)` pathspec selects the numbered ADRs + # (`*` does not cross `/`), `--diff-filter=a` drops additions, `--no-renames` turns a rename + # into the deletion it is, and any output fails. The awk path match this replaced missed a + # name git prints quoted (a non-ASCII one) and a name awk splits (a spaced one); both read + # as "no ADR changed" (context-builder-kit#60). + # Three dots: the diff runs from the merge base of the two SHAs, so an ADR merged to the base + # branch after this branch was cut is not read as deleted here. + # Fail closed: command substitution hands `git diff`'s status to the `||`, so a git error + # (an unreadable SHA, no merge base) is never "no ADR changed" (context-builder-kit#61). + # This body runs under .claude/workflows/tests/adr-ci-body-fixture.sh. + changes="$(git diff --no-renames --diff-filter=a --name-status "$BASE_SHA...$HEAD_SHA" -- ':(glob)docs/adr/[0-9][0-9][0-9][0-9]-*.md')" \ + || { echo "::error::git diff $BASE_SHA...$HEAD_SHA failed; not reporting a pass on no data."; exit 1; } + + if [ -z "$changes" ]; then + echo "No existing numbered ADR changed in this PR." + exit 0 + fi + + echo "::error::ADR immutability violation: existing ADRs cannot be modified, deleted, renamed or re-moded." + echo "Per docs/adr/0000-record-architecture-decisions.md, ADRs are immutable once accepted." + echo "To revise a decision, write a new ADR that supersedes the old one; a wrong claim goes to docs/adr/corrections.md." + echo "" + echo "Forbidden changes:" + printf '%s\n' "$changes" + exit 1 +``` + +Edit `.claude/rules/cbk-conventions-reference.md` — in § Verification, replace the kit sentinel line (exact; it occurs once): +```bash +echo "verification: kit sub-block complete" +``` +with (the new checks sit immediately before it): +```bash +# The ADR-immutability job's own run: body on a throwaway repository (context-builder-kit#60, +# context-builder-kit#61): a modified ADR with an ASCII, non-ASCII or spaced name, a deletion, a rename and a +# mode change fail it; a new ADR, the README, the corrections register and a nested non-ADR pass; a branch +# behind a base that gained an ADR passes (the diff runs from the merge base); an unreadable base SHA and no +# merge base fail closed. Needs git. +bash .claude/workflows/tests/adr-ci-body-fixture.sh || { echo "adr-immutability-check.yml's body regressed on its fixture"; exit 1; } +# The job's shape the fixture cannot see: the parse-nothing pathspec and the merge-base diff, a blobless checkout +# that stays blobless only while --no-renames holds, bash with pipefail, a named runner image; and no workflow or +# workflow template reads a process substitution into mapfile, whose failure `set -e` never sees (context-builder-kit#61). +# The flags are read from the step's commands with its comments stripped, and the keys as whole lines, because the +# job's own comments name every one of them. Unconditional: the job ships in the drop-in set, so a target keeps the +# pins, or narrows this check in its own copy with a comment saying why (a self-hosted runner, say). +. .claude/workflows/tests/extract-run-block.sh +adrcmd=$(extract_run_block .github/workflows/adr-immutability-check.yml "Detect modified or deleted ADR files" | grep -v '^[[:space:]]*#' || true) +for w in ":(glob)docs/adr/" "--diff-filter=a" "--no-renames" '"$BASE_SHA...$HEAD_SHA"'; do grep -qF -- "$w" <<<"$adrcmd" || { echo "adr-immutability-check.yml's run: body lacks: $w"; exit 1; }; done +for l in " shell: bash" " runs-on: ubuntu-24.04" " filter: blob:none"; do grep -qxF -- "$l" .github/workflows/adr-immutability-check.yml || { echo "adr-immutability-check.yml lacks the line:$l"; exit 1; }; done +absent grep -rnE 'mapfile[^<]*<[[:space:]]*<\(' .github/workflows .claude/skills/blueprint/references/templates +echo "verification: kit sub-block complete" +``` + +- [ ] **Step 4: Run the fixture and the block** + +Run: `bash .claude/workflows/tests/adr-ci-body-fixture.sh; echo "exit=$?"` +Expected: `adr-ci-body-fixture: 13 cases ok`, `exit=0`. + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | grep -E 'adr-ci-body-fixture|^verification: (kit sub-block complete|done)$|^verification: block exited'; echo "exit=${PIPESTATUS[0]}"` +Expected: `adr-ci-body-fixture: 13 cases ok`, `verification: kit sub-block complete`, `verification: done`, `exit=0`. + +Run (the shape pins bite on the flag or key, not on the comments that name it — in a scratch copy): +```bash +S=$(mktemp -d) && cp -a . "$S/kit" && cd "$S/kit" && w=.github/workflows/adr-immutability-check.yml && cp "$w" "$S/w" \ +&& for m in 's/git diff --no-renames --diff/git diff --diff/' '/^defaults:$/,/^ shell: bash$/d' 's/^ filter: blob:none$/ # dropped/'; do + cp "$S/w" "$w"; sed -i "$m" "$w"; bash .claude/workflows/tests/run-verification-block.sh 2>&1 | grep -E "adr-immutability-check.yml('s run: body)? lacks"; done; cd - >/dev/null; rm -rf "$S" +``` +Expected, one line per mutant: `adr-immutability-check.yml's run: body lacks: --no-renames`, `adr-immutability-check.yml lacks the line: shell: bash`, `adr-immutability-check.yml lacks the line: filter: blob:none`. (Deleting `--diff-filter=a` is caught by the fixture itself: `FAIL: add a new ADR (want exit 0, got 1)`.) + +- [ ] **Step 5: Commit** + +```bash +git add .claude/workflows/tests/extract-run-block.sh .claude/workflows/tests/adr-ci-body-fixture.sh .github/workflows/adr-immutability-check.yml .claude/rules/cbk-conventions-reference.md +git commit -q -F - <<'MSG' +fix(V2): the ADR job parses no paths, diffs from the merge base and fails closed + +The job's body is a :(glob) pathspec with --diff-filter=a and --no-renames, and +any output fails it: the awk match it replaces read a quoted non-ASCII name and a +spaced name as "no ADR changed". The diff is three-dot, so an ADR merged to the +base after the branch point no longer reads as a deletion. A git error now fails +the step, where mapfile of a process substitution read it as a pass. The checkout +is blobless; the job runs under shell: bash on a named runner image. +adr-ci-body-fixture.sh runs the job's own body, extracted by the shared +extract-run-block.sh; the block runs the fixture and pins what it cannot see. + +Red first: FAIL: modify a non-ASCII-named ADR (want exit 1, got 0) + +Trace: #60/c5876324022/adr-job, #60/c5881158391/port-glob-body, +#60/c5892401033/adr-job-glob-fixture, #60/c5901494943/3, #61/body/fix, +#61/body/grep, review/security/19; the extractor for #67/c5892401572/2. +Decisions: D55, D44. + +Co-Authored-By: Claude Opus 5.5 +MSG +``` +### Task V2.2: The ADR guard resolves the path it judges, through a shared helper (#60/body/fix, #60/body/alt, #60/body/fixture, #60/c5876324022/table, #60/c5876324022/closed-1, #60/c5876324022/closed-2, #60/c5876324022/closed-3, #60/c5876324022/closed-4, #60/c5876324022/closed-5, #60/c5876324022/closed-6, #60/c5876324022/closed-note, #60/c5881158391/port-helper, #60/c5892401033/helper, #60/c5892401033/adr-path-independent, #60/c5892401033/linked-worktrees, #60/c5892401033/ef-inode, #60/c5892401033/gitignore-trap, #60/c5892401033/fixture, #60/c5901494943/2a, #60/c5901494943/2b, #60/c5901494943/2c, #60/c5901494943/2d, #60/c5901494943/2e, #62/body/extra-adr-hook, #58/c5901493591/R1 (the path guard), release/5 (§ Hook authoring), #69/c5859756889/apply-h4/1, review/consistency/48; D43, D44, D61) + +**Files:** +- Create: `.claude/hooks/lib/resolve-path.sh` (mode 644) +- Create: `.claude/workflows/tests/protected-paths-hook-fixture.sh` (mode 755) +- Modify: `.claude/hooks/protect-immutable-adrs.sh` (whole file) +- Modify: `.claude/workflows/tests/hook-contract-fixture.sh` (header lines 2–5, the early-reader loop at line 51) +- Modify: `.claude/rules/cbk-conventions-reference.md` — § Hook authoring (the opening paragraph, the Header bullet, the stdin bullet's citation, the Project-relative paths bullet, a new bullet before Four tiers, the Verify by payload citation) and § Verification (the kit sentinel) + +**Interfaces:** +- Produces: `.claude/hooks/lib/resolve-path.sh`, functions only: `rp_payload ` (sets `RP_TOOL`, `RP_FILE`, `RP_CWD`; returns 1 on a payload jq cannot read), `rp_lexnorm ` (`RP_LEX`), `rp_canon ` (`RP_CANON`; flags `RP_THROUGH_PROC`, `RP_UNREADABLE`), `rp_target ` (`RP_T_LEXICAL`, `RP_T_PHYSICAL`), `rp_roots ` (`RP_ROOTS`), `rp_linked_worktree ` (`RP_WT`), `rp_nearest_dir ` (`RP_DIR`), `rp_same_entry []` (`RP_HIT`). A target's corpus guard is the second caller; the kit ships none. +- Produces: the ADR guard's fail-open messages, pinned by the fixture: the missing-helper warning names `helper`, `Backstop` and `adr-immutability-check.yml`; the jq warning names `jq not installed` and `Backstop`; every refusal still names `docs/adr/corrections.md` (the block's existing check). +- Produces: the § Hook authoring bullet that begins `- **A sourced helper, and a target's own hooks, keep the same contract.**` — the one statement of the sourced-helper contract. It names the `!/.claude/hooks/lib/` trap and cites § .gitignore anchoring, where V8.4 lands the negation bullet (`**A negation keeps the hook helpers tracked.**`) with its sources; the section exists at `74edf84`, so the citation holds between the two commits. +- Produces: the block check `absent git check-ignore -q --no-index .claude/hooks/lib/resolve-path.sh`. +- Consumes: `.github/workflows/adr-immutability-check.yml` (V2.1) as the backstop the messages name. + +- [ ] **Step 1: Write the failing fixture** + +Create `.claude/workflows/tests/protected-paths-hook-fixture.sh`: +```bash +#!/usr/bin/env bash +# Fixture for the path guard that sources .claude/hooks/lib/resolve-path.sh — protect-immutable-adrs.sh, which +# denies an existing docs/adr/NNNN-*.md in ANY checkout — and for the helper's root-scoped functions, driven through +# a corpus guard the fixture writes for itself (the kit ships no corpus guard; a target with a frozen corpus writes +# its own on this helper). Deny cases are the spellings context-builder-kit#60 measured against the ADR hook: a `..` +# segment, `.` and a doubled slash, a trailing-slash project dir, a relative path from a subdirectory, a symlinked +# directory, a symlink to the file, a hardlink, a path through /proc, an unparseable payload (a lone UTF-16 +# surrogate), and a project dir pointing at another checkout — plus the control, a project root containing a space, +# and a hardlink inside a linked worktree. Allow cases are the neighbours a looser match catches. The hooks run from +# a copy of the checkout's layout (.claude/hooks + lib) inside throwaway `git init` trees; the real checkout is never +# touched and the launch directory never matters. Only exit 2 denies, so every case asserts the exact exit. The +# guard's fail-open branches — no lib/ helper, and no jq on PATH — exit 0 and name the surviving backstop. +# Needs bash, git, jq, readlink (GNU or BSD). Four cases need more and print a SKIP line where the host lacks it: +# the /proc case needs /proc/self/root, and the three bind-mount cases need `unshare -rm` (an unprivileged user and +# mount namespace). Run by the verification block; also: bash .claude/workflows/tests/protected-paths-hook-fixture.sh +set -euo pipefail +here=$(cd "$(dirname "$0")" && pwd) +src="$here/../../hooks" +ADR=protect-immutable-adrs.sh; CORPUS=corpus-double.sh +[ -f "$src/$ADR" ] || { echo "protected-paths-hook-fixture: $src/$ADR is missing"; exit 1; } +command -v jq >/dev/null && command -v git >/dev/null || { echo "protected-paths-hook-fixture: needs jq and git"; exit 1; } +t=$(mktemp -d) +cleanup() { chmod -R u+rwX "$t" 2>/dev/null || true; rm -rf "$t"; } +trap cleanup EXIT + +install_hooks() { # install_hooks : the guard where settings.json registers it, lib beside it + mkdir -p "$1/.claude/hooks" + cp "$src/$ADR" "$1/.claude/hooks/" + if [ -d "$src/lib" ]; then cp -R "$src/lib" "$1/.claude/hooks/"; fi # absent, the deny cases below go red + cat > "$1/.claude/hooks/$CORPUS" <<'EOF' +#!/usr/bin/env bash +# The fixture's root-scoped guard over docs/corpus/ — anything under it, additions too, in the hook's own checkout, +# the project dir's, or a linked worktree of either — in the shape a target's frozen-corpus guard takes. +set -uo pipefail +input="$(cat)" +here=${BASH_SOURCE[0]%/*} +. "$here/lib/resolve-path.sh" +deny() { echo "BLOCKED: $1" >&2; exit 2; } +rp_payload "$input" || deny "the payload could not be read" +case "$RP_TOOL" in Edit|Write|MultiEdit) ;; *) exit 0 ;; esac +[ -n "$RP_FILE" ] || exit 0 +rp_target "$RP_FILE" "$RP_CWD" +[ -z "$RP_THROUGH_PROC" ] || deny "a path through /proc" +[ -z "$RP_UNREADABLE" ] || deny "an unreadable symlink" +rp_roots "${BASH_SOURCE[0]}" +roots=("${RP_ROOTS[@]}") +for t in "$RP_T_LEXICAL" "$RP_T_PHYSICAL"; do rp_linked_worktree "$t"; [ -z "$RP_WT" ] || roots+=("$RP_WT"); done +for root in "${roots[@]}"; do + c="${root%/}/docs/corpus" + for t in "$RP_T_LEXICAL" "$RP_T_PHYSICAL"; do case "$t" in "$c"|"$c/"*) deny "$t is in the corpus" ;; esac; done +done +rp_nearest_dir "$RP_T_PHYSICAL" +file=''; [ -f "$RP_T_PHYSICAL" ] && file=$RP_T_PHYSICAL +for root in "${roots[@]}"; do + c="${root%/}/docs/corpus"; [ -d "$c" ] || continue + RP_HIT=''; if rp_same_entry "$c" "$RP_DIR" "$file"; then deny "$RP_FILE is $RP_HIT by another name"; fi +done +exit 0 +EOF + chmod +x "$1/.claude/hooks/$ADR" "$1/.claude/hooks/$CORPUS" +} +make_root() { # make_root : a committed checkout shaped like a target + mkdir -p "$1/docs/adr/sub" "$1/docs/adr-old" "$1/docs/corpus" "$1/docs/sub" + for f in 0001-x.md README.md template.md corrections.md sub/0001-x.md; do printf '# %s\n' "$f" > "$1/docs/adr/$f"; done + printf 'old\n' > "$1/docs/adr-old/0001-x.md" + printf 'frozen\n' > "$1/docs/corpus/00-overview.md" + printf 'notes\n' > "$1/docs/corpus-notes.md" + git -c init.defaultBranch=main init -q "$1" + git -C "$1" add -A; git -C "$1" -c user.email=f@x -c user.name=f -c commit.gpgsign=false commit -q -m base + install_hooks "$1" +} +root="$t/repo"; other="$t/other"; spaced="$t/a b/repo" +make_root "$root"; make_root "$other"; make_root "$spaced" +git -C "$root" worktree add -q --detach "$root/.claude/worktrees/wt" HEAD +git -C "$root" worktree add -q --detach "$t/wt-outside" HEAD +ln -s docs/adr "$root/adr-link"; ln -s docs/adr/0001-x.md "$root/adr-file-link.md" +ln -s docs/corpus "$root/corpus-link"; ln -s docs/corpus/00-overview.md "$root/corpus-file-link.md" +ln "$root/docs/adr/0001-x.md" "$root/adr-hard.md"; ln "$root/docs/corpus/00-overview.md" "$root/corpus-hard.md" +ln "$t/wt-outside/docs/adr/0001-x.md" "$t/wt-outside/notes-hard.md" +ln -s loop-b "$root/loop-a"; ln -s loop-a "$root/loop-b" +A="$root/docs/adr/0001-x.md"; C="$root/docs/corpus/00-overview.md" + +n=0 +fail() { echo "FAIL [$1]: $2"; sed 's/^/ stderr: /' "$t/err"; exit 1; } +# probe +probe() { + local want=$1 desc=$2 hook=$3 hroot=$4 cwd=$5 pd=$6 tool=$7 fp=$8 rc=0 payload + payload=$(jq -cn --arg tool "$tool" --arg fp "$fp" --arg cwd "$cwd" '{tool_name: $tool, tool_input: {file_path: $fp}, cwd: $cwd}') + if [ "$pd" = - ]; then + (cd "$cwd" && printf '%s' "$payload" | env -u CLAUDE_PROJECT_DIR "$hroot/.claude/hooks/$hook") >/dev/null 2>"$t/err" || rc=$? + else + (cd "$cwd" && printf '%s' "$payload" | env CLAUDE_PROJECT_DIR="$pd" "$hroot/.claude/hooks/$hook") >/dev/null 2>"$t/err" || rc=$? + fi + n=$((n + 1)) + [ "$rc" -eq "$want" ] || fail "$hook" "$desc (want exit $want, got $rc)" +} +raw() { # raw : the payload byte for byte + local rc=0 + (cd "$root" && printf '%s' "$4" | env CLAUDE_PROJECT_DIR="$root" "$root/.claude/hooks/$3") >/dev/null 2>"$t/err" || rc=$? + n=$((n + 1)) + [ "$rc" -eq "$1" ] || fail "$3" "$2 (want exit $1, got $rc)" +} + +# ── protect-immutable-adrs.sh: deny every spelling of an existing numbered ADR ── +probe 2 "control: the absolute path" $ADR "$root" "$root" "$root" Edit "$A" +probe 2 "Write to an existing ADR" $ADR "$root" "$root" "$root" Write "$A" +probe 2 "MultiEdit on an existing ADR" $ADR "$root" "$root" "$root" MultiEdit "$A" +probe 2 "a .. segment (docs/sub/../adr/)" $ADR "$root" "$root" "$root" Edit "$root/docs/sub/../adr/0001-x.md" +probe 2 "a . segment and a doubled slash" $ADR "$root" "$root" "$root" Edit "$root/docs/./adr//0001-x.md" +probe 2 "CLAUDE_PROJECT_DIR with a trailing slash" $ADR "$root" "$root" "$root/" Edit "$A" +probe 2 "a relative path from a subdirectory" $ADR "$root" "$root/docs/sub" "$root" Edit "../adr/0001-x.md" +probe 2 "a symlinked directory into docs/adr" $ADR "$root" "$root" "$root" Edit "$root/adr-link/0001-x.md" +probe 2 "a symlink to the ADR file" $ADR "$root" "$root" "$root" Edit "$root/adr-file-link.md" +probe 2 "a hardlink to the ADR file" $ADR "$root" "$root" "$root" Edit "$root/adr-hard.md" +if [ -e /proc/self/root ]; then + probe 2 "a path through /proc/self/root" $ADR "$root" "$root" "$root" Edit "/proc/self/root$A" +else + echo "SKIP: a path through /proc/self/root (/proc unavailable)" +fi +raw 2 "a lone UTF-16 surrogate makes the payload unparseable, and that is refused" $ADR \ + "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$A\",\"content\":\"x\\ud800y\"},\"cwd\":\"$root\"}" +raw 2 "a file_path that is an object cannot be read, and is refused" $ADR '{"tool_name":"Edit","tool_input":{"file_path":{"x":1}}}' +probe 2 "CLAUDE_PROJECT_DIR set to another checkout" $ADR "$root" "$root" "$other" Edit "$A" +probe 2 "CLAUDE_PROJECT_DIR unset, launched from a subdirectory" $ADR "$root" "$root/docs/sub" - Edit "$A" +probe 2 "another checkout's existing ADR (every target's ADRs are immutable)" $ADR "$root" "$root" "$root" Edit "$other/docs/adr/0001-x.md" +probe 2 "an existing ADR in a linked worktree" $ADR "$root" "$root" "$root" Edit "$t/wt-outside/docs/adr/0001-x.md" +probe 2 "a hardlink inside a linked worktree to that worktree's ADR" $ADR "$root" "$root" "$root" Edit "$t/wt-outside/notes-hard.md" +probe 2 "a project root containing a space" $ADR "$spaced" "$spaced" "$spaced" Edit "$spaced/docs/adr/0001-x.md" +probe 2 "a relative path under a project root containing a space" $ADR "$spaced" "$spaced/docs/sub" "$spaced" Edit "../adr/0001-x.md" +rc=0; (cd "$root" && jq -cn --arg fp "$root/adr-hard.md" --arg cwd "$root" '{tool_name:"Edit",tool_input:{file_path:$fp},cwd:$cwd}' \ + | env -u CLAUDE_PROJECT_DIR .claude/hooks/$ADR) >/dev/null 2>"$t/err" || rc=$? +n=$((n + 1)); [ "$rc" -eq 2 ] || fail $ADR "a hardlink, with the hook launched by a relative path (want exit 2, got $rc)" +# ── protect-immutable-adrs.sh: allow the neighbours ── +probe 0 "a new ADR" $ADR "$root" "$root" "$root" Write "$root/docs/adr/0099-new.md" +probe 0 "the README index" $ADR "$root" "$root" "$root" Edit "$root/docs/adr/README.md" +probe 0 "the template" $ADR "$root" "$root" "$root" Edit "$root/docs/adr/template.md" +probe 0 "the corrections register" $ADR "$root" "$root" "$root" Edit "$root/docs/adr/corrections.md" +probe 0 "a sibling directory sharing the prefix (docs/adr-old/)" $ADR "$root" "$root" "$root" Edit "$root/docs/adr-old/0001-x.md" +probe 0 "a nested file under docs/adr/sub/" $ADR "$root" "$root" "$root" Edit "$root/docs/adr/sub/0001-x.md" +probe 0 "a symlink loop ends, and is not an ADR" $ADR "$root" "$root" "$root" Edit "$root/loop-a/x.md" +probe 0 "a Read of an ADR" $ADR "$root" "$root" "$root" Read "$A" +probe 0 "a payload with no file_path" $ADR "$root" "$root" "$root" Edit "" +probe 0 "a path read exactly: an ADR name plus a trailing newline is another file" $ADR "$root" "$root" "$root" Write "$A"$'\n' + +# ── the helper's root-scoped functions, through the fixture's corpus guard ── +probe 2 "corpus: control, the absolute path" $CORPUS "$root" "$root" "$root" Edit "$C" +probe 2 "corpus: a new file (additions are frozen too)" $CORPUS "$root" "$root" "$root" Write "$root/docs/corpus/09-new.md" +probe 2 "corpus: a .. segment" $CORPUS "$root" "$root" "$root" Edit "$root/docs/adr/../corpus/00-overview.md" +probe 2 "corpus: a symlinked directory" $CORPUS "$root" "$root" "$root" Write "$root/corpus-link/new.md" +probe 2 "corpus: a symlink to a corpus file" $CORPUS "$root" "$root" "$root" Edit "$root/corpus-file-link.md" +probe 2 "corpus: a hardlink to a corpus file" $CORPUS "$root" "$root" "$root" Edit "$root/corpus-hard.md" +probe 2 "corpus: a project dir pointing elsewhere (the hook's own checkout is guarded)" $CORPUS "$root" "$root" "$other" Edit "$C" +probe 2 "corpus: a linked worktree inside the checkout" $CORPUS "$root" "$root" "$root" Edit "$root/.claude/worktrees/wt/docs/corpus/00-overview.md" +probe 2 "corpus: a linked worktree outside the checkout" $CORPUS "$root" "$root" "$root" Write "$t/wt-outside/docs/corpus/new.md" +probe 0 "corpus: another repository's corpus is not this project's" $CORPUS "$root" "$root" "$root" Edit "$other/docs/corpus/00-overview.md" +probe 0 "corpus: a sibling that shares the name as a prefix" $CORPUS "$root" "$root" "$root" Edit "$root/docs/corpus-notes.md" +probe 0 "corpus: a symlink loop ends, and is not the corpus" $CORPUS "$root" "$root" "$root" Edit "$root/loop-a/x.md" + +# ── a bind mount of the guarded directory (same device and inode, another path) — needs a user and mount namespace ── +if unshare -rm true 2>/dev/null; then + mkdir -p "$t/alias-adr" "$t/alias-corpus" + bind() { # bind + local rc=0 + unshare -rm bash -c 'mount --bind "$1" "$2" && cd "$3" && jq -cn --arg fp "$4" --arg cwd "$3" "{tool_name:\"Write\",tool_input:{file_path:\$fp},cwd:\$cwd}" | CLAUDE_PROJECT_DIR="$3" "$3/.claude/hooks/$5"' \ + _ "$4" "$5" "$root" "$6" "$3" >/dev/null 2>"$t/err" || rc=$? + n=$((n + 1)) + [ "$rc" -eq "$1" ] || fail "$3" "$2 (want exit $1, got $rc)" + } + bind 2 "an existing ADR through a bind mount of docs/adr" $ADR "$root/docs/adr" "$t/alias-adr" "$t/alias-adr/0001-x.md" + bind 0 "a new ADR through a bind mount of docs/adr" $ADR "$root/docs/adr" "$t/alias-adr" "$t/alias-adr/0099-new.md" + bind 2 "corpus: a new file through a bind mount of the corpus" $CORPUS "$root/docs/corpus" "$t/alias-corpus" "$t/alias-corpus/new.md" +else + echo "SKIP: three bind-mount cases (unshare -rm unavailable: no unprivileged user and mount namespace)" +fi + +# ── a copy without its lib/ helper fails open, and says which backstop still stands ── +solo="$t/solo"; mkdir -p "$solo/.claude/hooks"; cp "$src/$ADR" "$solo/.claude/hooks/"; chmod +x "$solo/.claude/hooks/$ADR" +rc=0; (cd "$root" && jq -cn --arg fp "$A" '{tool_name:"Edit",tool_input:{file_path:$fp}}' | env CLAUDE_PROJECT_DIR="$root" "$solo/.claude/hooks/$ADR") >/dev/null 2>"$t/err" || rc=$? +n=$((n + 1)) +[ "$rc" -eq 0 ] || fail $ADR "a copy without lib/ must fail open (want exit 0, got $rc)" +{ grep -q 'helper' "$t/err" && grep -q 'Backstop' "$t/err" && grep -q 'adr-immutability-check.yml' "$t/err"; } || fail $ADR "the missing-helper warning must name the helper and the backstop's workflow" +# ── with no jq on PATH the guard fails open, and says which backstop still stands (a PATH holding only what the +# guard runs before its jq check) ── +nojq="$t/nojq"; mkdir -p "$nojq" +for tool in bash cat readlink; do p=$(type -P "$tool" || true); [ -n "$p" ] && ln -sf "$p" "$nojq/$tool"; done +rc=0; (cd "$root" && jq -cn --arg fp "$A" '{tool_name:"Edit",tool_input:{file_path:$fp}}' > "$t/pay-nojq" && env -i PATH="$nojq" HOME="$HOME" CLAUDE_PROJECT_DIR="$root" bash "$src/$ADR" < "$t/pay-nojq") >/dev/null 2>"$t/err" || rc=$? +n=$((n + 1)) +[ "$rc" -eq 0 ] || fail $ADR "with no jq the guard must fail open (want exit 0, got $rc)" +{ grep -q 'jq not installed' "$t/err" && grep -q 'Backstop' "$t/err"; } || fail $ADR "the no-jq warning must say jq is missing and name the backstop" +echo "protected-paths-hook-fixture: $n cases ok" +``` + +Run: `chmod 755 .claude/workflows/tests/protected-paths-hook-fixture.sh` + +- [ ] **Step 2: Run it against the unfixed hook, and show the tripwire's blind spot** + +Run: `bash .claude/workflows/tests/protected-paths-hook-fixture.sh; echo "exit=$?"` +Expected: +```text +FAIL [protect-immutable-adrs.sh]: a .. segment (docs/sub/../adr/) (want exit 2, got 0) +exit=1 +``` +(The control, Write and MultiEdit cases pass on the unfixed hook; the first spelling it misses is the `..` segment.) Record the FAIL line in the red-first table. + +Run (a pipeline that can exit early, planted in a helper, is invisible to the unfixed tripwire): +```bash +S=$(mktemp -d) && cp -a . "$S/kit" && mkdir -p "$S/kit/.claude/hooks/lib" \ +&& printf 'x() { printf "%%s" "$1" | grep -q y; }\n' > "$S/kit/.claude/hooks/lib/x.sh" \ +&& (cd "$S/kit" && bash .claude/workflows/tests/hook-contract-fixture.sh; echo "exit=$?"); rm -rf "$S" +``` +Expected: `hook-contract-fixture: ok`, `exit=0` — the miss. Record it beside the FAIL line. + +- [ ] **Step 3: Verify the platform fact, then land the helper, the hook, the tripwire and § Hook authoring** + +Run: `curl -sL https://code.claude.com/docs/en/hooks.md | grep -F 'Handlers run in the current directory' | cut -c1-70` +Expected: `Handlers run in the current directory with Claude Code's environment.` If absent, stop: the hook's `Path:` line quotes it. + +Create `.claude/hooks/lib/resolve-path.sh` (then `chmod 644 .claude/hooks/lib/resolve-path.sh`): +```bash +# shellcheck shell=bash +# Sourced by a path guard after it drains stdin — protect-immutable-adrs.sh in the kit, and a target's own guard +# over a frozen corpus (the consultation skill's references/frozen_corpus_ingestion.md § The enforcement set). +# Never run, never reads stdin, defines functions only: no top-level statement runs on source. It answers one +# question the guards share: where does an Edit/Write/MultiEdit's `file_path` really land? +# +# Why a resolver: a prefix or suffix match on the payload's path string is bypassed by every spelling that names +# the same file differently. context-builder-kit#60 measured ten against the ADR hook — a `..` segment, `.` and +# `//`, a trailing-slash CLAUDE_PROJECT_DIR, a relative path, a symlinked directory, a symlink to the file, a +# hardlink, /proc/self/root, an unparseable payload, a project dir pointing elsewhere — and a suffix match closes +# three of them. Taking the root from the file's own checkout (`git -C "$(dirname "$file")" rev-parse +# --show-toplevel`) closes none: git fails for a new file in a directory that does not exist yet, and the root it +# finds follows no symlink, hardlink or /proc link. The recipe, which closes all ten: +# - the payload is read exactly, in one jq process (@sh-quoted, so a trailing newline survives), and a payload +# jq cannot read is reported for the guard to refuse; +# - the path is resolved two ways — lexically, collapsing `..` as a writer that normalizes the path would, and +# physically, one component at a time through symlinks, `..` taken from the physical parent — and a guard +# denies when either reading lands in its tree; an unreadable symlink and any path through /proc (whose magic +# links resolve per process, so a readlink run here is not the writer) are flagged for the guard to refuse; +# - the roots are the calling hook's own checkout, derived LEXICALLY from the hook's path (a symlinked +# .claude/hooks must not move it), and CLAUDE_PROJECT_DIR; a linked worktree of either (its `.git` file's +# gitdir resolves under the root's .git/worktrees/) is found on demand, in pure bash; +# - the same file or directory under another name (a hardlink, a bind mount) is caught with bash's own `-ef` +# over a glob walk: no stat or find, so no missing tool can switch the check off. +# Bash 3.2-safe (no mapfile, no associative arrays, no ${x,,}); pure bash plus jq and readlink. The contract a +# sourcing hook keeps is cbk-conventions-reference.md § Hook authoring (sourced helpers). Fixture: +# .claude/workflows/tests/protected-paths-hook-fixture.sh, which also drives the root-scoped functions through a +# corpus guard of its own. + +# rp_payload : RP_TOOL, RP_FILE and RP_CWD from the hook payload, read exactly, in ONE jq process. jq's +# @sh single-quotes each value, so a trailing newline in a path survives the command substitution, and eval +# assigns the three at once. Returns 1 when jq cannot read the payload — not parseable (jq refuses a lone UTF-16 +# surrogate escape anywhere in the input), not an object, or a field no shell word can hold, such as an object +# where a path belongs — and the caller refuses. +rp_payload() { + local fields + fields=$(jq -r 'if type == "object" then @sh "RP_TOOL=\(.tool_name // "") RP_FILE=\(.tool_input.file_path // "") RP_CWD=\(.cwd // "")" else error("not an object") end' <<<"$1" 2>/dev/null) || return 1 + eval "$fields" +} + +# rp_lexnorm : made absolute against , with `.`, empty components and `..` collapsed +# LEXICALLY. Result in RP_LEX. +rp_lexnorm() { + local rest=$1 out='' part + [[ "$rest" == /* ]] || rest="$2/$rest" + while [ -n "$rest" ]; do + case "$rest" in */*) part=${rest%%/*}; rest=${rest#*/} ;; *) part=$rest; rest='' ;; esac + case "$part" in + ''|.) ;; + ..) out=${out%/*} ;; + *) out="$out/$part" ;; + esac + done + RP_LEX=${out:-/} +} + +# rp_canon : the physical path the kernel would open, component by component — every symlink followed +# where it is met (its exact text: a target may end in a newline), `..` from the PHYSICAL parent, components that +# do not exist kept as given. Result in RP_CANON. Flags, never guesses: RP_THROUGH_PROC (a path through /proc) and +# RP_UNREADABLE (a symlink it could not read). After 40 symlink hops the rest of the path is taken as given, so a +# symlink loop ends. +rp_canon() { + local rest=$1 phys=/ part cand link hops=0 + [[ "$rest" == /* ]] || rest="$2/$rest" + while [ -n "$rest" ]; do + case "$rest" in */*) part=${rest%%/*}; rest=${rest#*/} ;; *) part=$rest; rest='' ;; esac + case "$part" in + ''|.) ;; + ..) phys=${phys%/*}; [ -n "$phys" ] || phys=/ ;; + *) + if [ "$phys" = / ]; then cand="/$part"; else cand="$phys/$part"; fi + case "$cand" in /proc|/proc/*) RP_THROUGH_PROC=$cand ;; esac + if [ -L "$cand" ] && [ "$hops" -lt 40 ]; then + hops=$((hops + 1)) + link=$(readlink -- "$cand" 2>/dev/null && printf x) + case "$link" in + *x) link=${link%x}; link=${link%$'\n'} ;; + *) RP_UNREADABLE=$cand; phys=$cand; continue ;; + esac + [[ "$link" == /* ]] && phys=/ + rest="$link/$rest" + else + phys=$cand + fi + ;; + esac + done + RP_CANON=$phys +} + +# rp_target : RP_T_LEXICAL and RP_T_PHYSICAL — the target under both readings of `..` (lexical, +# as a writer that normalizes the path resolves it; physical, as the kernel resolves a raw path), a relative path +# resolved against the payload's cwd. Clears, then sets, RP_THROUGH_PROC and RP_UNREADABLE. +rp_target() { + local base=${2:-$PWD} + [[ "$base" == /* ]] || base="$PWD/$base" + RP_THROUGH_PROC='' RP_UNREADABLE='' + rp_lexnorm "$1" "$base"; rp_canon "$RP_LEX" /; RP_T_LEXICAL=$RP_CANON + rp_canon "$1" "$base"; RP_T_PHYSICAL=$RP_CANON +} + +# rp_roots : RP_ROOTS — the hook's own checkout (the hook is /.claude/hooks/.sh, +# taken lexically, then resolved physically like the target) and CLAUDE_PROJECT_DIR when it is set and names +# another directory, so a worktree session's hook also guards the checkout the session belongs to. +rp_roots() { + local r + rp_lexnorm "$1" "$PWD" + r=${RP_LEX%/*}; r=${r%/*}; r=${r%/*} + rp_canon "${r:-/}" / + RP_ROOTS=("$RP_CANON") + if [ -n "${CLAUDE_PROJECT_DIR:-}" ]; then + rp_canon "$CLAUDE_PROJECT_DIR" "$PWD" + # In an ordinary session the project dir IS the hook's checkout: one root, so no walk runs twice. + [ "$RP_CANON" -ef "${RP_ROOTS[0]}" ] || RP_ROOTS+=("$RP_CANON") + fi +} + +# rp_linked_worktree : RP_WT — the root of the checkout the path lives in when that checkout is a +# linked worktree of a root in RP_ROOTS (its `.git` is a file whose `gitdir:` resolves under +# /.git/worktrees/), else empty. Pure bash: the `.git` file is read with the `read` builtin; no git runs. +rp_linked_worktree() { + local d=$1 line gd r + RP_WT='' + while [ -n "$d" ] && [ "$d" != / ] && [ ! -d "$d" ]; do d=${d%/*}; done + [ -n "$d" ] || d=/ + while :; do + if [ -f "$d/.git" ]; then + line='' + IFS= read -r line < "$d/.git" || true + case "$line" in "gitdir: "*) gd=${line#gitdir: } ;; *) return 0 ;; esac + [[ "$gd" == /* ]] || gd="$d/$gd" + rp_canon "$gd" / + for r in "${RP_ROOTS[@]}"; do + case "$RP_CANON" in "${r%/}/.git/worktrees/"*) RP_WT=$d; return 0 ;; esac + done + return 0 + fi + [ -d "$d/.git" ] && return 0 + [ "$d" = / ] && return 0 + d=${d%/*}; [ -n "$d" ] || d=/ + done +} + +# rp_nearest_dir : RP_DIR — the path itself if it is a directory, else its nearest existing ancestor. +rp_nearest_dir() { + RP_DIR=$1 + while [ ! -d "$RP_DIR" ]; do RP_DIR=${RP_DIR%/*}; [ -n "$RP_DIR" ] || RP_DIR=/; done +} + +# rp_same_entry []: returns 0 and sets RP_HIT when +# is one of the guarded files (a hardlink) or is the guarded dir under another name (a +# bind mount) — same device and inode, by bash's `-ef`, over a walk in bash globs. Without a glob the whole tree is +# guarded: every file, and every subdirectory as a directory too (a corpus). With one, only the files that glob +# names directly under the guarded dir, and the dir itself, with no recursion (the numbered ADRs); the files are +# compared first, so a hardlink is caught whatever directory it is reached through. +rp_same_entry() { + local e + if [ -n "${4:-}" ]; then + if [ -n "$3" ]; then + for e in "$1"/$4; do # $4 unquoted on purpose: it is the glob + if [ -f "$e" ] && [ ! -L "$e" ] && [ "$e" -ef "$3" ]; then RP_HIT=$e; return 0; fi + done + fi + if [ "$1" -ef "$2" ]; then RP_HIT=$1; return 0; fi + return 1 + fi + if [ "$1" -ef "$2" ]; then RP_HIT=$1; return 0; fi + for e in "$1"/* "$1"/.[!.]* "$1"/..?*; do + if [ -d "$e" ] && [ ! -L "$e" ]; then + rp_same_entry "$e" "$2" "$3" && return 0 + elif [ -n "$3" ] && [ -f "$e" ] && [ ! -L "$e" ] && [ "$e" -ef "$3" ]; then + RP_HIT=$e; return 0 + fi + done + return 1 +} +``` + +Replace the whole of `.claude/hooks/protect-immutable-adrs.sh` with (mode stays 755): +```bash +#!/usr/bin/env bash +# PreToolUse hook (Edit|Write|MultiEdit): block edits/writes to existing immutable ADR files. +# +# ADRs in docs/adr/-*.md are immutable per ADR-0000. Superseding requires a NEW ADR (with a higher number +# that links to the old one as `Supersedes:`) — never an edit to the old one. +# +# Allowed: creating a new ADR file (NNNN doesn't exist yet — tested on the RESOLVED path, so creation stays +# allowed); editing docs/adr/template.md, docs/adr/README.md (the index) and docs/adr/corrections.md +# (the append-only claim register — where a wrong citation, figure, attribution or formula in an +# accepted ADR is corrected); a file nested below docs/adr/, and a sibling directory that only shares +# the prefix (docs/adr-old/); every other tool. +# Blocked: any Edit/Write/MultiEdit whose target resolves — lexically or physically — to an existing +# docs/adr/NNNN-*.md in ANY checkout: every target keeps its immutable ADRs at that path, and a session +# in one target edits its siblings; the same ADR under another name in the hook's own checkout, the +# project dir's, or a linked worktree of either (a hardlink to an ADR, a bind mount of docs/adr/ — same +# device and inode); a path through /proc, a symlink that cannot be read, and a payload jq cannot read +# — none of them can be checked, so each is refused. +# Path: resolved by .claude/hooks/lib/resolve-path.sh (the recipe and the ten spellings it closes are in its +# header; context-builder-kit#60). Registered as ${CLAUDE_PROJECT_DIR}/.claude/hooks/… — "Handlers run in +# the current directory" (https://code.claude.com/docs/en/hooks, read 2026-09-30), so a bare relative +# path would not resolve from a subdirectory. +# Not seen: a write through the Bash tool (the matcher is Edit|Write|MultiEdit), a case-insensitive filesystem, a +# hand edit, and a symlink swapped between this check and the write. CI's ADR immutability job +# (.github/workflows/adr-immutability-check.yml) refuses any of them that reaches a PR. +# Tier: HARD-DENY (see the registry comment in .claude/settings.json). +# Depends: jq (the payload) — absent, the guard fails open: exit 0 with a stderr warning naming CI's ADR +# immutability job as the backstop. The sourced helper .claude/hooks/lib/resolve-path.sh — absent, the +# same fail-open and the same backstop. readlink (a symlink's target) — absent, a path through a symlink +# is refused. Nothing else: the device-and-inode check is bash's own `-ef`. +# +# Hook receives JSON on stdin with the tool input. Exit 2 + stderr blocks. Fixture: +# .claude/workflows/tests/protected-paths-hook-fixture.sh. + +set -uo pipefail + +# Drain stdin before any early exit, or a piping caller's SIGPIPE masks this hook's own +# exit code (cbk-conventions-reference.md § Hook authoring › The stdin / exit contract). +input="$(cat)" +# Deliberately NOT `set -e`: on an environment defect an abort exits non-2, which the hook +# contract reads as NON-blocking — the ADR edit would go through with a cryptic error. Warn and +# fall open instead, naming the backstop. + +if ! command -v jq &>/dev/null; then + echo "protect-immutable-adrs: WARNING — jq not installed; ADR-immutability protection DISABLED." >&2 + echo " Install jq to re-enable. Backstop: CI's ADR immutability job (.github/workflows/adr-immutability-check.yml) still refuses the change at PR time." >&2 + exit 0 +fi + +case "${BASH_SOURCE[0]}" in */*) here=${BASH_SOURCE[0]%/*} ;; *) here=. ;; esac +if [ ! -r "$here/lib/resolve-path.sh" ]; then + echo "protect-immutable-adrs: WARNING — its path helper $here/lib/resolve-path.sh is missing; ADR-immutability protection DISABLED." >&2 + echo " Backstop: CI's ADR immutability job (.github/workflows/adr-immutability-check.yml) still refuses the change at PR time." >&2 + exit 0 +fi +# shellcheck source=lib/resolve-path.sh +. "$here/lib/resolve-path.sh" + +# deny : the one refusal, with the ADR's standing explanation. +deny() { + cat >&2 <&1 | grep -E 'protected-paths-hook-fixture|^verification: (kit sub-block complete|done)$|^verification: block exited'; echo "exit=${PIPESTATUS[0]}"` +Expected: `protected-paths-hook-fixture: 48 cases ok` (or 45 with its SKIP line), `verification: kit sub-block complete`, `verification: done`, `exit=0`. + +- [ ] **Step 5: Commit** + +```bash +git add .claude/hooks/lib/resolve-path.sh .claude/hooks/protect-immutable-adrs.sh .claude/workflows/tests/protected-paths-hook-fixture.sh .claude/workflows/tests/hook-contract-fixture.sh .claude/rules/cbk-conventions-reference.md +git ls-files -s .claude/hooks/lib/resolve-path.sh | cut -c1-6 +git commit -q -F - <<'MSG' +fix(V2): the ADR guard resolves the path it judges, through a shared helper + +protect-immutable-adrs.sh matched the payload's path text against a prefix, so +every spelling context-builder-kit#60 measured passed: a .. segment, . and //, a +trailing-slash or foreign project dir, a relative path, a symlinked directory, a +symlink to the file, a hardlink, /proc/self/root and an unparseable payload. The +guard now sources .claude/hooks/lib/resolve-path.sh: the payload read exactly in +one jq, the path resolved lexically and physically, /proc and unreadable symlinks +refused, the same file under another name caught by bash's -ef over the hook's own +checkout, the project dir and a linked worktree. The deny is path-independent: an +existing docs/adr/NNNN-*.md in any checkout; the existence test runs on the +resolved path, so a new ADR stays creatable. A missing helper or jq fails open +naming the ADR job. hook-contract-fixture.sh's early-reader check reads lib/*.sh; +section Hook authoring states the sourced-helper contract and the lib/ gitignore +trap, and the block asserts the helper is not ignored. + +Red first: FAIL [protect-immutable-adrs.sh]: a .. segment (docs/sub/../adr/) (want exit 2, got 0) + +Trace: #60/body/fix, #60/body/alt, #60/body/fixture, #60/c5876324022/table, +#60/c5876324022/closed-1, closed-2, closed-3, closed-4, +#60/c5876324022/closed-5, closed-6, #60/c5876324022/closed-note, +#60/c5881158391/port-helper, #60/c5892401033/helper, +#60/c5892401033/adr-path-independent, #60/c5892401033/linked-worktrees, +#60/c5892401033/ef-inode, #60/c5892401033/gitignore-trap, +#60/c5892401033/fixture, #60/c5901494943/2a, 2b, 2c, 2d, 2e, +#62/body/extra-adr-hook, +#58/c5901493591/R1 (the path guard), release/5 (section Hook authoring), +#69/c5859756889/apply-h4/1 (a target's own hooks, held to the same contract), +review/consistency/48 (the tally, rewritten count-free). +Decisions: D43, D44, D61. + +Co-Authored-By: Claude Opus 5.5 +MSG +``` +Expected from `git ls-files -s`: `100644`. +### Task V2.3: Every fail-open names its backstop, and an unreadable payload is refused or asked (#62/body/lock-files, #62/body/pr-state, #62/body/main-branch, #58/c5901493591/R8, #58/c5901493591/R1 (the four guards), critic/8, #66/body/2 (the ask-gate header), release/5 (protect-lock-files.sh); D61, D44) + +**Files:** +- Create: `.claude/workflows/tests/hook-guards-fixture.sh` (mode 755) +- Modify: `.claude/hooks/protect-lock-files.sh` (header lines 9–22, the jq warning at line 40, the fallback arm at lines 82–86) +- Modify: `.claude/hooks/protect-main-branch.sh` (header lines 21–24, gaining a `Depends:` line after `Tier:`; the jq warning at lines 36–37, the non-repo warning at line 67) +- Modify: `.claude/hooks/guard-pr-state.sh` (header lines 14–16, the jq warning at lines 26–27) +- Modify: `.claude/hooks/require-knowledge-backend-ok.sh` (header lines 2–9 and 17) +- Modify: `.claude/rules/cbk-conventions-reference.md` — § Hook authoring's Fail-open bullet; § Verification's kit and project sentinels +- Modify: `.claude/skills/scaffold/references/bootstrap_checklist_template.md` — § 4 Rule-file disposition, the one-time choices list + +**Interfaces:** +- Produces: `.claude/workflows/tests/hook-guards-fixture.sh`. It finds the knowledge-backend ask-gate through `settings.json` — the `.sh` path in the `command` or `args` of the `PreToolUse` entry whose matcher starts `mcp__` — never by name, so V3's move to exec form does not break it (tested on shell form, on `args: []` and on `command: "bash"` with the path in `args`). With no such entry it prints `SKIP: the knowledge-backend ask-gate (no mcp__ matcher in settings.json — the knowledge axis is none)` — the line V5's knowledge-axis-`none` dry run expects (Review Focus 3). +- Produces: the wording the fixture pins, which any later edit to these hooks keeps: `BLOCKED`, `git switch -c`, `not a git repo`, `jq not installed`, `Backstop`, `Backstops`, `could not read the tool payload`, `package-manager-managed`, `pnpm install`, `fallback arm`, "above the `*.lock)` arm", and the absence of `exemption comment`. +- Produces: three bracketed backstop slots — `[the project's CI lockfile check — …]` in `protect-lock-files.sh`, and `[the base branch's ruleset — pull requests only — where one exists]` in both warnings of `protect-main-branch.sh` — the bootstrap row `**Hook backstop slots**` that fills them, and the project-sub-block check `absent grep -nE 'Backstops?( until then)?: \[' .claude/hooks/*.sh` that refuses one left unfilled. +- Produces: the § Hook authoring sentence `**An unreadable payload is not an environment defect.**`, cited by every hook header this cluster touches. V3's registry comment restates the fail-open rule with it (§ Handed to other clusters). + +- [ ] **Step 1: Write the failing fixture** + +Create `.claude/workflows/tests/hook-guards-fixture.sh`: +```bash +#!/usr/bin/env bash +# Behavioural cases for the four guards no other fixture drives by payload: the main-branch deny +# (protect-main-branch.sh), the PR-state ask-gate (guard-pr-state.sh), the lock-file deny (protect-lock-files.sh) and +# the knowledge-backend ask-gate — every branch each documents in its header gets a crafted payload and an asserted +# exit or decision (§ Hook authoring › Verify by payload, made durable; context-builder-kit#58 item 4). A payload jq +# cannot read is refused by each deny and asked by each ask-gate; each fail-open warning names what still stands; one +# case per deny runs from a project root containing a space. hook-contract-fixture.sh checks every hook's structure +# and one over-buffer probe per decision site; hook-payloads-fixture.sh drives the launch-root guard and the fork +# detector; protected-paths-hook-fixture.sh drives the path guard. The two advisory exemplars (format-on-edit.sh, +# analyze-on-edit.sh) ship with no live case arm, so there is no branch to drive until a target fills one. +# The knowledge-backend guard is found by the registry, not by name: the script (the `command`, or an `args` element, +# ending in .sh) of the settings.json entry whose matcher names a knowledge-backend MCP tool (mcp__…), so shell form +# and exec form both resolve. A target whose knowledge axis is `none` deletes that guard and its stanza at the +# bootstrap disposition pass, and its cases then print a SKIP line. +# Every case runs against throwaway `git init` trees under mktemp, never the real repository; the hooks run from their +# real path, so a change to any of them is caught. Needs bash, git and jq; runs in throwaway trees. +# Run by the verification block; also: bash .claude/workflows/tests/hook-guards-fixture.sh +set -euo pipefail +here=$(cd "$(dirname "$0")" && pwd) +hooks="$here/../../hooks" +settings="$here/../../settings.json" +MAIN="$hooks/protect-main-branch.sh"; PRS="$hooks/guard-pr-state.sh"; LOCK="$hooks/protect-lock-files.sh" +for h in "$MAIN" "$PRS" "$LOCK"; do [ -x "$h" ] || { echo "hook-guards-fixture: $h is missing or not executable"; exit 1; }; done +command -v jq >/dev/null && command -v git >/dev/null || { echo "hook-guards-fixture: needs jq and git"; exit 1; } +[ -f "$settings" ] || { echo "hook-guards-fixture: $settings is missing"; exit 1; } +kbname=$(jq -r '[.hooks.PreToolUse[]? | select((.matcher // "") | test("^mcp__")) | .hooks[] | [.command] + (.args // []) | map(select(type == "string" and endswith(".sh"))) | .[0] // empty] | first // empty' "$settings") +KB=""; [ -z "$kbname" ] || KB="$hooks/${kbname##*/}" +d=$(mktemp -d) +trap 'rm -rf "$d"' EXIT +# Git discovery stops at the throwaway root (the ceiling is its parent), so a TMPDIR inside a work tree cannot turn +# the non-checkout case into a checkout. +export GIT_CEILING_DIRECTORIES="${d%/*}" + +repo_on() { # repo_on : a throwaway checkout with one commit, on + git -c init.defaultBranch=main init -q "$1" + git -C "$1" -c user.email=t@t -c user.name=t -c commit.gpgsign=false commit -q --allow-empty -m x + [ "$2" = main ] || git -C "$1" switch -q -c "$2" +} +repo_on "$d/on-main" main; repo_on "$d/on-master" main; git -C "$d/on-master" branch -q -m master; repo_on "$d/on-feat" feat/x +repo_on "$d/a b" main +mkdir -p "$d/plain" + +n=0; RC=0; OUT=""; ERR="" +run() { # run [env args…]: RC, OUT (stdout), ERR (stderr); runs in $RUN_CWD when it is set + local hook=$1 payload=$2; shift 2 + RC=0; OUT="$(cd "${RUN_CWD:-$PWD}" && printf '%s' "$payload" | env "$@" "$hook" 2>"$d/err")" || RC=$?; ERR="$(cat "$d/err")"; n=$((n + 1)) +} +want() { [ "$RC" -eq "$1" ] || { echo "FAIL: $2 (want exit $1, got $RC)"; [ -n "$ERR" ] && printf ' stderr: %s\n' "$ERR"; exit 1; }; } +says() { grep -q -- "$1" <<<"$ERR" || { echo "FAIL: $2 (stderr lacks '$1')"; printf ' stderr: %s\n' "$ERR"; exit 1; }; } +asks() { [ "$RC" -eq 0 ] && jq -e '.hookSpecificOutput.permissionDecision == "ask"' >/dev/null 2>&1 <<<"$OUT" \ + || { echo "FAIL: $1 (want an ask decision on stdout, exit 0; got exit $RC)"; printf ' stdout: %s\n' "$OUT"; exit 1; }; } +silent() { [ "$RC" -eq 0 ] && [ -z "$OUT" ] || { echo "FAIL: $1 (want exit 0 and no decision; got exit $RC)"; printf ' stdout: %s\n' "$OUT"; exit 1; }; } +bash_payload() { jq -cn --arg c "$1" --arg cwd "$2" '{tool_name:"Bash", tool_input:{command:$c}, cwd:$cwd}'; } +edit_payload() { jq -cn --arg t "$1" --arg f "$2" '{tool_name:$t, tool_input:{file_path:$f}}'; } +surrogate() { printf '{"tool_name":"%s","tool_input":{"%s":"%s","content":"x\\ud800y"},"cwd":"%s"}' "$1" "$2" "$3" "$4"; } +nojq="$d/nojq"; mkdir -p "$nojq" +for t in bash cat git basename dirname; do p=$(type -P "$t" || true); [ -n "$p" ] && ln -sf "$p" "$nojq/$t"; done + +# ── protect-main-branch.sh (HARD-DENY: a `git commit` on main or master) ── +run "$MAIN" "$(bash_payload 'git commit -m x' "$d/on-main")"; want 2 "a commit on main is denied" +says "BLOCKED" "the denial names itself"; says "git switch -c" "the denial gives the branch-first remediation" +run "$MAIN" "$(bash_payload 'git commit -m x' "$d/on-master")"; want 2 "a commit on master is denied" +run "$MAIN" "$(bash_payload 'git commit -m x' "$d/on-feat")"; want 0 "a commit on a feature branch is allowed" +run "$MAIN" "$(bash_payload 'git -C . commit -m x' "$d/on-main")"; want 2 "options between git and commit are still a commit" +run "$MAIN" "$(bash_payload 'git add -A && git commit -m x' "$d/on-main")"; want 2 "a commit after && is still a commit" +run "$MAIN" "$(bash_payload 'echo legit commitment' "$d/on-main")"; want 0 "text merely containing the phrase is allowed" +run "$MAIN" "$(bash_payload 'git status' "$d/on-main")"; want 0 "a non-commit git command is allowed" +run "$MAIN" "$(jq -cn --arg cwd "$d/on-main" '{tool_name:"Edit", tool_input:{file_path:"x"}, cwd:$cwd}')"; want 0 "a non-Bash tool passes through" +run "$MAIN" "$(bash_payload '' "$d/on-main")"; want 0 "an empty command passes through" +run "$MAIN" "$(bash_payload 'git commit -m x' "$d/a b")"; want 2 "a commit on main under a project root containing a space is denied" +# The payload's cwd wins over CLAUDE_PROJECT_DIR, both ways (the two disagree in a worktree session). +run "$MAIN" "$(bash_payload 'git commit -m x' "$d/on-feat")" CLAUDE_PROJECT_DIR="$d/on-main"; want 0 "payload cwd on a feature branch wins over a project dir on main" +run "$MAIN" "$(bash_payload 'git commit -m x' "$d/on-main")" CLAUDE_PROJECT_DIR="$d/on-feat"; want 2 "payload cwd on main wins over a project dir on a feature branch" +run "$MAIN" "$(jq -cn '{tool_name:"Bash", tool_input:{command:"git commit -m x"}}')" CLAUDE_PROJECT_DIR="$d/on-main"; want 2 "with no payload cwd, the project dir decides" +# With neither a payload cwd nor CLAUDE_PROJECT_DIR, the hook's own working directory decides ($PWD, the last fallback). +RUN_CWD="$d/on-main" run "$MAIN" "$(jq -cn '{tool_name:"Bash", tool_input:{command:"git commit -m x"}}')" -u CLAUDE_PROJECT_DIR; want 2 "with no cwd and no project dir, \$PWD on main is denied" +RUN_CWD="$d/on-feat" run "$MAIN" "$(jq -cn '{tool_name:"Bash", tool_input:{command:"git commit -m x"}}')" -u CLAUDE_PROJECT_DIR; want 0 "with no cwd and no project dir, \$PWD on a feature branch is allowed" +# A commit with nothing after it — the end-of-command form, reached after && or alone. +run "$MAIN" "$(bash_payload 'git add -A && git commit' "$d/on-main")"; want 2 "a bare commit at the end of the command is still a commit" +run "$MAIN" "$(bash_payload 'git commit' "$d/on-main")"; want 2 "a bare commit alone is a commit" +# Fail-open branches name what still stands. +run "$MAIN" "$(bash_payload 'git commit -m x' "$d/plain")"; want 0 "a directory that is not a checkout fails open" +says "not a git repo" "the non-checkout warning says why"; says "Backstop" "the non-checkout warning names a backstop" +run "$MAIN" "$(bash_payload 'git commit -m x' "$d/on-main")" -i PATH="$nojq" HOME="$HOME"; want 0 "no jq fails open" +says "jq not installed" "the no-jq warning says why"; says "Backstops" "the no-jq warning names the backstops" +# Input it cannot read is refused, on any branch: the guard cannot tell what it is. +run "$MAIN" "$(surrogate Bash command 'git commit -m x' "$d/on-feat")"; want 2 "a payload jq cannot parse is refused" +says "could not read the tool payload" "the refusal says why" +run "$MAIN" '["not", "an", "object"]'; want 2 "a payload that is not an object is refused" + +# ── guard-pr-state.sh (ASK-GATE: a PR-state change) ── +for c in 'gh pr merge 7 --squash' 'gh pr ready 7' 'gh pr close 7' 'gh pr reopen 7' \ + 'gh -R o/r pr merge 7' 'gh pr --repo o/r ready 7' 'git push && gh pr merge 7' \ + 'gh pr merge' 'gh pr ready' 'git push && gh pr merge'; do # the bare forms act on the current branch's PR + run "$PRS" "$(bash_payload "$c" "$d/on-feat")"; asks "a PR-state change asks: $c" +done +for c in 'gh pr create --draft' 'gh pr view 7' 'gh pr list' 'gh pr checks 7' 'gh issue close 7'; do + run "$PRS" "$(bash_payload "$c" "$d/on-feat")"; silent "no decision for: $c" +done +run "$PRS" "$(jq -cn '{tool_name:"Edit", tool_input:{file_path:"x"}}')"; silent "a non-Bash tool passes through" +run "$PRS" "$(bash_payload 'gh pr merge 7' "$d/on-feat")" -i PATH="$nojq" HOME="$HOME"; want 0 "no jq fails open" +says "jq not installed" "the no-jq warning says why"; says "Backstop" "the no-jq warning names what still stands" +run "$PRS" "$(surrogate Bash command 'gh pr merge 7' "$d/on-feat")"; asks "a payload jq cannot parse gets the prompt" +run "$PRS" 'not json at all'; asks "a payload that is not JSON gets the prompt" + +# ── protect-lock-files.sh (HARD-DENY: a hand edit to a lock file) ── +for f in uv.lock pnpm-lock.yaml package-lock.json yarn.lock Cargo.lock Gemfile.lock poetry.lock composer.lock mix.lock pubspec.lock; do + run "$LOCK" "$(edit_payload Edit "$d/on-feat/$f")"; want 2 "a hand edit to $f is denied" + says "package-manager-managed" "$f is denied by its named arm, not the fallback" +done +run "$LOCK" "$(edit_payload Write "$d/on-feat/frontend/pnpm-lock.yaml")"; want 2 "a nested lock file is denied too" +says "pnpm install" "the named arm gives its package manager" +run "$LOCK" "$(edit_payload Edit "$d/a b/uv.lock")"; want 2 "a lock file under a project root containing a space is denied" +run "$LOCK" "$(edit_payload MultiEdit "$d/on-feat/flake.lock")"; want 2 "an unnamed *.lock is denied by the fallback arm" +says "fallback arm" "the fallback denial names itself" +says "above the \`\*.lock)\` arm" "the fallback gives the real route for a non-lock file: a case arm above it" +! grep -q "exemption comment" <<<"$ERR" || { echo "FAIL: the fallback still points at an exemption comment the hook does not have"; exit 1; } +for f in pyproject.toml lockfile.md uv.lock.md docs/locking.lock.txt; do + run "$LOCK" "$(edit_payload Edit "$d/on-feat/$f")"; want 0 "a non-lock file is allowed: $f" +done +run "$LOCK" "$(edit_payload Read "$d/on-feat/uv.lock")"; want 0 "a read of a lock file is allowed" +run "$LOCK" "$(jq -cn '{tool_name:"Edit", tool_input:{}}')"; want 0 "an empty file_path passes through" +run "$LOCK" "$(edit_payload Edit "$d/on-feat/uv.lock")" -i PATH="$nojq" HOME="$HOME"; want 0 "no jq fails open" +says "jq not installed" "the no-jq warning says why"; says "Backstop" "the no-jq warning names what still stands" +run "$LOCK" "$(surrogate Write file_path "$d/on-feat/uv.lock" "$d/on-feat")"; want 2 "a payload jq cannot parse is refused" +says "could not read the tool payload" "the refusal says why" + +# ── the knowledge-backend ask-gate (ASK-GATE: every knowledge-backend write) — asks whatever the payload ── +if [ -n "$KB" ]; then + [ -x "$KB" ] || { echo "FAIL: settings.json registers ${kbname##*/}, which is missing or not executable"; exit 1; } + run "$KB" '{"tool_name":"mcp__notion__notion-update-page","tool_input":{"page_id":"x"}}'; asks "a knowledge-backend write asks" + run "$KB" 'not json at all'; asks "an unparseable payload still asks (no parse, no fail-open)" + run "$KB" ''; asks "an empty payload still asks" +else + echo "SKIP: the knowledge-backend ask-gate (no mcp__ matcher in settings.json — the knowledge axis is none)" +fi + +echo "hook-guards-fixture: $n cases ok" +``` + +Run: `chmod 755 .claude/workflows/tests/hook-guards-fixture.sh` + +- [ ] **Step 2: Run it against the unfixed hooks** + +Run: `bash .claude/workflows/tests/hook-guards-fixture.sh; echo "exit=$?"` +Expected: +```text +FAIL: the non-checkout warning names a backstop (stderr lacks 'Backstop') + stderr: protect-main-branch: WARNING — /tmp/tmp.XXXXXXXXXX/plain is not a git repo; guard inactive for this call. +exit=1 +``` +(the temp path varies). Record the FAIL line in the red-first table. The unreadable-payload cases sit at the end of each guard's section; each is red on the unfixed hooks too (exit 0 for a lone-surrogate payload, D61's probe at `74edf84`). + +- [ ] **Step 3: Land the wording, the refusals and the rule** + +No platform fact is new here: the ask-gate's ten-verb rationale is moved, unchanged and still dated 2026-09-21, from the registry comment into the hook header (#66/body/2's first half; the trim of the registry comment is V9's and V3's). + +Edit `.claude/hooks/protect-lock-files.sh` — replace this text (exact; it occurs once): +```bash +# Allowed: +# - Bash invocations of the package manager (the proper way to change locks) +# - Reads of lock files (no Edit/Write involved) +# Blocked: +# - Edit/Write/MultiEdit on lock files +# +# Hook receives JSON on stdin with the tool input. Exit 2 + stderr blocks. +# +# Fail-open on environment defects (missing jq, unset CLAUDE_PROJECT_DIR): +# exit 0 with a loud stderr warning. The alternative (failing closed) converts +# a targeted lock-file block into a universal Edit/Write/MultiEdit block, which +# is worse than allowing a lock-file edit to slip through. The operator notices +# the warning and fixes their environment. +# Tier: HARD-DENY (see the registry comment in .claude/settings.json). +``` +with: +```bash +# Allowed: Bash invocations of the package manager (the proper way to change locks); reads of lock +# files (no Edit/Write involved); a file with a case arm of its own above the `*.lock)` arm. +# Blocked: Edit/Write/MultiEdit on a lock file — a named ecosystem's, or any other `*.lock` by the +# fallback arm; and a payload jq cannot read, whose path cannot be checked. +# Not seen: an edit through a symlink under another name (the guard decides on the path text, so a +# link named deps.txt that points at uv.lock passes), and a write through the Bash tool +# (unmatched on purpose: the package manager is the route). The project's CI lockfile +# check, where one exists, refuses a drifted lock at PR time. +# Tier: HARD-DENY (see the registry comment in .claude/settings.json). +# Depends: jq (the payload) — absent, the guard fails open: exit 0 with a stderr warning naming the +# project's CI lockfile check as the backstop (a bracketed slot the project fills). +# +# Hook receives JSON on stdin with the tool input. Exit 2 + stderr blocks. Fail-open on environment +# defects (missing jq, unset CLAUDE_PROJECT_DIR): exit 0 with a loud stderr warning — failing closed +# would turn a targeted lock-file block into a universal Edit/Write/MultiEdit block. Input the guard +# cannot read is not an environment defect, and is refused (cbk-conventions-reference.md § Hook +# authoring). Fixture: .claude/workflows/tests/hook-guards-fixture.sh. +``` + + +Edit `.claude/hooks/protect-lock-files.sh` — replace this text (exact; it occurs once): +```bash + echo " Install jq to re-enable. Lock-file edits will NOT be caught until fixed." >&2 + exit 0 +fi +``` +with: +```bash + echo " Install jq to re-enable. Backstop until then: [the project's CI lockfile check — a" >&2 + echo " locked or frozen-lockfile install that fails when the lock file and its manifest disagree]." >&2 + exit 0 +fi + +# A payload jq cannot read is refused, never waved through: this hook runs on Edit|Write|MultiEdit only, +# so the call is still a file write, and its path cannot be checked (a lone UTF-16 surrogate escape +# anywhere in the tool input does this). +if ! jq -e 'type == "object"' >/dev/null 2>&1 <<<"$input"; then + echo "BLOCKED: protect-lock-files could not read the tool payload (not parseable JSON, or not an object)," >&2 + echo "so its path cannot be checked. A lone UTF-16 surrogate escape in the tool input does this — remove it and retry." >&2 + exit 2 +fi +``` + + +Edit `.claude/hooks/protect-lock-files.sh` — replace this text (exact; it occurs once): +```bash +by construction rather than by an edit, #58 item 3). +Run the package manager that owns it instead of editing it by hand. If this file is +NOT a lock file, add its name to the hook's exemption comment and say so in the PR. +``` +with: +```bash +by construction rather than by an edit, context-builder-kit#58 item 3). +Run the package manager that owns it instead of editing it by hand. If this file is +NOT a lock file, give it a case arm of its own above the \`*.lock)\` arm in this hook +(an arm that exits 0), and say so in the PR. +``` + + +Edit `.claude/hooks/protect-main-branch.sh` — replace this text (exact; it occurs once): +```bash +# Hook receives JSON on stdin with the tool input. Exit 2 + stderr blocks. +# Fail-open on environment defects (missing jq, non-repo cwd): exit 0 with a +# loud stderr warning, mirroring protect-lock-files.sh. +# Tier: HARD-DENY (see the registry comment in .claude/settings.json). +``` +with (the hook gains the `Depends:` line every header carries, which it lacked): +```bash +# Hook receives JSON on stdin with the tool input. Exit 2 + stderr blocks. +# Fail-open on environment defects (missing jq, non-repo cwd): exit 0 with a +# loud stderr warning naming the backstops, mirroring protect-lock-files.sh. +# A payload jq cannot read is refused (cbk-conventions-reference.md § Hook +# authoring). Fixture: .claude/workflows/tests/hook-guards-fixture.sh. +# Tier: HARD-DENY (see the registry comment in .claude/settings.json). +# Depends: jq (the payload) and git (the branch) — absent jq, the guard fails +# open: exit 0 with a stderr warning naming the backstops; absent git, +# or a working directory that is not a checkout, the same. +``` + + +Edit `.claude/hooks/protect-main-branch.sh` — replace this text (exact; it occurs once): +```bash + echo " Install jq to re-enable. Until then the only backstop is the operator" >&2 + echo " noticing a commit on main before pushing." >&2 + exit 0 +fi +``` +with: +```bash + echo " Install jq to re-enable. Backstops until then: [the base branch's ruleset —" >&2 + echo " pull requests only — where one exists], and the operator noticing a commit" >&2 + echo " on local main before branching." >&2 + exit 0 +fi + +# A payload jq cannot read is refused: its command and working directory cannot be checked, and a +# guard that waved it through would fall to one lone UTF-16 surrogate escape in the command. +if ! jq -e 'type == "object"' >/dev/null 2>&1 <<<"$input"; then + echo "BLOCKED: protect-main-branch could not read the tool payload (not parseable JSON, or not an object)," >&2 + echo "so it cannot tell whether this is a commit on main. A lone UTF-16 surrogate escape in the command does this — remove it and retry." >&2 + exit 2 +fi +``` + + +Edit `.claude/hooks/protect-main-branch.sh` — replace this text (exact; it occurs once): +```bash + echo "protect-main-branch: WARNING — $PROJECT_DIR is not a git repo; guard inactive for this call." >&2 + exit 0 +``` +with: +```bash + echo "protect-main-branch: WARNING — $PROJECT_DIR is not a git repo; guard inactive for this call." >&2 + echo " Backstop: [the base branch's ruleset — pull requests only — where one exists]." >&2 + exit 0 +``` + + +Edit `.claude/hooks/guard-pr-state.sh` — replace this text (exact; it occurs once): +```bash +# Fail-open on environment defects (missing jq), mirroring +# protect-lock-files.sh. +# Tier: ASK-GATE (see the registry comment in .claude/settings.json). +``` +with: +```bash +# Tier: ASK-GATE (see the registry comment in .claude/settings.json). +# Depends: jq (the payload) — absent, the guard fails open: exit 0 with a stderr +# warning that says no mechanical backstop exists. A payload jq cannot +# read is not an environment defect: it gets the prompt +# (cbk-conventions-reference.md § Hook authoring). +# Fixture: .claude/workflows/tests/hook-guards-fixture.sh. +``` + + +Edit `.claude/hooks/guard-pr-state.sh` — replace this text (exact; it occurs once): +```bash + echo " Until fixed, the only backstop is the prose rule in /finish and" >&2 + echo " /pr-respond that PR-state changes are the operator's calls." >&2 + exit 0 +fi +``` +with: +```bash + echo " Install jq to re-enable. Backstop until then: none mechanical — every" >&2 + echo " gh pr ready/merge/close/reopen needs the operator's explicit per-action OK." >&2 + exit 0 +fi + +# A payload jq cannot read gets the prompt, never a pass: its command cannot be checked, so the +# operator decides. +if ! jq -e 'type == "object"' >/dev/null 2>&1 <<<"$input"; then + cat <<'EOF' +{ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "ask", + "permissionDecisionReason": "guard-pr-state could not read this tool payload (not parseable JSON, or not an object), so it cannot tell whether the command changes a PR's state. Approve only if the operator asked for exactly this command." + } +} +EOF + exit 0 +fi +``` + + +Edit `.claude/hooks/require-knowledge-backend-ok.sh` — replace this text (exact; it occurs once): +```bash +# PreToolUse hook: force a per-action operator confirmation on every +# knowledge-backend WRITE tool call. The matcher in settings.json owns the +# tool-name pattern — the shipped regex covers Notion (the kit's v1 reference +# knowledge backend) mutating verbs (create/update/move/duplicate/convert/ +# delete) across both the direct mcp__notion__* and plugin-namespaced tool +# names; when the MCP grows a new mutating verb, widen the matcher — and +# adjust it to your configured MCP's tool names if your knowledge backend +# differs. Reads (fetch/search) are unmatched and unaffected. +``` +with: +```bash +# PreToolUse hook: force a per-action operator confirmation on every +# knowledge-backend WRITE tool call. The matcher in settings.json owns the +# tool-name pattern. The shipped regex covers the ten mutating verbs of Notion +# (the kit's v1 reference knowledge backend), across both the direct +# mcp__notion__* and plugin-namespaced tool names: create, update, move, +# duplicate, convert and delete, plus upload (upload-skill replaces a page's +# body), spawn and send (spawn-session and send-message-to-session drive an +# agent that writes) and stop (stop-session). The verb list is a dated +# observation of the vendor's tool names (2026-09-21): re-verify it when the +# MCP's tool list changes, widen the matcher for a new mutating verb, and +# adjust it to your configured MCP's tool names if your knowledge backend +# differs. Reads (fetch/search) are unmatched and unaffected. +``` + + +Edit `.claude/hooks/require-knowledge-backend-ok.sh` — replace this text (exact; it occurs once): +```bash +# Tier: ASK-GATE (see the registry comment in .claude/settings.json). + +set -uo pipefail +# Drain stdin first +``` +with: +```bash +# Tier: ASK-GATE (see the registry comment in .claude/settings.json). +# Depends: nothing — the decision is unconditional for a matched tool, so the +# payload is never parsed, and an unreadable one asks like any other +# (cbk-conventions-reference.md § Hook authoring). +# Fixture: .claude/workflows/tests/hook-guards-fixture.sh. + +set -uo pipefail +# Drain stdin first +``` + + +Edit `.claude/rules/cbk-conventions-reference.md` — replace this text (exact; it occurs once): +```markdown +a guard exits 0 with a stderr warning that names the surviving backstop — the CI lint, the Stop-tier hook, the verification block — never fails closed into a universal block. +``` +with: +```markdown +a guard exits 0 with a stderr warning that names the surviving backstop — the CI lint, the Stop-tier hook, the verification block — never fails closed into a universal block. **An unreadable payload is not an environment defect.** Input the guard cannot read — JSON jq cannot parse (a lone UTF-16 surrogate escape anywhere in the tool input does it) or a value that is not an object — cannot be checked, so a hard-deny guard refuses it (exit 2, saying why) and an ask-gate asks; waving it through would let one crafted character bypass the guard (context-builder-kit#60). A backstop only the project can supply — a CI lockfile check, the base branch's ruleset — ships as a bracketed slot in the warning, filled at scaffold's rule-file disposition pass; the project sub-block refuses a slot left unfilled. +``` + + +Edit `.claude/rules/cbk-conventions-reference.md` — replace this text (exact; it occurs once): +```markdown + echo "verification: project sub-block complete" +``` +with: +```markdown + # A hook's fail-open warning names the backstop that still stands; where that backstop is the project's own (a CI + # lockfile check, the base branch's ruleset) the kit ships a bracketed slot, filled at scaffold's rule-file + # disposition pass (§ Hook authoring). An unfilled slot prints a placeholder at the moment the guard is down. + absent grep -nE 'Backstops?( until then)?: \[' .claude/hooks/*.sh + + echo "verification: project sub-block complete" +``` + + +Edit `.claude/skills/scaffold/references/bootstrap_checklist_template.md` — replace this text (exact; it occurs once): +```markdown +- **Orchestration posture** recorded in `.claude/rules/orchestration.md` § The ceiling rule — which row the main loop runs by default (the exercised default is the workhorse tier) and what a deliberate escalation to the top tier looks like, dated with the reason. Decision: . +``` +with: +```markdown +- **Orchestration posture** recorded in `.claude/rules/orchestration.md` § The ceiling rule — which row the main loop runs by default (the exercised default is the workhorse tier) and what a deliberate escalation to the top tier looks like, dated with the reason. Decision: . +- **Hook backstop slots**: two guards' fail-open warnings name a backstop only the project can supply — `protect-lock-files.sh`'s `[the project's CI lockfile check …]`, and `protect-main-branch.sh`'s `[the base branch's ruleset …]` (in both of its warnings). Replace each bracket with the real check or ruleset, or with `none` and the reason. Decision: · . The project sub-block of the verification block refuses a slot left bracketed. +``` + + +Edit `.claude/rules/cbk-conventions-reference.md` — in § Verification, replace the kit sentinel line (exact; it occurs once): +```bash +echo "verification: kit sub-block complete" +``` +with (the new checks sit immediately before it): +```bash +# The four guards no other fixture drives by payload — the main-branch deny, the PR-state ask-gate, the lock-file +# deny (every named arm, the *.lock fallback, the non-lock neighbours) and the knowledge-backend ask-gate, found by +# its registry entry — on every branch their headers document: an unreadable payload refused by a deny and asked by +# an ask-gate, each fail-open warning naming what still stands, one deny per guard from a root containing a space +# (context-builder-kit#58 item 4, context-builder-kit#62). Needs git and jq; runs in throwaway trees. +bash .claude/workflows/tests/hook-guards-fixture.sh || { echo "a guard regressed on its fixture (protect-main-branch.sh / guard-pr-state.sh / protect-lock-files.sh / the knowledge-backend ask-gate)"; exit 1; } +echo "verification: kit sub-block complete" +``` + +- [ ] **Step 4: Run the fixture, `bash -n`, the slot check and the block** + +Run: `bash .claude/workflows/tests/hook-guards-fixture.sh; echo "exit=$?"` +Expected: `hook-guards-fixture: 64 cases ok`, `exit=0`. + +Run: `for h in protect-lock-files protect-main-branch guard-pr-state require-knowledge-backend-ok; do bash -n .claude/hooks/$h.sh || echo "SYNTAX: $h"; done; bash .claude/workflows/tests/hook-contract-fixture.sh` +Expected: `hook-contract-fixture: ok` and no `SYNTAX:` line. + +Run (the project-sub-block check bites on the unfilled kit tree, as it must in a target that skipped the slots): `grep -nE 'Backstops?( until then)?: \[' .claude/hooks/*.sh | cut -d: -f1,2` +Expected: `.claude/hooks/protect-lock-files.sh:43`, `.claude/hooks/protect-main-branch.sh:41`, `.claude/hooks/protect-main-branch.sh:82` (line numbers as measured in the scratch replay). + +Run (the knowledge-axis-`none` skip, in a scratch copy): +```bash +S=$(mktemp -d) && cp -a . "$S/kit" && cd "$S/kit" \ +&& jq '.hooks.PreToolUse |= map(select(.matcher | test("^mcp__") | not))' .claude/settings.json > "$S/s.json" && mv "$S/s.json" .claude/settings.json \ +&& rm .claude/hooks/require-knowledge-backend-ok.sh && bash .claude/workflows/tests/hook-guards-fixture.sh | tail -2; cd - >/dev/null; rm -rf "$S" +``` +Expected: `SKIP: the knowledge-backend ask-gate (no mcp__ matcher in settings.json — the knowledge axis is none)` then `hook-guards-fixture: 61 cases ok`. + +Run (V1's hand-off item 3 — Review Focus 4 keeps holding: a fresh scaffold is red until the disposition pass fills the slots, then prints all three sentinels. It uses V1.4's helper `${TMPDIR:-/tmp}/v1-fresh-scaffold.sh`; if it is gone, re-create it from V1.4 Step 1's `cat > … <<'EOF'` block first. The helper is not committed, and the slot lines stay in it for V5's re-run): +```bash +. "${TMPDIR:-/tmp}/v1-fresh-scaffold.sh"; fresh_scaffold yes +echo --- +h="${TMPDIR:-/tmp}/v1-fresh-scaffold.sh" +cat > "${TMPDIR:-/tmp}/v2-slots.txt" <<'EOF' + # The disposition pass fills the hook backstop slots (bootstrap checklist § 4, Hook backstop slots; V2.3). + sed -i -e "s/until then: \[the project's CI lockfile check — a/until then: the CI job's \`uv sync --locked\` — a/" -e 's/its manifest disagree\]\./its manifest disagree./' "$t/.claude/hooks/protect-lock-files.sh" + sed -i -e "s/until then: \[the base branch's ruleset —/until then: the base branch's ruleset —/" -e 's/where one exists\], and the operator/where one exists, and the operator/' -e "s/Backstop: \[the base branch's ruleset — pull requests only — where one exists\]\./Backstop: the base branch's ruleset (pull requests only)./" "$t/.claude/hooks/protect-main-branch.sh" +EOF +n=$(grep -n 'rules/testing.md"$' "$h" | cut -d: -f1); grep -q 'Hook backstop slots' "$h" || sed -i "${n}r ${TMPDIR:-/tmp}/v2-slots.txt" "$h" +. "$h"; fresh_scaffold yes +``` +Expected: `exit=1`, `verification: kit sub-block complete`, `verification: block exited 1` (the project sub-block's slot check), then `---`, then `exit=0`, `verification: kit sub-block complete`, `verification: project sub-block complete`, `verification: done`. Every V2 fixture this task and V2.1–V2.2 added runs green inside the scaffolded target too. + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | grep -E 'hook-guards-fixture|^verification: (kit sub-block complete|done)$|^verification: block exited'; echo "exit=${PIPESTATUS[0]}"` +Expected: `hook-guards-fixture: 64 cases ok`, `verification: kit sub-block complete`, `verification: done`, `exit=0`. + +- [ ] **Step 5: Commit** + +```bash +git add .claude/workflows/tests/hook-guards-fixture.sh .claude/hooks/protect-lock-files.sh .claude/hooks/protect-main-branch.sh .claude/hooks/guard-pr-state.sh .claude/hooks/require-knowledge-backend-ok.sh .claude/rules/cbk-conventions-reference.md .claude/skills/scaffold/references/bootstrap_checklist_template.md +git commit -q -F - <<'MSG' +fix(V2): every fail-open names its backstop; an unreadable payload is refused or asked + +The lock-file, main-branch and PR-state guards failed open on a payload jq cannot +parse (one lone UTF-16 surrogate escape); the two denies now refuse it and the +ask-gate asks, and section Hook authoring says why an unreadable payload is not an +environment defect. Each fail-open warning names what still stands: a bracketed +slot for the project's CI lockfile check and for the base branch's ruleset, filled +at scaffold's rule-file disposition pass (a new bootstrap row) and refused unfilled +by the project sub-block; the PR-state gate says no mechanical backstop exists. +The lock-file fallback's remediation names the real route, a case arm above the +*.lock) arm. The knowledge-backend ask-gate's header carries its ten verbs, and +protect-main-branch.sh gains the Depends: line every header carries. +hook-guards-fixture.sh drives every branch of the four guards, finds the ask-gate +through the registry, and runs one deny per guard from a root containing a space. + +Red first: FAIL: the non-checkout warning names a backstop (stderr lacks 'Backstop') + +Trace: #62/body/lock-files, #62/body/pr-state, #62/body/main-branch, +#58/c5901493591/R8, #58/c5901493591/R1 (the four guards), critic/8, +#66/body/2 (the ask-gate header), release/5 (protect-lock-files.sh). +Decisions: D61, D44. + +Co-Authored-By: Claude Opus 5.5 +MSG +``` +### Task V2.4: The main-branch and PR-state patterns anchor on non-word characters (review/security/20, #68/body/1a (the guard's remediation); D44) + +**Files:** +- Modify: `.claude/workflows/tests/hook-guards-fixture.sh` (two insertions) +- Modify: `.claude/hooks/protect-main-branch.sh` (header `Blocked:`/`Allowed:` lines 10–14, the pattern comment at lines 52–59, the pattern at line 62, the remediation at lines 78–79) +- Modify: `.claude/hooks/guard-pr-state.sh` (header, the pattern comment at lines 37–39, the pattern at line 42) + +**Interfaces:** +- Produces: the two patterns `(^|[^[:alnum:]_.-])git([[:space:]]+[^[:space:]]+)*[[:space:]]+commit([^[:alnum:]_.-]|$)` and `(^|[^[:alnum:]_.-])gh([[:space:]]+[^[:space:]]+)*[[:space:]]+pr([[:space:]]+[^[:space:]]+)*[[:space:]]+(ready|merge|close|reopen)([^[:alnum:]_.-]|$)`, and a `Not seen:` line in each header naming what a pattern guard cannot see. +- Produces: the remediation comments `# an issue this PR closes` and `# work no issue tracks` (#68/body/1a's echo site in this hook; V9 lands the rule in § Branch naming). The block's `grep -q 'short-slug'` on this hook stays green. +- Consumes: `hook-guards-fixture.sh` from V2.3. + +- [ ] **Step 1: Extend the fixture with the bypass spellings** + +Edit `.claude/workflows/tests/hook-guards-fixture.sh` — replace this text (exact; it occurs once): +```bash +run "$MAIN" "$(bash_payload 'git commit' "$d/on-main")"; want 2 "a bare commit alone is a commit" +``` +with: +```bash +run "$MAIN" "$(bash_payload 'git commit' "$d/on-main")"; want 2 "a bare commit alone is a commit" +# Both anchors are negated classes: a commit reached through a subshell, a quoted `-c` string, an absolute path or a +# trailing separator is still a commit; `commit-tree` and a `commit.*` key are not. +for c in 'bash -c "git commit -m x"' "sh -c 'git commit -m x'" '(git commit -m x)' '$(git commit -m x)' \ + '`git commit -m x`' '/usr/bin/git commit -m x' 'git commit;' 'git commit&&true' 'true;git commit'; do + run "$MAIN" "$(bash_payload "$c" "$d/on-main")"; want 2 "a commit on main is denied: $c" +done +for c in 'git commit-tree HEAD^{tree} -m x' 'git -c commit.gpgsign=false log' 'git config commit.gpgsign true' 'git log --grep=commit'; do + run "$MAIN" "$(bash_payload "$c" "$d/on-main")"; want 0 "not a commit: $c" +done +``` + + +Edit `.claude/workflows/tests/hook-guards-fixture.sh` — replace this text (exact; it occurs once): +```bash + run "$PRS" "$(bash_payload "$c" "$d/on-feat")"; asks "a PR-state change asks: $c" +done +for c in 'gh pr create --draft' +``` +with: +```bash + run "$PRS" "$(bash_payload "$c" "$d/on-feat")"; asks "a PR-state change asks: $c" +done +for c in 'bash -c "gh pr merge 5"' '(gh pr merge 5)' '$(gh pr merge 5)' '/usr/bin/gh pr merge 5' '(gh pr merge)' 'gh pr close 5;echo done'; do + run "$PRS" "$(bash_payload "$c" "$d/on-feat")"; asks "a PR-state change asks: $c" +done +for c in 'gh pr view 7 --json mergeable' 'gh pr list --state merged'; do + run "$PRS" "$(bash_payload "$c" "$d/on-feat")"; silent "no decision for: $c" +done +for c in 'gh pr create --draft' +``` + + +- [ ] **Step 2: Run it against the unfixed patterns** + +Run: `bash .claude/workflows/tests/hook-guards-fixture.sh; echo "exit=$?"` +Expected: +```text +FAIL: a commit on main is denied: bash -c "git commit -m x" (want exit 2, got 0) +exit=1 +``` +Record the FAIL line. The PR-state half is red on its own too: with the main-branch pattern fixed and the PR-state one not, the fixture stops at `FAIL: a PR-state change asks: bash -c "gh pr merge 5" (want an ask decision on stdout, exit 0; got exit 0)` (measured in the scratch replay); record it as the second line. + +- [ ] **Step 3: Harden both patterns and name what they cannot see** + +No platform fact is stated; the anchors' behaviour is measured by the fixture's cases. + +Edit `.claude/hooks/protect-main-branch.sh` — replace this text (exact; it occurs once): +```bash +# Blocked: Bash tool calls whose command contains a token-anchored `git` +# followed (any number of option tokens later) by a `commit` token, +# while the call's working directory is on main or master. +# Allowed: everything else — commits on feature branches, and all +# non-commit git commands on main. +``` +with: +```bash +# Blocked: Bash tool calls whose command contains `git` — at the start, or +# after any character that cannot continue a word (a space, `;`, `&`, +# `|`, `(`, `$(`, a backtick, a quote, the `/` of /usr/bin/git) — +# followed, any number of option tokens later, by a `commit` token that +# no word character, `.` or `-` continues, while the call's working +# directory is on main or master; and a payload jq cannot read. +# Allowed: everything else — commits on feature branches, non-commit git +# commands on main, `git commit-tree`, and a `commit.*` config key. +# Not seen: a commit the command spells indirectly — `eval`, a variable holding +# `git`, a git alias, a script that commits. A pattern guard reads the +# text, not what runs; the base branch's ruleset, where one exists, +# refuses such a commit when it is pushed. +``` + + +Edit `.claude/hooks/protect-main-branch.sh` — replace this text (exact; it occurs once): +```bash +# operator can override by running the commit themselves; under-matching +# would be a silent bypass, which is worse for a deny-tier guard. +``` +with: +```bash +# operator can override by running the commit themselves; under-matching +# would be a silent bypass, which is worse for a deny-tier guard. Both anchors +# are negated classes: `git` may follow anything that cannot continue a word, +# so `bash -c "git commit …"`, `(git commit …)`, `$(git commit …)` and +# `/usr/bin/git commit` are caught; `commit` may be followed by anything but a +# word character, `.` or `-`, so `git commit;` and `git commit&&…` are caught +# while `git commit-tree` and `git config commit.gpgsign …` are not (each +# measured 2026-09-30; the fixture holds the cases). +``` + + +Edit `.claude/hooks/protect-main-branch.sh` — replace this text (exact; it occurs once): +```bash +if ! grep -Eq '(^|[;&|[:space:]])git([[:space:]]+[^[:space:]]+)*[[:space:]]+commit([[:space:]]|$)' <<<"$command"; then +``` +with: +```bash +if ! grep -Eq '(^|[^[:alnum:]_.-])git([[:space:]]+[^[:space:]]+)*[[:space:]]+commit([^[:alnum:]_.-]|$)' <<<"$command"; then +``` + + +Edit `.claude/hooks/protect-main-branch.sh` — replace this text (exact; it occurs once): +```bash + git switch -c /-- # cascade issue + git switch -c / # operator-directed maintenance, no issue +``` +with: +```bash + git switch -c /-- # an issue this PR closes + git switch -c / # work no issue tracks +``` + + +Edit `.claude/hooks/guard-pr-state.sh` — replace this text (exact; it occurs once): +```bash +# Tier: ASK-GATE (see the registry comment in .claude/settings.json). +# Depends: +``` +with: +```bash +# Not seen: a PR-state change the command spells indirectly — `eval`, a +# variable holding `gh`, a `gh` alias, `gh api` against the pulls +# endpoint — and one made in the web UI. No mechanical backstop exists: +# the per-action OK is the operator's. +# Tier: ASK-GATE (see the registry comment in .claude/settings.json). +# Depends: +``` + + +Edit `.claude/hooks/guard-pr-state.sh` — replace this text (exact; it occurs once): +```bash +# command) costs one extra confirmation — acceptable for an ask-gate. +``` +with: +```bash +# command) costs one extra confirmation — acceptable for an ask-gate. The +# anchors are negated classes, as in protect-main-branch.sh: `gh` may follow +# anything that cannot continue a word (`bash -c "gh pr merge 5"`, +# `(gh pr merge 5)`, `$(gh pr merge 5)`, `/usr/bin/gh`), and the verb anything +# but a word character, `.` or `-` (measured 2026-09-30; the fixture holds the cases). +``` + + +Edit `.claude/hooks/guard-pr-state.sh` — replace this text (exact; it occurs once): +```bash +if grep -Eq '(^|[;&|[:space:]])gh([[:space:]]+[^[:space:]]+)*[[:space:]]+pr([[:space:]]+[^[:space:]]+)*[[:space:]]+(ready|merge|close|reopen)([[:space:]]|$|[;&|])' <<<"$command"; then +``` +with: +```bash +if grep -Eq '(^|[^[:alnum:]_.-])gh([[:space:]]+[^[:space:]]+)*[[:space:]]+pr([[:space:]]+[^[:space:]]+)*[[:space:]]+(ready|merge|close|reopen)([^[:alnum:]_.-]|$)' <<<"$command"; then +``` + + +- [ ] **Step 4: Run the fixture, `bash -n`, the contract fixture and the block** + +Run: `bash .claude/workflows/tests/hook-guards-fixture.sh; echo "exit=$?"` +Expected: `hook-guards-fixture: 85 cases ok`, `exit=0`. + +Run: `bash -n .claude/hooks/protect-main-branch.sh && bash -n .claude/hooks/guard-pr-state.sh && bash .claude/workflows/tests/hook-contract-fixture.sh` +Expected: `hook-contract-fixture: ok` (the over-buffer commit and merge probes still bite). + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | grep -E 'hook-guards-fixture|^verification: (kit sub-block complete|done)$|^verification: block exited'; echo "exit=${PIPESTATUS[0]}"` +Expected: `hook-guards-fixture: 85 cases ok`, `verification: kit sub-block complete`, `verification: done`, `exit=0`. + +- [ ] **Step 5: Commit** + +```bash +git add .claude/workflows/tests/hook-guards-fixture.sh .claude/hooks/protect-main-branch.sh .claude/hooks/guard-pr-state.sh +git commit -q -F - <<'MSG' +fix(V2): the main-branch and PR-state patterns anchor on non-word characters + +The left anchor admitted only a start, a space or ;&|, so a commit on main passed +the hard-deny inside bash -c "…", sh -c '…', a subshell, $(…), backticks or through +/usr/bin/git, and the right anchor let git commit; and git commit&&… through; the +PR-state ask-gate shared the left anchor. Both anchors are now negated classes: +anything that cannot continue a word before, anything but a word character, . or +- after, so git commit-tree and git config commit.gpgsign stay allowed. Each +header's Not seen: line names what a pattern guard cannot see (eval, a variable, an +alias). The remediation's comments say "an issue this PR closes" and "work no issue +tracks". The fixture gains every spelling, both ways. + +Red first: FAIL: a commit on main is denied: bash -c "git commit -m x" (want exit 2, got 0) + +Trace: review/security/20, #68/body/1a (the guard's remediation). +Decision: D44. + +Co-Authored-By: Claude Opus 5.5 +MSG +``` +### Task V2.5: The launch-root guard and the fork detector, by payload — and probe P1 (#58/c5901493591/R1 (the two hooks), #58/c5901493591/R6, #58/c5901493591/R13, #73/body/2 (the WARNING pins), release/5 (the two hooks); D61, D44; probe P1) + +**Files:** +- Create: `.claude/workflows/tests/hook-payloads-fixture.sh` (mode 755) +- Modify: `.claude/hooks/require-repo-root-for-agents.sh` (header `Blocked:` line 16–17, `Timing:` lines 25–40, `Depends:` lines 46–49; a refusal after the jq check at line 68) +- Modify: `.claude/hooks/detect-forked-agent-memory.sh` (six bare citations; rule 1 at lines 100–103) +- Modify: `.claude/hooks/protect-main-branch.sh` (the cwd comment at lines 43–45) +- Modify: `.claude/rules/cbk-conventions-reference.md` — § Hook authoring's Header bullet (the `Timing:` clause) and § Verification (the kit sentinel) + +**Interfaces:** +- Produces: `.claude/workflows/tests/hook-payloads-fixture.sh`, asserting the literal `WARNING` on every could-not-look branch of the detector (outside a checkout, a partial scan) — the contract V1's `stop_hook_clean` reads — and on the launch-root guard's non-checkout branch. +- Produces: P1's answer, written into three places in one variant: the launch-root guard's `Timing:` paragraph, the main-branch guard's cwd comment, and § Hook authoring's Header bullet. V10's CHANGELOG sync note states the variant's behaviour. +- Consumes: V1's Stop-hook check (unchanged here); nothing in this task changes the detector's messages. + +- [ ] **Step 1: Write the failing fixture** + +Create `.claude/workflows/tests/hook-payloads-fixture.sh`: +```bash +#!/usr/bin/env bash +# Behavioural cases for the two hooks hook-contract-fixture.sh covers only structurally or in part: the launch-root +# guard (require-repo-root-for-agents.sh, PreToolUse, HARD-DENY) and the forked-memory detector +# (detect-forked-agent-memory.sh, Stop). Every branch each documents in its `Blocked:` / `Allowed:` header is +# exercised with a crafted payload and an asserted exit — § Hook authoring › Verify by payload, made durable +# (context-builder-kit#58 items 1, 2 and 4). Neither hook sources a helper. Every could-not-look branch of the +# detector prints the literal WARNING the verification block's Stop-hook check reads; the cases assert it. +# Two fail-open branches are reachable only by mutation, not by payload (the guard's race between its git call and +# its `cd`; the detector's `cd` failure) — stated here rather than claimed as payload coverage. +# Every case runs against throwaway `git init` trees under mktemp, never the real repository, and never depends on +# the directory the fixture is launched from; the hooks run from their real path, so a change to either is caught. +# Needs bash, git and jq; `find`, `sed`, `sort` and `mktemp` for the detector. The partial-scan case needs a +# directory the walk cannot read, which root can read anyway: run as root it prints a SKIP line. +# Run by the verification block; also: bash .claude/workflows/tests/hook-payloads-fixture.sh +set -euo pipefail +here=$(cd "$(dirname "$0")" && pwd) +hooks="$here/../../hooks" +GUARD="$hooks/require-repo-root-for-agents.sh" +STOP="$hooks/detect-forked-agent-memory.sh" +[ -x "$GUARD" ] && [ -x "$STOP" ] || { echo "hook-payloads-fixture: a hook is missing or not executable"; exit 1; } +command -v jq >/dev/null && command -v git >/dev/null || { echo "hook-payloads-fixture: needs jq and git"; exit 1; } +d=$(mktemp -d) +cleanup() { chmod -R u+rwX "$d" 2>/dev/null || true; rm -rf "$d"; } +trap cleanup EXIT +# Git discovery stops at the throwaway root (the ceiling is its parent), so a TMPDIR inside a work tree cannot turn +# the outside-a-checkout cases into inside ones. +export GIT_CEILING_DIRECTORIES="${d%/*}" + +# A throwaway checkout whose ignore rules name one build directory and one tool cache, so the ignore-driven prune has +# something to prune that is not hard-coded anywhere. +repo="$d/repo"; mkdir -p "$repo" +git -c init.defaultBranch=main init -q "$repo" +ignore() { printf '%b' "$1" > "$repo/.gitignore"; } +ignore '/pkg/*/build/\n/pkg/*/.cache/\n' +mkdir -p "$repo/pkg/a" "$repo/docs" "$repo/.claude/agent-memory/reviewer" +spaced="$d/a b"; mkdir -p "$spaced/docs"; git -c init.defaultBranch=main init -q "$spaced" + +n=0; RC=0; ERR="" +guard() { RC=0; ERR="$(printf '%s' "$1" | "$GUARD" 2>&1 >/dev/null)" || RC=$?; n=$((n + 1)); } +guard_in() { RC=0; ERR="$(cd "$1" && printf '%s' "$2" | "$GUARD" 2>&1 >/dev/null)" || RC=$?; n=$((n + 1)); } +stop() { RC=0; ERR="$(printf '%s' "$2" | CLAUDE_PROJECT_DIR="$1" "$STOP" 2>&1 >/dev/null)" || RC=$?; n=$((n + 1)); } +want() { [ "$RC" -eq "$1" ] || { echo "FAIL: $2 (want exit $1, got $RC)"; [ -n "$ERR" ] && printf ' stderr: %s\n' "$ERR"; exit 1; }; } +says() { grep -q -- "$1" <<<"$ERR" || { echo "FAIL: $2 (stderr does not carry '$1')"; printf ' stderr: %s\n' "$ERR"; exit 1; }; } +payload() { jq -cn --arg t "$1" --arg cwd "$2" '{tool_name:$t, tool_input:{}, cwd:$cwd}'; } + +# ── the launch-root guard ── +guard "$(payload Agent "$repo")"; want 0 "Agent from the checkout root is allowed" +guard "$(payload Agent "$repo/docs")"; want 2 "Agent from a subdirectory is denied" +guard "$(payload Task "$repo/docs")"; want 2 "Task (the older tool name) from a subdirectory is denied" +guard "$(payload Workflow "$repo/docs")"; want 2 "Workflow from a subdirectory is denied" +guard "$(payload Bash "$repo/docs")"; want 0 "a non-matching tool passes through" +guard "$(payload Agent "$repo/docs")"; says "BLOCKED" "the denial names itself" +says "$repo" "the denial names the root to return to" +guard "$(payload Agent "$spaced")"; want 0 "Agent from a root containing a space is allowed" +guard "$(payload Agent "$spaced/docs")"; want 2 "Agent from a subdirectory of a root containing a space is denied" +# Canonicalization: a symlinked or trailing-slash spelling of the root IS the root (a false HARD-DENY otherwise). +ln -s "$repo" "$d/rootlink" +guard "$(payload Agent "$d/rootlink")"; want 0 "a symlink to the root is not a false mismatch" +guard "$(payload Agent "$repo/")"; want 0 "a trailing slash on the root is not a false mismatch" +guard "$(payload Agent "$d/rootlink/docs")"; want 2 "a symlinked subdirectory is still denied" +# The cwd fallback resolves against the PROCESS's directory, asserted from a controlled directory both ways. +guard_in "$repo" '{"tool_name":"Agent","tool_input":{}}'; want 0 "no cwd field: falls back to \$PWD, allowed at the root" +guard_in "$repo/docs" '{"tool_name":"Agent","tool_input":{}}'; want 2 "no cwd field: falls back to \$PWD, denied in a subdirectory" +# Fail-open branches: each exits 0, warns, and names a surviving backstop. +guard "$(payload Agent "$d")"; want 0 "a cwd outside any checkout fails open" +says "WARNING" "the non-checkout warning is a WARNING"; says "not inside a git checkout" "the non-checkout warning says why" +says "Backstop:" "the non-checkout warning names a backstop" +bin="$d/bin"; mkdir -p "$bin" +for t in bash git cat printf sed dirname basename find grep sort head mktemp rm; do + p=$(type -P "$t" || true); [ -n "$p" ] && ln -sf "$p" "$bin/$t" +done +RC=0; ERR="$(printf '%s' "$(payload Agent "$repo/docs")" | env -i PATH="$bin" HOME="$HOME" bash "$GUARD" 2>&1 >/dev/null)" || RC=$?; n=$((n + 1)) +want 0 "the guard fails open when jq is absent" +says "jq not installed" "the jq warning says why"; says "Backstop:" "the jq warning names a backstop" +# Input it cannot read is refused: the guard cannot tell where the dispatch launches from. +guard "$(printf '{"tool_name":"Agent","tool_input":{"prompt":"x\\ud800y"},"cwd":"%s"}' "$repo")"; want 2 "a payload jq cannot parse is refused" +says "could not read the tool payload" "the refusal says why" +guard '["Agent"]'; want 2 "a payload that is not an object is refused" + +# ── the forked-memory detector ── +stop "$repo" '{"stop_hook_active":false}'; want 0 "a clean tree lets the stop proceed" +stop "$repo" '{}'; want 0 "the root's own agent-memory tree is not a fork" +mkdir -p "$repo/pkg/a/.claude/agent-memory/reviewer" +stop "$repo" '{}'; want 2 "a stray agent-memory under a package blocks the stop" +says "BLOCKED" "the block names itself" +says "pkg/a/.claude/agent-memory" "the block names the stray path" +stop "$repo" '{"stop_hook_active": true}'; want 0 "a second stop after one block proceeds" +says "still present after one fix attempt" "the second stop warns instead of blocking" +rm -rf "$repo/pkg/a/.claude" +mkdir -p "$repo/pkg/a/.claude/agent-memory-local/reviewer" +stop "$repo" '{}'; want 2 "the local memory scope forks the same way" +says "agent-memory-local" "the block names the local-scope path" +rm -rf "$repo/pkg/a/.claude" +# A worktree is its own checkout with its own root tree, so its memory is not a fork. +mkdir -p "$repo/.claude/worktrees/wt/.claude/agent-memory/reviewer" +stop "$repo" '{}'; want 0 "a worktree's own agent-memory tree is pruned" +rm -rf "$repo/.claude/worktrees" +# The prune list is the project's own ignore rules, read at run time: both ignored directories are pruned, an +# unignored one is not. +mkdir -p "$repo/pkg/a/build/agent-memory" "$repo/pkg/a/.cache/agent-memory" +stop "$repo" '{}'; want 0 "trees under ignored directories are pruned" +mkdir -p "$repo/pkg/a/vendored/agent-memory" +stop "$repo" '{}'; want 2 "a tree under an untracked but UNignored directory is still found" +says "vendored/agent-memory" "the block names the unignored stray path" +rm -rf "$repo/pkg/a/vendored" "$repo/pkg/a/build" "$repo/pkg/a/.cache" +# An ignore-driven prune list is attacker-shaped input; three rules keep it safe, each against a tree that would +# defeat a naive splice. +mkdir -p "$repo/pkg/a/.claude/agent-memory/reviewer" +( cd "$repo" && mkdir -p -- '*' ) +ignore '\\*/\n' +stop "$repo" '{}'; want 2 "an ignored directory named '*' prunes itself, not the whole walk" +rm -rf "${repo:?}/*" +ignore 'agent-memory/\n' +stop "$repo" '{}'; want 2 "an ignore rule naming the memory tree never hides it" +ignore '.claude/\n' +stop "$repo" '{}'; want 2 "an ignore rule naming .claude never hides a fork inside it" +ignore 'reviewer/\n' +stop "$repo" '{}'; want 2 "a collapsed ancestor that no rule names is still walked" +ignore '/pkg/*/build/\n/pkg/*/.cache/\n' +rm -rf "$repo/pkg/a/.claude" +# The scan is rooted at the checkout's top level, so a project dir pointing at a subdirectory still finds a fork. +mkdir -p "$repo/pkg/a/.claude/agent-memory/reviewer" +stop "$repo/docs" '{}'; want 2 "a subdirectory project dir still scans from the top level" +# No jq dependency: the detector backstops exactly the case where the jq-dependent guards have failed open. +RC=0; ERR="$(printf '{}' | env -i PATH="$bin" HOME="$HOME" CLAUDE_PROJECT_DIR="$repo" bash "$STOP" 2>&1 >/dev/null)" || RC=$?; n=$((n + 1)) +want 2 "the detector blocks without jq on PATH" +rm -rf "$repo/pkg/a/.claude" +stop "$d" '{}'; want 0 "a project dir outside any checkout fails open" +says "WARNING" "the non-checkout warning is a WARNING"; says "not inside a git checkout" "the non-checkout warning says why" +says "Backstop:" "the non-checkout warning names a backstop" +# A directory the walk cannot read makes the scan partial; reporting "clean" there is the silent miss the hook exists +# to stop, so it warns. Root ignores mode bits, so the case cannot be staged there. +if [ "$(id -u)" -ne 0 ]; then + mkdir -p "$repo/pkg/a/sealed/inner"; chmod 000 "$repo/pkg/a/sealed" + stop "$repo" '{}'; want 0 "an unreadable directory does not fake a block" + says "WARNING" "a partial scan is a WARNING"; says "the scan was partial" "a partial scan is reported, never silently clean" + chmod 755 "$repo/pkg/a/sealed"; rm -rf "$repo/pkg/a/sealed" +else + echo "SKIP: the detector's partial-scan case (an unreadable directory unavailable: running as root)" +fi +echo "hook-payloads-fixture: $n cases ok" +``` + +Run: `chmod 755 .claude/workflows/tests/hook-payloads-fixture.sh` + +- [ ] **Step 2: Run it against the unfixed guard, and run probe P1** + +Run: `bash .claude/workflows/tests/hook-payloads-fixture.sh; echo "exit=$?"` +Expected: +```text +FAIL: a payload jq cannot parse is refused (want exit 2, got 0) + stderr: jq: parse error: Invalid \uXXXX\uXXXX surrogate pair escape at line 1, column 54 +exit=1 +``` +(the column varies with the temp path). Record the FAIL line. Every earlier case passes on the unfixed hooks: they pin behaviour the two hooks already have, including the partial-scan warning R6 found unpinned. + +**Probe P1 — a hook payload's `cwd` after the Bash tool runs `cd`.** The hooks page says the field is "the new directory after Claude runs `cd`"; harvest 3 measured the opposite (2026-09-07), and the launch-root guard's header rests on that measurement. Run, from the repository root, in a throwaway repository (it spends one short Haiku session and a subagent): +```bash +curl -sL https://code.claude.com/docs/en/hooks.md | grep -cF 'the new directory after Claude runs `cd`' +claude --version +P=$(mktemp -d)/p1 && mkdir -p "$P/docs" && git -c init.defaultBranch=main init -q "$P" \ +&& git -C "$P" -c user.email=p@x -c user.name=p -c commit.gpgsign=false commit -q --allow-empty -m init +cat > "$P/log-cwd.sh" <> "$P/cwd.log" +exit 0 +EOF +chmod +x "$P/log-cwd.sh" +jq -n --arg c "$P/log-cwd.sh" '{hooks: {PreToolUse: [{matcher: "Bash|Agent|Task", hooks: [{type: "command", command: $c}]}]}}' > "$P/p1-settings.json" +(cd "$P" && timeout 300 claude -p --model haiku --settings "$P/p1-settings.json" \ + --allowedTools 'Bash(pwd)' 'Bash(cd docs)' 'Agent' --max-turns 8 \ + "Make exactly these tool calls, one at a time, in order: a Bash call running pwd; a Bash call running cd docs; a Bash call running pwd; then one Agent call whose task is to reply with the single word done. Then reply with the word finished." < /dev/null) +cat "$P/cwd.log" +``` +Expected: `1` (the page's `cwd follows Claude` bullet carries the phrase), the CLI version, `finished`, and four log lines — Bash `pwd`, Bash `cd docs`, Bash `pwd`, then `Agent`. If the count is `0`, stop: the quotation in the texts below cannot be written. + +**Decision rule.** Read the `cwd` of the first log line after the `cd docs` line and of the `Agent` line: +- both end in `/p1/docs` → **variant B** (the field follows `cd`; the guard now denies a dispatch made after a `cd` into a subdirectory); +- both end in `/p1` → **variant A** (the field stays at the launch directory, as measured 2026-09-07); +- they disagree → stop and report both lines to the operator; neither text below is true. + +Two dry runs of this procedure on 2026-09-30, Claude Code 2.1.285 (the drafter's, and the plan verifier's independent re-run), logged `…/p1/docs` for both: **variant B**. Apply the variant your own run shows. In the chosen variant's texts, replace `2.1.285` with the version `claude --version` printed and `2026-09-30` with the run date, if they differ. Record the four log lines, the version and the variant in the PR body under P1. + +**Under variant B, this session is guarded too.** The kit's `settings.json` registers the working-tree hook, so from this commit on an Agent or Workflow dispatch made while the Bash tool's directory is not the repository root is denied, in the executor's own session as well (F2's review sweep included). Every step in these cluster files that enters a scratch copy returns with `cd -`; run `cd "$(git rev-parse --show-toplevel)"` as its own command before any dispatch. + +- [ ] **Step 3: Refuse an unreadable payload, write P1's answer, correct rule 1** + +The launch-root guard's other three edits (the same in both variants): +Edit `.claude/hooks/require-repo-root-for-agents.sh` — replace this text (exact; it occurs once): +```bash +# Blocked: Task, Agent and Workflow tool calls whose working directory is not +# the git top-level of the checkout it sits in. +``` +with: +```bash +# Blocked: Task, Agent and Workflow tool calls whose working directory is not +# the git top-level of the checkout it sits in; and a payload jq cannot +# read, whose working directory cannot be checked. +``` + + +Edit `.claude/hooks/require-repo-root-for-agents.sh` — replace this text (exact; it occurs once): +```bash +# naming the backstop, detect-forked-agent-memory.sh (Stop tier, +# needs no jq). +``` +with: +```bash +# naming the backstop, detect-forked-agent-memory.sh (Stop tier, +# needs no jq). A payload jq cannot read is not an environment defect: +# it is refused (cbk-conventions-reference.md § Hook authoring). +# Fixture: .claude/workflows/tests/hook-payloads-fixture.sh. +``` + + +Edit `.claude/hooks/require-repo-root-for-agents.sh` — replace this text (exact; it occurs once): +```bash + echo " catches a stray agent-memory tree before the hand-off; git status shows it untracked." >&2 + exit 0 +fi +``` +with: +```bash + echo " catches a stray agent-memory tree before the hand-off; git status shows it untracked." >&2 + exit 0 +fi + +# A payload jq cannot read is refused: this hook runs on Task|Agent|Workflow only, so the call is +# still a dispatch, and where it launches from cannot be checked. +if ! jq -e 'type == "object"' >/dev/null 2>&1 <<<"$input"; then + echo "BLOCKED: require-repo-root-for-agents could not read the tool payload (not parseable JSON, or not an object)," >&2 + echo "so it cannot tell where this dispatch launches from. A lone UTF-16 surrogate escape in the tool input does this — remove it and retry." >&2 + exit 2 +fi +``` + + +**Variant B** (the field follows `cd`) — three replacements: + +Edit `.claude/hooks/require-repo-root-for-agents.sh` — replace this text (exact; it occurs once): +```bash +# Timing: the guard reads the payload's `cwd` (a common field on every hook +# event — https://code.claude.com/docs/en/hooks-guide § How hooks +# work) BEFORE the tool runs. That field is the SESSION's working +# directory — where Claude Code was launched — not the Bash tool's +# persisted shell directory: a real dispatch made after `cd docs` in +# the shell was allowed, with the payload cwd still at the root (dated +# observation, 2026-09-07). The confirmed deny is a session launched +# from a subdirectory (the verification block's payload dry-run); the +# remedy there is to relaunch the session from the root. The hooks +# reference states the OPPOSITE of the observation above — "cwd follows +# Claude: … the new directory after Claude runs cd" +# (https://code.claude.com/docs/en/hooks § Reference scripts by path, +# read 2026-09-07). The two disagree; this guard judges whichever cwd +# the payload carries and is correct under either reading. RE-VERIFY +# TRIGGER: a dispatch made after `cd ` that is denied means the +# page's reading now holds — update this paragraph (#58, S3). +``` +with: +```bash +# Timing: the guard reads the payload's `cwd` (a common field on every hook +# event — https://code.claude.com/docs/en/hooks-guide § How hooks +# work) BEFORE the tool runs. That field follows the Bash tool's `cd` +# — "the new directory after Claude runs `cd`" +# (https://code.claude.com/docs/en/hooks § Reference scripts by path, +# read 2026-09-30) — and a logging-hook probe on Claude Code 2.1.285 +# showed it (2026-09-30): a Bash call and an Agent dispatch made after +# `cd docs` both carried /docs. So a dispatch made after a `cd` +# into a subdirectory is denied, and the remedy is to `cd` back to the +# root as its own command. The 2026-09-07 observation that the field +# stayed at the session's launch directory is retired +# (context-builder-kit#58, S3). RE-VERIFY TRIGGER: a dispatch made after +# `cd ` that is allowed means the field stopped following `cd` — +# re-run the probe and update this paragraph. +``` + +Edit `.claude/hooks/protect-main-branch.sh` — replace this text (exact; it occurs once): +```bash +# The Bash tool's payload carries the call's working directory; the branch +# check must use it (not a fixed project dir), or a commit run from a +# worktree / nested repo is judged against the wrong repo's branch. +``` +with: +```bash +# The payload's `cwd` is the directory the Bash tool is in — it follows `cd` +# (probe, 2026-09-30; require-repo-root-for-agents.sh § Timing) — so a commit +# run from a worktree or a nested repo is judged against that checkout's +# branch, not a fixed project dir. Precedence: cwd, CLAUDE_PROJECT_DIR, $PWD. +``` + +Edit `.claude/rules/cbk-conventions-reference.md` — in § Hook authoring's Header bullet, replace this text (exact; it occurs once): +```markdown +and the payload `cwd` is the session's launch directory, which a shell `cd` does not move — a real dispatch, 2026-09-07 — so a launch-directory guard is answered by relaunching the session from the root) +``` +with: +```markdown +and the payload `cwd` follows the Bash tool's `cd` — "the new directory after Claude runs `cd`" (`https://code.claude.com/docs/en/hooks`, read 2026-09-30; a logging-hook probe on Claude Code 2.1.285, 2026-09-30, retired the 2026-09-07 observation that it did not) — so a launch-directory guard is answered by returning to the root as its own command) +``` + + +**Variant A** (the field stays at the launch directory) — three replacements instead: + +Edit `.claude/hooks/require-repo-root-for-agents.sh` — replace this text (exact; it occurs once): +```bash +# Timing: the guard reads the payload's `cwd` (a common field on every hook +# event — https://code.claude.com/docs/en/hooks-guide § How hooks +# work) BEFORE the tool runs. That field is the SESSION's working +# directory — where Claude Code was launched — not the Bash tool's +# persisted shell directory: a real dispatch made after `cd docs` in +# the shell was allowed, with the payload cwd still at the root (dated +# observation, 2026-09-07). The confirmed deny is a session launched +# from a subdirectory (the verification block's payload dry-run); the +# remedy there is to relaunch the session from the root. The hooks +# reference states the OPPOSITE of the observation above — "cwd follows +# Claude: … the new directory after Claude runs cd" +# (https://code.claude.com/docs/en/hooks § Reference scripts by path, +# read 2026-09-07). The two disagree; this guard judges whichever cwd +# the payload carries and is correct under either reading. RE-VERIFY +# TRIGGER: a dispatch made after `cd ` that is denied means the +# page's reading now holds — update this paragraph (#58, S3). +``` +with: +```bash +# Timing: the guard reads the payload's `cwd` (a common field on every hook +# event — https://code.claude.com/docs/en/hooks-guide § How hooks +# work) BEFORE the tool runs. That field is the SESSION's working +# directory — where Claude Code was launched — not the Bash tool's +# persisted shell directory: a logging-hook probe on Claude Code 2.1.285 +# (2026-09-30) logged the launch directory for a Bash call and an Agent +# dispatch made after `cd docs`, as a real dispatch did on 2026-09-07. +# The confirmed deny is a session launched from a subdirectory (the +# verification block's payload dry-run); the remedy there is to relaunch +# the session from the root. The hooks reference states the OPPOSITE — +# "the new directory after Claude runs `cd`" +# (https://code.claude.com/docs/en/hooks § Reference scripts by path, +# read 2026-09-30); this guard judges whichever cwd the payload carries +# and is correct under either reading. RE-VERIFY TRIGGER: a dispatch +# made after `cd ` that is denied means the page's reading now +# holds — re-run the probe and update this paragraph +# (context-builder-kit#58, S3). +``` + +Edit `.claude/hooks/protect-main-branch.sh` — replace this text (exact; it occurs once): +```bash +# The Bash tool's payload carries the call's working directory; the branch +# check must use it (not a fixed project dir), or a commit run from a +# worktree / nested repo is judged against the wrong repo's branch. +``` +with: +```bash +# The payload's `cwd` is the session's launch directory, which a shell `cd` +# does not move (probe, 2026-09-30; require-repo-root-for-agents.sh § Timing); +# judged there, a session launched inside a worktree or a nested repo is checked +# against that checkout's branch. Precedence: cwd, CLAUDE_PROJECT_DIR, $PWD. +``` + +Edit `.claude/rules/cbk-conventions-reference.md` — in § Hook authoring's Header bullet, replace this text (exact; it occurs once): +```markdown +and the payload `cwd` is the session's launch directory, which a shell `cd` does not move — a real dispatch, 2026-09-07 — so a launch-directory guard is answered by relaunching the session from the root) +``` +with: +```markdown +and the payload `cwd` is the session's launch directory, which a shell `cd` does not move — a real dispatch, 2026-09-07, and a logging-hook probe on Claude Code 2.1.285, 2026-09-30, against the hooks page's "the new directory after Claude runs `cd`" (`https://code.claude.com/docs/en/hooks`, read 2026-09-30) — so a launch-directory guard is answered by relaunching the session from the root) +``` + + +The detector: first the bare citations (D53), then rule 1 (R13). Rule 1 promised never to prune a path that could be or contain the tree, which its own `Residual:` line contradicts; the code tests only a directory's own name. + +Edit `.claude/hooks/detect-forked-agent-memory.sh` — replace every occurrence (Edit with `replace_all: true`; the file carries it only as a bare kit-issue citation, six times at `74edf84`): +```text +#58 +``` +with: +```text +context-builder-kit#58 +``` + +Edit `.claude/hooks/detect-forked-agent-memory.sh` — replace this text (exact; it occurs once): +```bash +# 1. Never prune a path that could BE or CONTAIN the tree this hook hunts for. A +# project that ignores the memory directory by an unanchored name (`agent-memory/` +# — the natural spelling for the `local` scope) or ignores `.claude/` wholesale +# would otherwise have its own ignore rules hide the fork. +``` +with: +```bash +# 1. Never prune a directory whose own name is one of the three this rule protects — +# the two the hook hunts, `agent-memory` and `agent-memory-local`, and `.claude`, +# which holds them: such a directory could BE or CONTAIN the tree. A project that +# ignores the memory directory by an unanchored name (`agent-memory/` — the natural +# spelling for the `local` scope) or ignores `.claude/` wholesale would otherwise +# have its own ignore rules hide the fork. A tree nested inside some OTHER ignored +# directory is pruned with it — the Residual above, a limit of this rule, not a +# promise it keeps. +``` + + +Edit `.claude/rules/cbk-conventions-reference.md` — in § Verification, replace the kit sentinel line (exact; it occurs once): +```bash +echo "verification: kit sub-block complete" +``` +with (the new checks sit immediately before it): +```bash +# The launch-root guard and the forked-memory detector on every branch their headers document, by payload +# (context-builder-kit#58 items 1, 2 and 4): the guard's deny and allow by working directory, a root containing a +# space, canonicalization, the $PWD fallback, an unreadable payload refused, and its fail-open branches; the +# detector's block, second stop, prune rules and ignore-driven pruning, and every could-not-look branch printing +# WARNING — the partial scan included (a SKIP line when run as root). Needs git and jq; runs in throwaway trees. +bash .claude/workflows/tests/hook-payloads-fixture.sh || { echo "require-repo-root-for-agents.sh or detect-forked-agent-memory.sh regressed on its fixture"; exit 1; } +echo "verification: kit sub-block complete" +``` + +- [ ] **Step 4: Run the fixture, `bash -n` and the block** + +Run: `bash .claude/workflows/tests/hook-payloads-fixture.sh; echo "exit=$?"` +Expected: `hook-payloads-fixture: 33 cases ok` (or `SKIP: the detector's partial-scan case (an unreadable directory unavailable: running as root)` and 32), `exit=0`. + +Run: `for h in require-repo-root-for-agents detect-forked-agent-memory protect-main-branch; do bash -n .claude/hooks/$h.sh || echo "SYNTAX: $h"; done; grep -c 'context-builder-kit#58' .claude/hooks/detect-forked-agent-memory.sh; grep -nE '(^|[^-a-z])#58' .claude/hooks/detect-forked-agent-memory.sh .claude/hooks/require-repo-root-for-agents.sh | grep -v 'context-builder-kit#58'; echo "bare=$?"` +Expected: no `SYNTAX:` line, `6`, `bare=1`. + +Run: `bash .claude/workflows/tests/hook-guards-fixture.sh | tail -1` +Expected: `hook-guards-fixture: 85 cases ok` (the main-branch guard's comment changed, not its code). + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | grep -E 'hook-payloads-fixture|^verification: (kit sub-block complete|done)$|^verification: block exited'; echo "exit=${PIPESTATUS[0]}"` +Expected: `hook-payloads-fixture: 33 cases ok`, `verification: kit sub-block complete`, `verification: done`, `exit=0`. + +- [ ] **Step 5: Commit** + +```bash +git add .claude/workflows/tests/hook-payloads-fixture.sh .claude/hooks/require-repo-root-for-agents.sh .claude/hooks/detect-forked-agent-memory.sh .claude/hooks/protect-main-branch.sh .claude/rules/cbk-conventions-reference.md +git commit -q -F - <<'MSG' +fix(V2): the launch-root guard refuses what it cannot read; both agent hooks, by payload + +require-repo-root-for-agents.sh failed open on a payload jq cannot parse; it now +refuses it. hook-payloads-fixture.sh drives every branch the launch-root guard and +the forked-memory detector document, a root containing a space included, and pins +the literal WARNING on each could-not-look branch, the partial scan among them +(context-builder-kit#58 items 1, 2 and 4). Probe P1 settled whether a hook +payload's cwd follows the Bash tool's cd on the release CLI; the guard's Timing +paragraph, the main-branch guard's cwd comment and section Hook authoring state +the answer, dated. The detector's rule 1 now states what it guarantees and points +at its Residual; its bare citations are qualified. + +Red first: FAIL: a payload jq cannot parse is refused (want exit 2, got 0) + +Trace: #58/c5901493591/R1 (the two hooks), #58/c5901493591/R6, +#58/c5901493591/R13, #73/body/2 (the WARNING pins), release/5 (the two hooks). +Decisions: D61, D44. Probe: P1. + +Co-Authored-By: Claude Opus 5.5 +MSG +``` +### Task V2.6: Every backstop a rule names exists (#60/c5876324022/corpus-s1, #60/c5881158391/port-s1, #60/c5892401033/suggestion-1-built; D51) + +**Files:** +- Modify: `.claude/rules/cbk-conventions-reference.md` — § Verification: the kit sentinel (the `backstops()` check) and the project sentinel (the task-runner arm) +- Modify: `.claude/rules/cbk-conventions.md` — § Mutation discipline, the two-views paragraph (always loaded: +152 bytes, measured 130,628 → 130,780 in the scratch replay; recorded for D50's before and after) + +**Interfaces:** +- Produces: the kit-sub-block function `backstops ` and its three reads: § Mutation discipline of `cbk-conventions.md`, the `_comment_hooks` string of `.claude/settings.json`, and every `.claude/hooks/*.sh`. Each `.github/workflows/.yml` or `scripts/.(sh|py)` they name must exist. V3's registry sentence may name the ADR workflow (the check then reads it too); a hook or registry text that names a workflow must name a real one. +- Produces: the project-sub-block `mise run ` arm, live only where `mise.toml` exists. +- Consumes: `.github/workflows/adr-immutability-check.yml` (V2.1). + +- [ ] **Step 1: Write the failing check** + +Edit `.claude/rules/cbk-conventions-reference.md` — in § Verification, replace the kit sentinel line (exact; it occurs once): +```bash +echo "verification: kit sub-block complete" +``` +with (the new checks sit immediately before it): +```bash +# Every backstop the mutation table, the hook registry or a hook header names exists — a `.github/workflows/*.yml` +# file or a `scripts/*` file (context-builder-kit#60 comment, suggestion 1: a CI job named but never landed is how a +# target's frozen corpus went two weeks with no backstop behind its hook). The checker is asked about bogus names +# first, so it cannot pass by matching nothing, and a mutation section that names no checkable backstop is red. +backstops() { # backstops : prints each workflow or script path the text names that does not exist; returns 1 if any + local miss=0 tok + while IFS= read -r tok; do [ -z "$tok" ] || [ -f "$tok" ] || { echo " names $tok, which does not exist"; miss=1; }; done <<<"$(grep -oE '\.github/workflows/[A-Za-z0-9._-]+\.ya?ml|scripts/[A-Za-z0-9._/-]+\.(sh|py)' <<<"$1" | sort -u || true)" + return $miss +} +if backstops ".github/workflows/no-such-job.yml scripts/no-such-leg.sh" >/dev/null; then echo "backstops(): a bogus name passed — the checker is broken"; exit 1; fi +mt=$(awk '/^## Mutation discipline/{p=1; next} p && /^## /{exit} p' .claude/rules/cbk-conventions.md) +grep -qE '\.github/workflows/[A-Za-z0-9._-]+\.ya?ml' <<<"$mt" || { echo "cbk-conventions.md § Mutation discipline names no CI backstop in a checkable form (.github/workflows/.yml)"; exit 1; } +backstops "$mt +$(jq -r '._comment_hooks' .claude/settings.json) +$(cat .claude/hooks/*.sh)" || { echo "the mutation table, the hook registry or a hook header names a CI workflow or script that does not exist (above)"; exit 1; } +echo "verification: kit sub-block complete" +``` + +- [ ] **Step 2: Run it against the unfixed contract** + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | grep -E 'names no CI backstop|^verification: (kit sub-block complete|done)$|^verification: block exited'; echo "exit=${PIPESTATUS[0]}"` +Expected: +```text +cbk-conventions.md § Mutation discipline names no CI backstop in a checkable form (.github/workflows/.yml) +verification: block exited 1 +exit=1 +``` +The mutation table names its CI lint only as "the CI lint", so nothing is checkable. Record the line in the red-first table. + +- [ ] **Step 3: Name the backstop by path, and add the task-runner arm** + +Edit `.claude/rules/cbk-conventions.md` — replace this text (exact; it occurs once): +```markdown +A row enforced by a hook names it: ADRs → `protect-immutable-adrs.sh` (plus the CI lint); lock files → `protect-lock-files.sh`. +``` +with: +```markdown +A row enforced by a hook names it: ADRs → `protect-immutable-adrs.sh` (plus the CI lint, `.github/workflows/adr-immutability-check.yml`); lock files → `protect-lock-files.sh`. +``` + + +Edit `.claude/rules/cbk-conventions.md` — replace this text (exact; it occurs once): +```markdown +and the verification block checks the registry against the files. The authoring shape lives in +``` +with: +```markdown +and the verification block checks the registry against the files — and that every workflow or script a backstop names here, in the registry or in a hook header exists. The authoring shape lives in +``` + + +Edit `.claude/rules/cbk-conventions-reference.md` — replace this text (exact; it occurs once): +```markdown + echo "verification: project sub-block complete" +``` +with: +```markdown + # A backstop named as a task-runner task exists in the runner's config (the kit sub-block checks workflow and script + # paths; a task name needs the project's runner). The mise arm is the exercised one, and it reads the `[tasks.]` + # table form; for another runner, swap the two patterns (a `just ` name against the justfile's recipes) — + # unexercised. + if [ -f mise.toml ]; then + for tok in $(cat <(awk '/^## Mutation discipline/{p=1; next} p && /^## /{exit} p' .claude/rules/cbk-conventions.md) <(jq -r '._comment_hooks' .claude/settings.json) .claude/hooks/*.sh | grep -oE 'mise run [a-z][a-z0-9:_-]*' | sed 's/^mise run //' | sort -u || true); do + grep -qE "^\[tasks\.(\"$tok\"|$tok)\]" mise.toml || { echo "a backstop names 'mise run $tok', which is not a task in mise.toml"; exit 1; } + done + fi + + echo "verification: project sub-block complete" +``` + + +- [ ] **Step 4: Run the block, then prove the check bites** + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | grep -E 'names no CI backstop|names a CI workflow|^verification: (kit sub-block complete|done)$|^verification: block exited'; echo "exit=${PIPESTATUS[0]}"` +Expected: `verification: kit sub-block complete`, `verification: done`, `exit=0`. + +Run (a backstop named but absent, in a scratch copy): +```bash +S=$(mktemp -d) && cp -a . "$S/kit" && cd "$S/kit" \ +&& sed -i 's#(plus the CI lint, `.github/workflows/adr-immutability-check.yml`)#(plus the CI lint, `.github/workflows/adr-immutability-chek.yml`)#' .claude/rules/cbk-conventions.md \ +&& bash .claude/workflows/tests/run-verification-block.sh 2>&1 | tail -3; cd - >/dev/null; rm -rf "$S" +``` +Expected: +```text + names .github/workflows/adr-immutability-chek.yml, which does not exist +the mutation table, the hook registry or a hook header names a CI workflow or script that does not exist (above) +verification: block exited 1 +``` + +Run (the project arm, on a synthetic tree): +```bash +S=$(mktemp -d) && mkdir -p "$S/.claude/rules" "$S/.claude/hooks" && cd "$S" \ +&& printf '## Mutation discipline\nrow: `mise run frozen-corpus`\n## Next\n' > .claude/rules/cbk-conventions.md \ +&& echo '{"_comment_hooks":"x mise run check"}' > .claude/settings.json && echo '# hook' > .claude/hooks/a.sh \ +&& printf '[tasks.frozen-corpus]\n' > mise.toml \ +&& awk '/# A backstop named as a task-runner task/{p=1} p&&/^ fi$/{print; exit} p' "$OLDPWD/.claude/rules/cbk-conventions-reference.md" > arm.sh \ +&& bash -e arm.sh; echo "exit=$?"; cd "$OLDPWD"; rm -rf "$S" +``` +Expected: `a backstop names 'mise run check', which is not a task in mise.toml`, `exit=1`. + +- [ ] **Step 5: Commit** + +```bash +git add .claude/rules/cbk-conventions-reference.md .claude/rules/cbk-conventions.md +git commit -q -F - <<'MSG' +feat(V2): the block checks that every backstop a rule names exists + +A CI job named as a hook's backstop but never landed is how a target's frozen +corpus went two weeks unguarded (context-builder-kit#60, suggestion 1). The kit +sub-block now reads the mutation table, the hook registry and every hook header, +and fails on a .github/workflows or scripts path that does not exist; the checker +is asked about bogus names first, and a mutation section that names no checkable +backstop is red. The table's ADR row names its workflow by path. The project +sub-block resolves `mise run ` names against mise.toml where one exists. + +Red first: cbk-conventions.md § Mutation discipline names no CI backstop in a checkable form (.github/workflows/.yml) + +Trace: #60/c5876324022/corpus-s1, #60/c5881158391/port-s1, +#60/c5892401033/suggestion-1-built. +Decision: D51. + +Co-Authored-By: Claude Opus 5.5 +MSG +``` +### Task V2.7: The frozen-corpus recipe carries the CI job's closures, one checklist row per item (#60/c5901494943/1, #60/c5876324022/corpus-s2, #60/c5876324022/echosphere-z, #60/c5892401033/corpus-leg, #60/c5892401033/leg-defects, critic/6, critic/7; D55) + +**Files:** +- Modify: `.claude/skills/consultation/references/frozen_corpus_ingestion.md` — § The enforcement set scaffold registers (its first bullet, and its closing sentence) +- Modify: `.claude/skills/scaffold/references/bootstrap_checklist_template.md` — § 3 Verification matrix, after its closing paragraph +- Modify: `.claude/skills/scaffold/references/brownfield_audit.md` — the frozen-corpus bullet (line 35) +- Modify: `.claude/skills/consultation/references/test_cases.md` — Test 4's handoff criterion +- Modify: `.claude/rules/cbk-conventions-reference.md` — § Verification (the kit sentinel) + +**Interfaces:** +- Produces: the enforcement-set bullet `**The CI job's closures.**` (nine closures and the additions call), and six verification rows keyed `| Corpus hook |`, `| Corpus CI job |`, `| Corpus CI job's closures |`, `| Corpus \`.gitattributes\` |`, `| Corpus editor settings |`, `| Corpus formatter skip |`, which the block asserts. +- Consumes: `lib/resolve-path.sh`'s root-scoped mode (V2.2) and the ADR job's parse-nothing body (V2.1), which the recipe tells a target to build on. No corpus guard or corpus CI script ships (D55; spec § Non-goals). + +- [ ] **Step 1: Write the failing check** + +Edit `.claude/rules/cbk-conventions-reference.md` — in § Verification, replace the kit sentinel line (exact; it occurs once): +```bash +echo "verification: kit sub-block complete" +``` +with (the new checks sit immediately before it): +```bash +# The frozen-corpus recipe (context-builder-kit#60): the enforcement set lists the CI job's closures and builds the job on +# the ADR job's parse-nothing body; the bootstrap checklist carries one verification row per item, never one for all. +{ grep -q "The CI job's closures" .claude/skills/consultation/references/frozen_corpus_ingestion.md && grep -qF ':(glob)' .claude/skills/consultation/references/frozen_corpus_ingestion.md; } || { echo "frozen_corpus_ingestion.md lacks the CI job's closure list or the parse-nothing body"; exit 1; } +for r in 'Corpus hook |' 'Corpus CI job |' "Corpus CI job's closures |" 'Corpus `.gitattributes` |' 'Corpus editor settings |' 'Corpus formatter skip |'; do grep -qF "| $r" .claude/skills/scaffold/references/bootstrap_checklist_template.md || { echo "the bootstrap checklist lacks the corpus verification row: $r"; exit 1; }; done +absent grep -n "carries the four items as one ro[w]" .claude/skills/consultation/references/frozen_corpus_ingestion.md +echo "verification: kit sub-block complete" +``` + +- [ ] **Step 2: Run it against the unfixed recipe** + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | grep -E 'closure list|corpus verification row|^verification: (kit sub-block complete|done)$|^verification: block exited'; echo "exit=${PIPESTATUS[0]}"` +Expected: +```text +frozen_corpus_ingestion.md lacks the CI job's closure list or the parse-nothing body +verification: block exited 1 +exit=1 +``` +Record the line in the red-first table. + +- [ ] **Step 3: Verify the platform fact, then write the recipe and the rows** + +Run: `curl -sL https://raw.githubusercontent.com/actions/checkout/11d5960a326750d5838078e36cf38b85af677262/README.md | grep -cF 'fetch all history for all branches and tags'` +Expected: `1`. If `0`, stop: the first closure quotes it. + +Edit `.claude/skills/consultation/references/frozen_corpus_ingestion.md` — replace this text (exact; it occurs once): +```markdown +- **A hook and a CI job on the corpus pattern** — the immutability hook and the ADR-immutability CI job extended (or copied) to match the corpus path, with a block message that names the errata companion as the place a correction goes. +``` +with: +```markdown +- **A hook and a CI job on the corpus pattern**, each with a block message that names the errata companion as the place a correction goes. The hook is a guard over the corpus path that sources `.claude/hooks/lib/resolve-path.sh` in its root-scoped mode — the corpus under the hook's own checkout, the project dir's and a linked worktree of either, each path resolved lexically and physically (`cbk-conventions-reference.md` § Hook authoring, on sourced helpers). The CI job extends (or copies) the kit's ADR-immutability job with that job's parse-nothing body — a `:(glob)` pathspec, `--no-renames`, a three-dot diff from the merge base, `--diff-filter=a` only if additions pass, any output failing and any git error failing — never an awk match on parsed paths, which reads a quoted non-ASCII name or a spaced one as "no change". +- **The CI job's closures.** The hook sees only the agent's edit tools; the CI job is the backstop for a Bash write, a hand edit or a case-insensitive filesystem, and a job that compares the corpus with its base can be fooled more ways than a hook. Two targets red-teamed their corpus jobs. Both implement every closure below, and each is a fixture case in at least one of them — except clearing git's environment and ignoring grafts, which neither fixture drives yet. The kit ships no corpus job; this list is what the two measured: + - the base resolves from fully qualified refs (`refs/remotes/origin/`, then `refs/heads/`) — a tag or local branch named `origin/main` would shadow the bare name, and `actions/checkout` with `fetch-depth: 0` fetches "all history for all branches and tags" (its README at the kit's pinned SHA, read 2026-09-30); no base fails, never skips; + - on the default branch, or a branch with no commits of its own, the merge base is HEAD, so the last commit is compared with its parent; + - `GIT_DIR`, `GIT_WORK_TREE`, `GIT_INDEX_FILE` and their kin are cleared, and replace refs and grafts ignored; + - nothing is sourced from the tree under check — a PR-supplied helper could define a `git` function that silences every diff; + - the commits, the index and the working tree are each compared with the base, so an edit staged or committed with the tree put back is caught; + - the bytes on disk are hashed with `--no-filters` against the base blob — git's own diffs trust the index (assume-unchanged, skip-worktree) and apply checkout conversions (`core.autocrlf`, a `.gitattributes` eol, encoding or filter a PR can commit); + - a path's type is compared before its content, so a symlink standing in for a file is a change even when what it reaches is identical; a failed `readlink` stops the job; + - every path list is read NUL-separated (`-z`) — a quoted non-ASCII name otherwise reads as a missing file; + - every git call fails the job on error; an error is never read as "unchanged". + + Whether an added file passes (a newly frozen document added by hand in a reviewed commit) is the target's call: of the two targets, one allows additions and the other freezes the directory whole. +``` + + +Edit `.claude/skills/consultation/references/frozen_corpus_ingestion.md` — replace this text (exact; it occurs once): +```markdown +Scaffold's brownfield audit and backend selection read the designated path from `## Pre-cascade sources`; the bootstrap checklist carries the four items as one row. +``` +with: +```markdown +Scaffold's brownfield audit and backend selection read the designated path from `## Pre-cascade sources`; the verification matrix of the scaffold skill's `references/bootstrap_checklist_template.md` carries one row per item above, so no item is ticked off with the others. +``` + + +Edit `.claude/skills/scaffold/references/bootstrap_checklist_template.md` — replace this text (exact; it occurs once): +```markdown +Only include rows that are actually applicable. State 2 (no projects toolset) omits the project board row. State 4 (no MCP) puts everything in section 2 (manual instructions) and the verification matrix becomes longer. +``` +with: +~~~~markdown +Only include rows that are actually applicable. State 2 (no projects toolset) omits the project board row. State 4 (no MCP) puts everything in section 2 (manual instructions) and the verification matrix becomes longer. + +**When a frozen corpus is designated** (`problem_brief.md` § Pre-cascade sources names one — the consultation skill's `references/frozen_corpus_ingestion.md`), the matrix gains one row per item of its enforcement set, so no item is ticked off with the others. Omit these rows when no corpus is designated: + +```markdown +| Corpus hook | Pipe an Edit payload naming a corpus file into the corpus guard | Exit 2; the message names the errata companion | ☐ | +| Corpus CI job | Open a throwaway PR that edits one corpus file | The job fails, and its check is one of the ruleset's required contexts | ☐ | +| Corpus CI job's closures | Read the job against the closure list in `frozen_corpus_ingestion.md` § The enforcement set scaffold registers | Every closure is implemented; the ones the job's own fixture drives are named | ☐ | +| Corpus `.gitattributes` | `git check-attr linguist-documentation /` | The attribute reads `false` | ☐ | +| Corpus editor settings | Open a corpus file in the editor and save it unchanged | `git status` shows no change (no whitespace trim, no final newline added) | ☐ | +| Corpus formatter skip | Run the docs formatter's check over the corpus path | It rewrites nothing | ☐ | +``` +~~~~ + + +Edit `.claude/skills/scaffold/references/brownfield_audit.md` — replace this text (exact; it occurs once): +```markdown +and register the enforcement set in the same commit — the immutability hook and CI job extended to the corpus pattern with a block message naming the errata, the `.gitattributes` `linguist-documentation=false` line, the editor trim/newline unsets, the formatter skip entry. +``` +with: +```markdown +and register the enforcement set in the same commit — a corpus guard on `.claude/hooks/lib/resolve-path.sh` and a CI job built on the ADR job's parse-nothing body with the closures that reference lists, each naming the errata in its block message; the `.gitattributes` `linguist-documentation=false` line; the editor trim/newline unsets; the formatter skip entry — and give each item its own row in the bootstrap checklist's verification matrix. +``` + + +Edit `.claude/skills/consultation/references/test_cases.md` — replace this text (exact; it occurs once): +```markdown +- The brief's `## Handoff notes for later phases` addresses scaffold by name (land the corpus at the recorded path, register the enforcement set) and blueprint by name (which decisions may be promoted, with the `Promotes:` form) +``` +with: +```markdown +- The brief's `## Handoff notes for later phases` addresses scaffold by name (land the corpus at the recorded path, register the enforcement set — the corpus guard, the CI job with its closures, and the other items, each a verification row of its own) and blueprint by name (which decisions may be promoted, with the `Promotes:` form) +``` + + +The closures are re-authored from two targets' red-team rounds; the shipped text names neither target, and the kit ships none of their scripts. + +- [ ] **Step 4: Run the block** + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | grep -E 'closure list|corpus verification row|^verification: (kit sub-block complete|done)$|^verification: block exited'; echo "exit=${PIPESTATUS[0]}"` +Expected: `verification: kit sub-block complete`, `verification: done`, `exit=0`. + +- [ ] **Step 5: Commit** + +```bash +git add .claude/skills/consultation/references/frozen_corpus_ingestion.md .claude/skills/scaffold/references/bootstrap_checklist_template.md .claude/skills/scaffold/references/brownfield_audit.md .claude/skills/consultation/references/test_cases.md .claude/rules/cbk-conventions-reference.md +git commit -q -F - <<'MSG' +docs(V2): the frozen-corpus recipe carries the CI job's closures, one checklist row per item + +Consultation's enforcement set told a target to extend the ADR job, which then +inherited its awk path match. The recipe now builds the corpus guard on +lib/resolve-path.sh's root-scoped mode and the corpus CI job on the ADR job's +parse-nothing body, and lists the nine closures two targets' red-team rounds +measured, ending with the additions call each target makes. The bootstrap +checklist gains one verification row per enforcement item, so no item is ticked +off with the others; brownfield audit and consultation's Test 4 say the same. +The block asserts the closures and the six rows. + +Red first: frozen_corpus_ingestion.md lacks the CI job's closure list or the parse-nothing body + +Trace: #60/c5901494943/1, #60/c5876324022/corpus-s2, +#60/c5876324022/echosphere-z, #60/c5892401033/corpus-leg, +#60/c5892401033/leg-defects, critic/6, critic/7. +Decision: D55. + +Co-Authored-By: Claude Opus 5.5 +MSG +``` +### Task V2.8: § Hook authoring names the fixture of every hook family and the three-edit wiring (#58/c5901493591/R1 (§ Verify by payload), #58/c5901493591/R11 (§ Hook authoring); D44) + +**Files:** +- Modify: `.claude/rules/cbk-conventions-reference.md` — § Hook authoring's "No hook-shaped object" bullet and Verify by payload bullet; § Verification (the kit sentinel) +- Modify: `.claude/workflows/tests/hook-contract-fixture.sh` (header line 8) + +**Interfaces:** +- Produces: the sentence "Wiring one is three edits, not one: …" — V8 writes the same three edits into both advisory exemplars' `Register:` headers. +- Produces: the Verify by payload bullet naming `hook-guards-fixture.sh`, `hook-payloads-fixture.sh`, `protected-paths-hook-fixture.sh` and `hook-contract-fixture.sh`, asserted by the block. + +- [ ] **Step 1: Write the failing check** + +Edit `.claude/rules/cbk-conventions-reference.md` — in § Verification, replace the kit sentinel line (exact; it occurs once): +```bash +echo "verification: kit sub-block complete" +``` +with (the new checks sit immediately before it): +```bash +# § Hook authoring › Verify by payload names the fixture of every hook family, and each one it names exists; the section +# says that wiring an advisory hook is three edits (context-builder-kit#58 item 4 and its residue). +vp=$(grep '^- \*\*Verify by payload\.\*\*' .claude/rules/cbk-conventions-reference.md) +for f in hook-guards-fixture.sh hook-payloads-fixture.sh protected-paths-hook-fixture.sh hook-contract-fixture.sh; do grep -qF "$f" <<<"$vp" || { echo "§ Hook authoring › Verify by payload does not name $f"; exit 1; }; [ -f ".claude/workflows/tests/$f" ] || { echo "§ Hook authoring › Verify by payload names $f, which does not exist"; exit 1; }; done +grep -q 'ADVISORY_WIRED' <<<"$(awk '/^## Hook authoring/{p=1; next} p && /^## /{exit} p' .claude/rules/cbk-conventions-reference.md)" || { echo "§ Hook authoring does not say that wiring an advisory hook also names it in ADVISORY_WIRED"; exit 1; } +echo "verification: kit sub-block complete" +``` + +- [ ] **Step 2: Run it against the unfixed section** + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | grep -E 'Verify by payload|ADVISORY_WIRED|^verification: (kit sub-block complete|done)$|^verification: block exited'; echo "exit=${PIPESTATUS[0]}"` +Expected: +```text +§ Hook authoring › Verify by payload does not name hook-guards-fixture.sh +verification: block exited 1 +exit=1 +``` +The bullet still says the payload-reachable branches are asserted in `hook-contract-fixture.sh`, which drives none of the ADR guard, the knowledge-backend ask-gate or the main-branch allow cases. Record the line. + +- [ ] **Step 3: Rewrite the two bullets and the contract fixture's header** + +Edit `.claude/rules/cbk-conventions-reference.md` — replace this text (exact; it occurs once): +```markdown +copied into `hooks.PostToolUse` after the case arms are filled — so a fresh checkout never runs a formatter it does not have. +``` +with: +```markdown +copied into `hooks.PostToolUse` after the case arms are filled — so a fresh checkout never runs a formatter it does not have. Wiring one is three edits, not one: the stanza into `hooks.PostToolUse`, the hook's name into the project sub-block's `ADVISORY_WIRED`, and its name into `cbk-conventions.md` § Mutation discipline's two-views paragraph — the verification block checks all three, so a registration alone turns it red. +``` + + +Edit `.claude/rules/cbk-conventions-reference.md` — replace this text (exact; it occurs once): +```markdown +- **Verify by payload.** When a hook is authored or changed, each branch it has is exercised by piping a crafted JSON payload and asserting the exact exit (only 2 denies — a crash is not a block), plus one real dispatch for a guard whose matcher is a dated observation; the branches a payload can reach are asserted in `.claude/workflows/tests/hook-contract-fixture.sh` (durable, run by the block), and the PR body's table is for the branches only a mutation can reach, named as such (context-builder-kit#58 item 4). The verification block re-runs a subset on every run — the launch-root guard's deny and allow payloads, and the Stop hook against the live tree — while a state-mutating dry-run (a stray memory tree, a commit on `main`) runs against a fixture or a throwaway clone at authoring time. +``` +with: +```markdown +- **Verify by payload.** When a hook is authored or changed, each branch it has is exercised by piping a crafted JSON payload and asserting the exact exit (only 2 denies — a crash is not a block), plus one real dispatch for a guard whose matcher is a dated observation. The branches a payload can reach are asserted durably, one fixture per hook family under `.claude/workflows/tests/`, each run by the verification block: `hook-guards-fixture.sh` (the main-branch deny, the PR-state ask-gate, the lock-file deny, the knowledge-backend ask-gate), `hook-payloads-fixture.sh` (the launch-root guard, the fork detector) and `protected-paths-hook-fixture.sh` (the ADR guard and the path helper); `hook-contract-fixture.sh` holds the structural checks and one over-buffer probe per decision site. A target that adds a hook adds its cases to its family's fixture, or writes a fixture of its own and runs it from the block. The PR body's table is for the branches only a mutation can reach, named as such (context-builder-kit#58 item 4). The verification block also re-runs a subset against the live tree — the launch-root guard's deny and allow payloads, and the Stop hook — while a state-mutating dry-run (a stray memory tree, a commit on `main`) runs against a fixture or a throwaway clone. +``` + + +Edit `.claude/workflows/tests/hook-contract-fixture.sh` — replace this text (exact; it occurs once): +```bash +# arms. Runs against throwaway `git init` trees under mktemp, never the real checkout, and never +``` +with: +```bash +# arms; every branch a payload can reach is driven by the family fixtures beside it — +# hook-guards-fixture.sh, hook-payloads-fixture.sh and protected-paths-hook-fixture.sh. +# Runs against throwaway `git init` trees under mktemp, never the real checkout, and never +``` + + +- [ ] **Step 4: Run the contract fixture and the block** + +Run: `bash .claude/workflows/tests/hook-contract-fixture.sh` +Expected: `hook-contract-fixture: ok`. + +Run: `bash .claude/workflows/tests/run-verification-block.sh 2>&1 | grep -E 'Verify by payload|ADVISORY_WIRED|^verification: (kit sub-block complete|done)$|^verification: block exited'; echo "exit=${PIPESTATUS[0]}"` +Expected: `verification: kit sub-block complete`, `verification: done`, `exit=0`. + +- [ ] **Step 5: Commit** + +```bash +git add .claude/rules/cbk-conventions-reference.md .claude/workflows/tests/hook-contract-fixture.sh +git commit -q -F - <<'MSG' +docs(V2): section Hook authoring names every hook family's fixture and the three-edit wiring + +Verify by payload said the payload-reachable branches were asserted in +hook-contract-fixture.sh, which drove none of the ADR guard, the knowledge-backend +ask-gate or the main-branch allow cases. It now names the fixture of each hook +family, all run by the block, and what a target does when it adds a hook. The +advisory-hook bullet says that wiring one is three edits: the stanza, the +ADVISORY_WIRED name and the two-views paragraph, each checked by the block. The +block asserts both. + +Red first: § Hook authoring › Verify by payload does not name hook-guards-fixture.sh + +Trace: #58/c5901493591/R1 (section Verify by payload), #58/c5901493591/R11 +(section Hook authoring; the exemplars' Register: headers are V8's). +Decision: D44. + +Co-Authored-By: Claude Opus 5.5 +MSG +``` +### Task V2.9: Probe P4 — record whether CI runs the bind-mount cases (probe P4; D44) + +**When:** after Task F4 Step 2 (the draft PR exists and `verify.yml` has run on it), before Task F4 Step 3. It is the one V2 commit that lands after the review floor; it changes one comment line of a fixture, which F2 does not re-review. + +**Files:** +- Modify: `.claude/workflows/tests/protected-paths-hook-fixture.sh` (header lines 14–16) + +**Interfaces:** +- Produces: a dated observation of the kit's CI runner in the fixture's header, and the P4 line of the PR body. + +- [ ] **Step 1: Read the PR's run** + +Run: +```bash +run=$(gh run list --branch feat/harvest-5-v1.0.0 --workflow verify.yml --limit 1 --json databaseId -q '.[0].databaseId') +gh run view "$run" --log | grep -E 'SKIP: three bind-mount cases|protected-paths-hook-fixture: [0-9]+ cases ok' +gh run view "$run" --json url,createdAt -q '.url + " " + .createdAt' +grep -m1 'runs-on:' .github/workflows/verify.yml +``` +Expected: either **(i)** `SKIP: three bind-mount cases (unshare -rm unavailable: no unprivileged user and mount namespace)` and `protected-paths-hook-fixture: 45 cases ok`, or **(ii)** `protected-paths-hook-fixture: 48 cases ok` with no SKIP line; then the run's URL and timestamp, and the runner label. A count other than 45 or 48 means a `/proc` skip or a regression: stop and read the log. + +- [ ] **Step 2: Write the observation** + +In the text below, `