From d03a30ad9a0240fe119561ad944e96fdffda3015 Mon Sep 17 00:00:00 2001 From: Victor Kzam Date: Fri, 24 Jul 2026 12:05:41 +0100 Subject: [PATCH 01/19] feat(hooks): add design-scope guard confining /design writes to artifact paths Co-Authored-By: Claude --- hooks/design-scope-guard.sh | 68 +++++++++++++++++++++++++++++++++++++ scripts/manifest.txt | 1 + 2 files changed, 69 insertions(+) create mode 100755 hooks/design-scope-guard.sh diff --git a/hooks/design-scope-guard.sh b/hooks/design-scope-guard.sh new file mode 100755 index 0000000..34cc569 --- /dev/null +++ b/hooks/design-scope-guard.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# PreToolUse(Write|Edit) scope guard. While /design is active for THIS session (its +# short post-approval write window), edits are confined to design artifact paths. +# Session-scoped + self-healing sentinel, structurally mirroring +# hooks/orchestrator-delegate-guard.sh. Honest strength claim: the sentinel is +# model-removable in principle — same trust class as the delegate guard. +# +# TTL_MINUTES: a sentinel older than this is treated as stale (a crashed/killed +# /design) and reaped, then the edit is allowed. /design's write window is short, +# so this is a generous ceiling rather than an expected duration. +TTL_MINUTES=120 + +INPUT=$(cat) + +# Fast path (no jq needed): if no sentinel file of any kind exists, /design is not +# active anywhere on the machine, so there is nothing to guard — exit before we +# need to parse anything. This is the common case for every edit outside a /design run. +if ! ls /tmp/claude-design-active* >/dev/null 2>&1; then + exit 0 +fi + +# Past this point a sentinel might be active, so we need jq to read session_id / +# agent_type / file_path from stdin. Fail closed rather than silently allowing +# an edit the guard can no longer evaluate. +command -v jq >/dev/null 2>&1 || { echo "jq required for this hook — install jq or remove it from settings.json" >&2; exit 2; } + +SID=$(echo "$INPUT" | jq -r '.session_id // empty') +SENTINEL="/tmp/claude-design-active${SID:+.$SID}" +LEGACY="/tmp/claude-design-active" + +# Find an active, non-stale sentinel: prefer the session-scoped one, fall back to the +# legacy global path for backward compatibility. Reap stale ones (self-heal). +ACTIVE="" +for S in "$SENTINEL" "$LEGACY"; do + [ -f "$S" ] || continue + if [ -n "$(find "$S" -mmin +"$TTL_MINUTES" 2>/dev/null)" ]; then + rm -f "$S" + continue + fi + ACTIVE="$S"; break +done +[ -n "$ACTIVE" ] || exit 0 + +# agent_type is present in hook stdin only inside a subagent (per Claude Code hooks +# docs; for custom agents it is the frontmatter `name`), absent on the main thread. +# A delegated subagent is allowed to edit (accepted residual exemption). +AGENT_TYPE=$(echo "$INPUT" | jq -r '.agent_type // empty') +if [ -n "$AGENT_TYPE" ] && [ "$AGENT_TYPE" != "main" ]; then + exit 0 +fi + +FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.filePath // empty') + +# /design may write plan and research artifacts, and .gitattributes (used to mark +# generated files). When CLAUDE_CONFIG_DIR is set (adopters using a non-default +# config location), its plans/ dir is editable too, alongside the default +# $HOME/.claude/plans/. +case "$FILE" in + */docs/plans/*|*/docs/research/*|*/.gitattributes|"$HOME"/.claude/plans/*|/tmp/*|"") exit 0 ;; +esac +if [ -n "$CLAUDE_CONFIG_DIR" ]; then + case "$FILE" in + "$CLAUDE_CONFIG_DIR"/plans/*) exit 0 ;; + esac +fi + +echo "/design is active — writes are confined to the design artifact paths (docs/plans/, docs/research/, plans dir)" >&2 +exit 2 diff --git a/scripts/manifest.txt b/scripts/manifest.txt index 11eab21..13890e7 100644 --- a/scripts/manifest.txt +++ b/scripts/manifest.txt @@ -4,6 +4,7 @@ agents/direction-reviewer.md agents/implementer.md agents/researcher.md agents/reviewer.md +hooks/design-scope-guard.sh hooks/orchestrator-delegate-guard.sh hooks/protect-branches.sh hooks/protect-secrets.sh From ddaa289332fd0d0287e8415f88c46ace7eb6337d Mon Sep 17 00:00:00 2001 From: Victor Kzam Date: Fri, 24 Jul 2026 12:05:42 +0100 Subject: [PATCH 02/19] feat(hooks): allow project docs and gitattributes writes in the delegate guard Widens the orchestrator delegate-guard allowlist so promoted design docs, docs/research/, and .gitattributes stay orchestrator-editable while the /build sentinel is armed. Co-Authored-By: Claude --- hooks/orchestrator-delegate-guard.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/hooks/orchestrator-delegate-guard.sh b/hooks/orchestrator-delegate-guard.sh index 225885f..b39a4b1 100755 --- a/hooks/orchestrator-delegate-guard.sh +++ b/hooks/orchestrator-delegate-guard.sh @@ -52,11 +52,12 @@ fi FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.filePath // empty') -# The orchestrator may still touch its own plan/design drafts and scratch space. -# When CLAUDE_CONFIG_DIR is set (adopters using a non-default config location), -# its plans/ dir is editable too, alongside the default $HOME/.claude/plans/. +# The orchestrator may still touch its own plan/design drafts, scratch space, +# promoted docs/plans/, docs/research/, and .gitattributes. When CLAUDE_CONFIG_DIR +# is set (adopters using a non-default config location), its plans/ dir is +# editable too, alongside the default $HOME/.claude/plans/. case "$FILE" in - "$HOME"/.claude/plans/*|/tmp/*|"") exit 0 ;; + "$HOME"/.claude/plans/*|*/docs/plans/*|*/docs/research/*|*/.gitattributes|/tmp/*|"") exit 0 ;; esac if [ -n "$CLAUDE_CONFIG_DIR" ]; then case "$FILE" in @@ -64,5 +65,5 @@ if [ -n "$CLAUDE_CONFIG_DIR" ]; then esac fi -echo "Blocked: /build is active (session ${SID:-global}); the orchestrator must not edit source files directly. Spawn an implementer subagent for this change. Plan files under ~/.claude/plans/ (or \$CLAUDE_CONFIG_DIR/plans/ if set) remain editable." >&2 +echo "Blocked: /build is active (session ${SID:-global}); the orchestrator must not edit source files directly. Spawn an implementer subagent for this change. Plan files under ~/.claude/plans/ (or \$CLAUDE_CONFIG_DIR/plans/ if set), project docs/plans/, docs/research/, and .gitattributes remain editable." >&2 exit 2 From 139eed5f242a1d3ee8cd9abcb91d98622f3cd4f6 Mon Sep 17 00:00:00 2001 From: Victor Kzam Date: Fri, 24 Jul 2026 12:06:02 +0100 Subject: [PATCH 03/19] chore(gitignore): allow docs/plans and .gitattributes through the default-deny ignore Co-Authored-By: Claude --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d74a5e3..d556144 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ # Explicitly allow tracked template files and directories. !.gitignore +!.gitattributes !README.md !LICENSE !CLAUDE.md @@ -22,6 +23,8 @@ !scripts/** !tests/ !tests/** +!docs/ +!docs/** !.github/ !.github/** @@ -40,7 +43,7 @@ settings.json settings.local.json .claude.json .mcp.json -plans/ +/plans/ projects/ history.jsonl *.log From 3c74c99203fe3619c0e8340086de49e975de62fe Mon Sep 17 00:00:00 2001 From: Victor Kzam Date: Fri, 24 Jul 2026 12:09:19 +0100 Subject: [PATCH 04/19] feat(design-skill): write the approved artifact set into the project docs folder Co-Authored-By: Claude --- skills/design/SKILL.md | 143 +++++++++++++++++++++++++++++++++++------ 1 file changed, 122 insertions(+), 21 deletions(-) diff --git a/skills/design/SKILL.md b/skills/design/SKILL.md index db3a584..b61d719 100644 --- a/skills/design/SKILL.md +++ b/skills/design/SKILL.md @@ -11,6 +11,13 @@ allowed-tools: - Edit - Bash(git log:*) - Bash(git diff:*) + - Bash(touch /tmp/claude-design-active*) + - Bash(rm -f /tmp/claude-design-active*) + - Bash(git rev-parse:*) + - Bash(mkdir:*) + - Bash(git check-ignore:*) + - Bash(git init:*) + - Bash(git commit --allow-empty*) --- # Research-Grounded Design @@ -20,20 +27,34 @@ You are helping the user design a feature. The cost model is **80/20 — spend t ## Input $ARGUMENTS -Derive a short kebab-case `` from the feature (first 3–5 meaningful words). All artifacts go in `~/.claude/plans/`: +Derive a short kebab-case `` from the feature (first 3–5 meaningful words). All plan-mode artifacts go in `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/plans/`: - `-research-codebase.md`, `-research-docs.md`, `-research-bestpractices.md` - `-design-draft.md` (the design itself; the review loop reads this) - `-tasks.md` (the executable task breakdown `/build` consumes) ## Mode -`/design` runs **in plan mode**. Plan mode normally restricts writes to a single plan -file; `/design` is the documented exception — it is model-invocable and writes its -`~/.claude/plans/-*.md` artifact set (all writes confined there, matching the -orchestrator-delegate-guard exemption). *Workflow judgment call — not officially blessed -by plan mode, but bounded to the plans directory.* The Step 5 Human Checkpoint is the -**exit-plan-mode boundary**: do not exit plan mode before it. `/build` and `/ship` run -afterward in execute mode. +`/design` is a **hybrid**: Steps 0–5 (research, draft, review loop) run **in plan mode**, +then a short guarded window in Step 6 runs in **execute mode** to write the approved +artifact set into the project, and Step 7 is the single human checkpoint that follows. + +Plan mode normally restricts writes to a single plan file; the Steps 0–5 loop is the +documented exception — it is model-invocable and writes its research/draft/task-breakdown +artifact set to `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/plans/-*.md` (all writes +confined there, matching the orchestrator-delegate-guard exemption). *Workflow judgment +call — not officially blessed by plan mode, but bounded to the plans directory.* This part +of the skill is unchanged by the hybrid model below. For meta-tooling designs (edits to +this repo's own skills/hooks, where there is no separate project `docs/plans/` to promote +into), the artifacts live and end here — an accepted limitation. + +At the end of Step 5, `/design` calls `ExitPlanMode` itself — this is the explicit mode +transition; there is no longer an implicit boundary to avoid crossing. **The user's +approval at that `ExitPlanMode` call is administrative, not the go/no-go decision** — it +only ends drafting and lets the turn continue into execute mode for the Step 6 artifact +write. The real accept/reject decision point remains `/build`. If the user rejects at +`ExitPlanMode`, stay in plan mode and keep iterating per the Iteration Protocol below, +exactly as before this change. If the user approves, the same turn continues straight into +Step 6 — no new turn, no re-prompt. ## Step 0: Office-Hours Gate (before any research spend) @@ -107,7 +128,7 @@ Schema per task: - model: sonnet | opus # opus only if >5 files, long-horizon, or cross-cutting schema - risk: high | normal | trivial # optional; feeds /build's tiered review depth (deep review for high, fast-path for trivial) - verification: exact command(s) + expected exit 0 # verification IS the acceptance contract (Proposal 4.2 acceptance: folded in deliberately) — this is what /build checks against ground truth, never narrative -- commit: (): # ONE atomic Conventional Commit; the Co-Authored-By trailer is required +- commit: (): | none (unversioned/external target — verification-only gates) # ONE atomic Conventional Commit; the Co-Authored-By trailer is required unless commit is `none` ``` (Spec-driven best practice: Pimzino/claude-code-spec-workflow, gotalab/cc-sdd; one @@ -140,26 +161,106 @@ Loop until convergence or the safety valve fires. Counter starts at 1. **Safety valve**: after 5 iterations without convergence, stop looping and escalate to the user with the full list of surviving (verified, unresolved) critical issues — do not silently finalize, and do not attempt a 6th iteration. -## Step 5: Human Checkpoint +## Step 5: Pre-Exit Summary + ExitPlanMode Present the final `-design-draft.md`, the Step 4 convergence result ([N] iterations run, `CONVERGED` with zero verified critical issues or `ESCALATED` after 5 iterations with the surviving verified critical issues listed, plus any unverified risks and accumulated -minor notes), and the `-tasks.md` task count. **Print the tasks-file path and the -exact next command verbatim** so they survive a `/clear` or a new session. Then STOP: - -> "Design + task plan ready. Review loop: [N] iteration(s) → [CONVERGED, zero verified critical issues / ESCALATED after 5 iterations, M surviving critical issues]. Minor notes: [count]. Unverified risks (logged, not acted on): [count]. Tasks: `~/.claude/plans/-tasks.md` ([K] tasks). -> When ready: `/build ` (or just `/build` and I'll confirm the most recent tasks file). +minor notes), and the `-tasks.md` task count: + +> "Design + task plan ready. Review loop: [N] iteration(s) → [CONVERGED, zero verified +> critical issues / ESCALATED after 5 iterations, M surviving critical issues]. Minor +> notes: [count]. Unverified risks (logged, not acted on): [count]. Tasks: [K] tasks in +> `-tasks.md`. +> Approving here exits plan mode so I can write the artifact set into the project — it's +> not the build decision, `/build` still is." + +Then call **`ExitPlanMode`**. This is the explicit mode transition described in Mode +above. +- If the user **rejects**, remain in plan mode and follow the pre-promotion path of the + Iteration Protocol below — unchanged from before this feature. +- If the user **approves**, continue in the same turn into Step 6. + +## Step 6: Post-Approval Artifact Write (execute mode, same turn) + +Runs only after `ExitPlanMode` is approved. This is a short guarded window, not a general +license to write anywhere — everything below is bounded by the sentinel and the routing +rule. + +1. **Arm the sentinel**: `touch "/tmp/claude-design-active.$CLAUDE_CODE_SESSION_ID"` (this + is what `hooks/design-scope-guard.sh` checks for). If `$CLAUDE_CODE_SESSION_ID` is + empty, fall back to `touch /tmp/claude-design-active` and warn the user that session + isolation is inactive for this run. +2. **Resolve the project root**: `ROOT=$(git rev-parse --show-toplevel 2>/dev/null)`. +3. **Routing rule** — decide whether artifacts stay in the plans dir or promote into the + project: + - If `ROOT` equals `${CLAUDE_CONFIG_DIR:-$HOME/.claude}` (this repo IS the config + repo — a meta-tooling design), OR `git -C "$ROOT" check-ignore -q docs/plans/probe` + reports that class of path as gitignored, the project doesn't want these files + tracked — **artifacts stay in the plans dir**, and the Step 7 checkpoint says so. + Always root-anchor the `check-ignore` probe (`git -C "$ROOT" check-ignore -q + docs/plans/probe`), never a cwd-relative one — a cwd-relative probe run from a + subdirectory can false-positive. + - Otherwise, promote: `mkdir -p "$ROOT/docs/plans/"`. +4. **Write the artifact set** (promotion path only) **with the `Write` tool — never + `cp`** — so the write goes through the design-scope-guard and protect-secrets rails + like every other artifact write in this skill: + - The two core files: `-design-draft.md` and `-tasks.md`. + - Every `-research-*.md` file present in the plans dir (a bounded glob on the + exact slug prefix — `login-research-*` cannot cross-match `login-flow-research-*` — + so legitimate extra research variants are captured without over-matching a + similarly-named design). +5. **Self-check gate**: only stamp promotion if BOTH hold — + - the two core files exist in `$ROOT/docs/plans//` and are non-empty, AND + - the count of `-research-*.md` files written into the project equals the count + of `-research-*.md` source files in the plans dir. + If the gate passes, stamp each **plans-dir** source draft (not the project copies) with + this as its first line: + `> PROMOTED to $ROOT/docs/plans// — the project copy is canonical.` + If any write fails, or the gate does not pass: report the failure, leave the plans dir + as the canonical copy, and stamp nothing. +6. **No repo at all** (`ROOT` empty): ask the user whether to run `git init -b main && git + commit --allow-empty -m "chore: initial commit"` so promotion has somewhere to land, or + to stay in the plans dir for this design. +7. **Disarm the sentinel** before stopping: `rm -f "/tmp/claude-design-active.$CLAUDE_CODE_SESSION_ID"` + (or the unsuffixed fallback path if that's what was armed). + +## Step 7: Human Checkpoint (the one stop) + +This is the single STOP for the whole `/design` run — it comes after the write, not +before it. Print the artifact directory path verbatim (`$ROOT/docs/plans//` if +promoted, otherwise the plans dir) and name the two files to open first: +`-design-draft.md`, then `-tasks.md`. If `$ROOT` is under `$HOME/Documents/`, +note that this location is phone-readable (e.g. via a synced Files app) — omit that note +otherwise. + +> "Design + task plan written to `[path]`. Open `-design-draft.md` first, then +> `-tasks.md`. Exiting plan mode above was provisional — `/build ` is still +> the real decision point. These files are untracked until `/build` commits them, so +> avoid a bulk `git add .` in the meantime. > What would you like to adjust before I implement?" -**This is the exit-plan-mode boundary.** Do not proceed to implementation and do not -exit plan mode yourself — the user approves, exits plan mode, and runs `/build` (execute -mode) when ready. - ## Iteration Protocol (post-checkpoint human feedback) -If the user gives feedback: + +Two paths, depending on which checkpoint the feedback arrives at: + +**Pre-promotion** (feedback given when the user rejects at the Step 5 `ExitPlanMode` +call) — nothing has been written to the project yet, so iterate exactly as before this +feature, entirely on the plans-dir copies: 1. For feedback that introduces a new unknown, spawn a `researcher` and update the relevant research file. 2. Revise `-design-draft.md`. 3. **Regenerate `-tasks.md`** (Step 3b) so it never lags an edited draft. 4. Re-run Step 4 (the convergence review loop, including its `direction-reviewer` pass) on the revised draft + tasks. -5. Present and STOP again. A `REDIRECT` from the re-run is **folded into this single Step 5 checkpoint** (Step 4 already surfaces it as a flagged human decision) — it does not create a second, separate stop. The human sees exactly one checkpoint per iteration cycle, consistent with the two-checkpoint rule. Repeat until the user says to proceed. +5. Return to Step 5 (present + `ExitPlanMode` again). A `REDIRECT` from the re-run is **folded into this single checkpoint** (Step 4 already surfaces it as a flagged human decision) — it does not create a second, separate stop. Repeat until the user approves at `ExitPlanMode`. + +**Post-promotion** (feedback given at the Step 7 checkpoint, after artifacts were written +into the project) — ALL references re-point to the project copies: revisions, Step 3b +regeneration, and the Step 4 re-review reviewer prompts all use +`$ROOT/docs/plans//-*.md` paths, never the plans dir. The plans-dir drafts are +now inert bannered scratch (Step 6's `PROMOTED` stamp) — a re-review must never converge +on them. +1. Re-arm the sentinel for this revision turn (Step 6.1). +2. Run steps 1–4 of the pre-promotion path above, but reading and writing + `$ROOT/docs/plans//-*.md` instead of the plans dir. +3. Present the revised checkpoint (Step 7) and STOP again; disarm the sentinel (Step 6.7) + before stopping. Repeat until the user says to proceed. From 607512f63563be755661c810371a807f106a3fb7 Mon Sep 17 00:00:00 2001 From: Victor Kzam Date: Fri, 24 Jul 2026 12:18:27 +0100 Subject: [PATCH 05/19] chore(setup): register the design-scope guard in example settings Co-Authored-By: Claude --- settings.example.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/settings.example.json b/settings.example.json index d32b761..4b06508 100644 --- a/settings.example.json +++ b/settings.example.json @@ -46,6 +46,11 @@ "type": "command", "command": "bash \"${CLAUDE_CONFIG_DIR:-$HOME/.claude}/hooks/orchestrator-delegate-guard.sh\"", "timeout": 5 + }, + { + "type": "command", + "command": "bash \"${CLAUDE_CONFIG_DIR:-$HOME/.claude}/hooks/design-scope-guard.sh\"", + "timeout": 5 } ] } From a96dbf08426b425643b44a2fb667f186d2c59681 Mon Sep 17 00:00:00 2001 From: Victor Kzam Date: Fri, 24 Jul 2026 12:19:31 +0100 Subject: [PATCH 06/19] test(hooks): cover the design-scope guard and the widened delegate-guard allowlist Co-Authored-By: Claude --- tests/hooks.bats | 76 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/tests/hooks.bats b/tests/hooks.bats index b98dc6e..b246221 100644 --- a/tests/hooks.bats +++ b/tests/hooks.bats @@ -9,12 +9,19 @@ setup() { HOOK="${BATS_TEST_DIRNAME}/../hooks/orchestrator-delegate-guard.sh" + DESIGN_HOOK="${BATS_TEST_DIRNAME}/../hooks/design-scope-guard.sh" # Unique, throwaway session id/sentinel per test -- never the real global sentinel. SID="bats-guard-$$-${RANDOM}${RANDOM}" SENTINEL="/tmp/claude-orchestrator-active.${SID}" touch "$SENTINEL" + # design-scope-guard.sh sentinel -- distinct filename prefix from $SENTINEL above, + # so the two guards' sentinels never collide. NOT touched by default: tests that + # need /design "active" call design_sentinel_on() explicitly; tests that need it + # absent (the fast-path case) simply never call it. Always removed in teardown. + DESIGN_SENTINEL="/tmp/claude-design-active.${SID}" + # Fake HOME/CLAUDE_CONFIG_DIR sandboxes MUST NOT live under /tmp: the hook's # own "/tmp/*" allowlist rule (line ~59) would then match any file path # inside them and exit 0 before the CLAUDE_CONFIG_DIR branch under test is @@ -39,7 +46,7 @@ setup() { } teardown() { - rm -f "$SENTINEL" + rm -f "$SENTINEL" "$DESIGN_SENTINEL" # Guard against an empty/unset SANDBOX_ROOT (e.g. setup aborted before it was # set) turning `rm -rf` into a no-op-on-cwd footgun. [ -n "${SANDBOX_ROOT:-}" ] && rm -rf "$SANDBOX_ROOT" @@ -52,6 +59,19 @@ json_for() { printf '{"session_id":"%s","tool_input":{"file_path":"%s"}}' "$SID" "$1" } +# Turns the design-scope-guard sentinel "on" for the current test's session id. +design_sentinel_on() { + touch "$DESIGN_SENTINEL" +} + +# Same stdin shape as json_for(), plus tool_name -- design-scope-guard.sh does not +# read tool_name at all (it only inspects tool_input.file_path), so a Write vs Edit +# probe on the same file_path must produce identical exit codes. That parity is +# what the "source-path probes" test below asserts. +json_for_design() { + printf '{"session_id":"%s","tool_name":"%s","tool_input":{"file_path":"%s"}}' "$SID" "$2" "$1" +} + @test "plan-file edit under \$HOME/.claude/plans is allowed (default layout)" { run bash "$HOOK" <<< "$(json_for "$TEST_HOME/.claude/plans/adopter-setup-tasks.md")" [ "$status" -eq 0 ] @@ -75,3 +95,57 @@ json_for() { [ "$status" -eq 2 ] [[ "$output" == *"Blocked"* ]] } + +@test "delegate-guard: docs/plans/ write is allowed while sentinel is active" { + run bash "$HOOK" <<< "$(json_for "$TEST_HOME/proj/docs/plans/some-slug/draft.md")" + [ "$status" -eq 0 ] +} + +@test "delegate-guard: .gitattributes write is allowed while sentinel is active" { + run bash "$HOOK" <<< "$(json_for "$TEST_HOME/proj/.gitattributes")" + [ "$status" -eq 0 ] +} + +@test "delegate-guard: near-miss docs/plansX/ (not docs/plans/) is still blocked" { + run bash "$HOOK" <<< "$(json_for "$TEST_HOME/proj/docs/plansX/evil.md")" + [ "$status" -eq 2 ] + [[ "$output" == *"Blocked"* ]] +} + +# --- hooks/design-scope-guard.sh --------------------------------------------- +# Same sandbox/sentinel-isolation conventions as above, applied to the sibling +# guard: a per-test session id, a scratch sentinel file, and (per this file's +# own note at setup()'s SANDBOX_ROOT check) fixture paths rooted outside /tmp +# so the hook's own "/tmp/*" allowlist branch can't make an assertion vacuous. + +@test "design-scope-guard: docs/plans/ write under a project fixture is allowed while sentinel is active" { + design_sentinel_on + FIXTURE="$SANDBOX_ROOT/proj/docs/plans/some-slug/draft.md" + run bash "$DESIGN_HOOK" <<< "$(json_for_design "$FIXTURE" "Write")" + [ "$status" -eq 0 ] +} + +@test "design-scope-guard: source-path probe is blocked identically for Write and Edit tool shapes" { + design_sentinel_on + FIXTURE="$SANDBOX_ROOT/proj/src/main.py" + + run bash "$DESIGN_HOOK" <<< "$(json_for_design "$FIXTURE" "Write")" + [ "$status" -eq 2 ] + write_output="$output" + + run bash "$DESIGN_HOOK" <<< "$(json_for_design "$FIXTURE" "Edit")" + [ "$status" -eq 2 ] + + # Note: issue #37210 documents a harness-side runtime caveat around how the + # test harness replays Write vs Edit shapes -- it is not a defect in this + # hook. The hook itself never reads tool_name, so both shapes must agree. + [ "$output" = "$write_output" ] +} + +@test "design-scope-guard: fast path allows any write when no design sentinel exists" { + # Deliberately do NOT call design_sentinel_on() -- exercises the jq-free + # `ls /tmp/claude-design-active*` fast path taken when /design isn't active. + FIXTURE="$SANDBOX_ROOT/proj/src/main.py" + run bash "$DESIGN_HOOK" <<< "$(json_for_design "$FIXTURE" "Write")" + [ "$status" -eq 0 ] +} From 586d3acc6a04aeeaec943acf709dc114773719bb Mon Sep 17 00:00:00 2001 From: Victor Kzam Date: Fri, 24 Jul 2026 12:21:45 +0100 Subject: [PATCH 07/19] feat(build-skill): consume project design artifacts and commit them at build start Co-Authored-By: Claude --- skills/build/SKILL.md | 93 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 78 insertions(+), 15 deletions(-) diff --git a/skills/build/SKILL.md b/skills/build/SKILL.md index fdcf49c..a92a7cf 100644 --- a/skills/build/SKILL.md +++ b/skills/build/SKILL.md @@ -5,6 +5,8 @@ disable-model-invocation: true argument-hint: -tasks.md> allowed-tools: - Read + - Write + - Edit - Glob - Grep - Agent @@ -42,16 +44,58 @@ $ARGUMENTS ## Phase 0: Pre-flight (orchestrator, main thread) 1. **Resolve the tasks file** (`$ARGUMENTS`): - - explicit slug or path argument → `~/.claude/plans/-tasks.md` (or the given path). - - no argument → glob `~/.claude/plans/*-tasks.md`, pick the newest by mtime, **show the match and confirm with the user before proceeding** (do not silently guess). - - no tasks file exists → "No `-tasks.md` found — run `/design` to generate one, or confirm you want me to decompose ad hoc." Do not invent a fixed task count. -2. Check git status — ensure clean working tree or stash changes. + - `ROOT=$(git rev-parse --show-toplevel 2>/dev/null)`. + - explicit slug or path argument → check the **project copy first**: + `$ROOT/docs/plans//-tasks.md`. This only counts if the pair is + **COMPLETE** — `-tasks.md` AND `-design-draft.md` both present and + non-empty. A partial pair (an interrupted `/design` Step 6 write — one file present, + the other missing or empty) is not usable: warn the user and fall back to the legacy + `~/.claude/plans/-tasks.md` (or the given path) instead. Never silently mix a + partial project copy with a plans-dir remainder. + - no argument → glob the **project location first**: `$ROOT/docs/plans/*/*-tasks.md`. + If that glob has no matches, fall back to the **legacy plans dir**: + `~/.claude/plans/*-tasks.md`. Pick the newest by mtime within whichever glob actually + matched, **show the match and confirm with the user before proceeding** (do not + silently guess). + - no tasks file exists in either location → "No `-tasks.md` found — run + `/design` to generate one, or confirm you want me to decompose ad hoc." Do not invent + a fixed task count. +2. Check git status — the working tree must be clean, but **tolerate untracked or + modified files under `docs/plans//`** (those are the artifacts step 1 just + resolved, not build drift). Everything else must be clean or stashed. If stashing, use + **plain `git stash` only — NEVER `git stash -u` or `-a`**: either flag would also sweep + the untracked `docs/plans//` artifacts into the stash, so step 4 below would see + no delta and silently skip the docs commit. 3. Create feature branch from main: `feat/` or `fix/`. -4. Read project CLAUDE.md for coding conventions. -5. **Allowlist-coverage check**: if permission prompts have come up frequently this +4. **Commit the design artifacts as the leading docs commit** — gated: this step only + runs when step 1 resolved a **COMPLETE project copy** at `$ROOT/docs/plans//` in + THIS repo. A plans-dir-resolved build (the normal meta-tooling case, where there is no + project copy to promote) **skips this whole step with a notice** — `git add` on a + directory that was never created is a fatal pathspec error, and committing a set the + build didn't actually read from would diverge from the source of truth. + - Non-repo (`ROOT` empty) → skip with a notice. + - **Root-anchored trackability pre-check**: `git -C "$ROOT" check-ignore -q + "docs/plans//probe"` — always root-anchored, never a cwd-relative probe (a + cwd-relative probe run from a subdirectory can false-positive). Ignored → warn and + skip; never `git add -f` to force past a project's own `.gitignore`. + - Ensure `docs/plans/** linguist-generated=true` is present in `$ROOT/.gitattributes` + (add it via `Edit`/`Write` if missing). + - `git add "$ROOT/docs/plans//"`, plus `.gitattributes` if it was just touched — + **never a bare `git add .`**. + - Make **ONE** commit: `docs(): add design + research artifacts`, with the + standard co-author trailer (`Co-Authored-By: Claude `, per + CLAUDE.md). + - No delta to commit (e.g. a prior run already committed it) → skip silently. + - Commit command fails → `git restore --staged`, print the failure, and continue with + the artifacts left untracked — no retry loop. + This is an **orchestrator-authored commit, not a task commit** — it is excluded from + the one-commit-per-task accounting everywhere that accounting happens (Phase 2→3 gate, + Phase 3 reviewer commit check, Phase 5 series line — see the notes at each). +5. Read project CLAUDE.md for coding conventions. +6. **Allowlist-coverage check**: if permission prompts have come up frequently this session, suggest running `/fewer-permission-prompts` before spawning implementers — a well-populated allowlist keeps delegation moving without repeated interruptions. -6. **Write the session-scoped delegation sentinel:** +7. **Write the session-scoped delegation sentinel:** `touch "/tmp/claude-orchestrator-active.$CLAUDE_CODE_SESSION_ID"` (this env var matches the `session_id` the guard hook reads from stdin; requires Claude Code ≥ v2.1.132 — if `$CLAUDE_CODE_SESSION_ID` is empty, fall back to @@ -79,7 +123,7 @@ whole `-tasks.md`. The prompt includes: 3. **`files_owned`** — the implementer reads current contents itself (do not paste file contents) 4. **A reference pattern**: "Follow the pattern in [existing_file:symbol]" 5. **The task's `verification`** — exact command(s) + expected result -6. **The task's `commit:` line** and this instruction: *"When the code is complete and `verification` passes, make exactly ONE atomic commit using this `commit:` line (Conventional format, inheriting CLAUDE.md's generic co-author trailer — never hardcode a model-specific trailer), committing only the files in `files_owned`. One task = one commit. Do NOT push."* +6. **The task's `commit:` line** and this instruction: *"When the code is complete and `verification` passes, make exactly ONE atomic commit using this `commit:` line (Conventional format, inheriting CLAUDE.md's generic co-author trailer — never hardcode a model-specific trailer), committing only the files in `files_owned`. One task = one commit. Do NOT push."* — **`commit: none`** (external/unversioned target, e.g. a task that edits a live config directory outside this repo): OMIT this instruction entirely. Do not tell the implementer to commit anything for that task; its work is verification-only. 7. **A required exit report**: instruct the implementer that its final message must end with a fenced JSON block matching exactly this schema: ```json @@ -99,14 +143,24 @@ quality-critical tasks where the work should not be scoped to a budget. ## Phase 2→3 Gate: Verify against ground truth (orchestrator, main thread) Before any reviewer is spawned, the orchestrator itself checks the implementer's exit report against ground truth — never proceeds on testimony alone: -- Run `git log --oneline main..HEAD` (or the task's expected range) and confirm - `commit_sha` exists, is Conventional-format, and touches only the task's `files_owned`. -- Re-run the task's `verification` command yourself and compare the real output against - the reported `verification_output`. +- **`commit: none` tasks**: skip the commit checks entirely — verify by **re-running the + task's `verification` command only** and comparing the real output against the reported + `verification_output`. There is no `commit_sha` and no `main..HEAD` check to perform; + the task has no commit to have produced. +- **All other tasks**: run `git log --oneline main..HEAD` (or the task's expected range) + and confirm `commit_sha` exists, is Conventional-format, and touches only the task's + `files_owned`. Also re-run the task's `verification` command yourself and compare the + real output against the reported `verification_output`. - If `status != "success"`, or the report doesn't match ground truth (commit missing, verification actually fails, files touched outside `files_owned`), treat the task as failed: do not proceed to Phase 3 — fold this into Phase 4's iteration logic instead (or escalate immediately if the iteration budget is already exhausted). +- **Note**: any `docs(): add design + research artifacts` commit from Phase 0 step 4 + is orchestrator-authored, not a task commit — exclude it from every commit-accounting + check above (`main..HEAD` may legitimately contain it in addition to the task's own + commit; it does not count against the one-commit-per-task expectation, and this is not + limited to a "leading" commit — a refresh committed later on `continue` is excluded the + same way). ## Phase 3: Review (delegate to reviewer subagent) Only after the ground-truth gate passes, spawn a **reviewer** subagent. @@ -135,7 +189,7 @@ The reviewer prompt includes: 5. **Reproduce-before-report**: "Every finding must include a file:line AND either a concrete failing command or a specific reproducible failure scenario. Findings without evidence are dropped — do not include them in your verdict." -6. **Commit check:** "Confirm the task produced exactly one commit (`git log --oneline main..HEAD`), in Conventional format, with CLAUDE.md's co-author trailer, touching only `files_owned`. Flag a missing commit, a squashed/multi-task commit, or a malformed message." +6. **Commit check:** "Confirm the task produced exactly one commit (`git log --oneline main..HEAD`), in Conventional format, with CLAUDE.md's co-author trailer, touching only `files_owned`. Flag a missing commit, a squashed/multi-task commit, or a malformed message. Exclude any `docs(): add design + research artifacts` commit from this check — that commit is orchestrator-authored (Phase 0 step 4), not a task commit, and may legitimately be present alongside the task's own commit." — **skip this check entirely for a `commit: none` task**: there is no task commit to confirm. 7. **Required structured verdict**: instruct the reviewer that its final message must end with a fenced JSON block matching exactly this schema: ```json @@ -160,7 +214,8 @@ Based on the reviewer's verdict: - **NEEDS WORK** (iteration 1-2): Spawn a NEW implementer subagent with: - The reviewer's specific issues (file:line + description) - The original design requirements - - Instruction: "Fix these specific issues. Do not refactor beyond what's listed. Fold the fix into THIS task's existing commit (`git commit --amend` or an autosquash fixup), so the series stays exactly one atomic commit per task — do not add a second commit for the same task." + - Instruction (task's `commit:` is NOT `none`): "Fix these specific issues. Do not refactor beyond what's listed. Fold the fix into THIS task's existing commit (`git commit --amend` or an autosquash fixup), so the series stays exactly one atomic commit per task — do not add a second commit for the same task." + - Instruction (task's `commit:` IS `none`): "Fix these specific issues by re-editing the owned files only. Do not run `git commit` or `git commit --amend` — this task has no commit of its own, and amending would rewrite an unrelated prior commit on the target (e.g. a live config repo's own history on its `main`)." - Then re-run the reviewer - **NEEDS WORK** (iteration 3): STOP and escalate to the user with: - What was implemented @@ -173,7 +228,13 @@ Max 3 implement-review iterations. After 2 failed corrections, the problem is us ## Phase 5: Human Checkpoint Present a summary to the user: - **What was built**: Files created/modified with brief descriptions -- **Commit series**: `git log --oneline main..HEAD` — one atomic Conventional commit per task, each with the co-author trailer (this is what `/ship` will verify, not rewrite) +- **Committed design artifacts**: if Phase 0 step 4 ran, list the files in the + `docs(): add design + research artifacts` commit (the design draft, tasks file, + and any research files) — this commit is separate from the task series below. +- **Commit series**: `git log --oneline main..HEAD` — one atomic Conventional commit per + task (tasks with `commit: none` contribute no commit here — that's expected, not a + gap), each with the co-author trailer, **excluding** the orchestrator-authored + `docs():` artifacts commit above (this is what `/ship` will verify, not rewrite) - **Design deviations**: Any places the implementer diverged and why - **Review result**: Final reviewer verdict + any remaining nits - **Test results**: Pass/fail with details @@ -196,4 +257,6 @@ Do not proceed to PR creation. the user will run /ship when ready. - Do NOT proceed to Phase 3 on the implementer's exit report alone — verify `commit_sha` and re-run `verification` yourself first - Do NOT reuse the implementer's model for review — the reviewer must be the counter-model - Do NOT treat NO_VERDICT as NEEDS_WORK — retry fresh once, then escalate; it must never consume a fix-iteration +- Do NOT `git commit --amend` a fix for a `commit: none` task — it has no commit of its own; amending would rewrite an unrelated prior commit on the target repo. Re-edit the owned files only. +- Do NOT `git add .` or `git add -f` when committing the Phase 0 design-artifacts commit — pathspec the artifact dir explicitly, and never force past a project's `.gitignore` - After 2 failed iterations, the problem is usually spec ambiguity — escalate to the user From a5363d04f2904f55cf20d748eb5e5ed90412f5f2 Mon Sep 17 00:00:00 2001 From: Victor Kzam Date: Fri, 24 Jul 2026 16:05:00 +0100 Subject: [PATCH 08/19] feat(claude-md): document project-local design artifacts in the workflow rules Co-Authored-By: Claude --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 38e0e94..9313341 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,7 @@ PM-turned-engineer. Quality and learning over speed. Always explain WHY behind a - Two human checkpoints are non-negotiable: after design, before PR. - Each task maps to exactly one atomic Conventional Commit, made by the implementer that did the task. `/ship` verifies the series; it never creates a catch-all or squashed commit. - A PR is opened only after the feature/milestone is fully built AND the quality gate has passed. No PR on a red gate; no PR mid-build. -- Mode transitions: `/design` runs in plan mode (may write only `~/.claude/plans/-*.md`); the post-design checkpoint is the exit-plan-mode point; `/build` and `/ship` run in execute mode. +- Mode transitions: `/design` runs in plan mode; on plan approval it writes the artifact set to `/docs/plans//` (project copy canonical; plans-dir drafts banner-stamped; plans dir itself for non-repo/meta-tooling work); the post-design checkpoint precedes `/build`; `/build` commits the set as its leading `docs:` commit on the feature branch; `/build` and `/ship` run in execute mode. - `/ship` ends by offering `/compound`. Process-rule edits (CLAUDE.md, `.claude/rules`) are never committed into the feature PR. - Model routing and workflow usage follow ~/.claude/rules/orchestration.md. From cf2c89e8c863d83684cab55476d1cd63794af6fd Mon Sep 17 00:00:00 2001 From: Victor Kzam Date: Fri, 24 Jul 2026 16:05:33 +0100 Subject: [PATCH 09/19] docs(skills): reference project design artifacts in ship and compound Co-Authored-By: Claude --- skills/compound/SKILL.md | 5 ++++- skills/ship/SKILL.md | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/skills/compound/SKILL.md b/skills/compound/SKILL.md index 97aa55f..c3c8918 100644 --- a/skills/compound/SKILL.md +++ b/skills/compound/SKILL.md @@ -23,7 +23,10 @@ $ARGUMENTS ## Step 1: Establish the shipped scope (main thread) 1. Determine the range: use `$ARGUMENTS` if given (a commit range or PR number); otherwise `git log --oneline -20` and identify the most recent merged/feature span. 2. `git diff --stat ` and `git log ` to see what changed and why. -3. Locate the design artifact if one exists (`~/.claude/plans/-design-draft.md` or a `docs/` design doc) and any `/design` review-loop findings recorded with it. +3. Locate the design artifact if one exists — check the project copy first, + `docs/plans//-design-draft.md` (canonical once `/build` has committed it), + then fall back to the legacy `~/.claude/plans/-design-draft.md` — and any + `/design` review-loop findings recorded with it. ## Step 2: Extract lessons (one delegated subagent) Spawn a single **`general-purpose`** subagent, model `sonnet`, read-only behavior (instruct it not to modify files), synchronously. Give it: the commit range, the diff/log, the design doc path, and the review-loop findings. Ask it to return ONLY structured JSON: diff --git a/skills/ship/SKILL.md b/skills/ship/SKILL.md index 351de81..d2210e5 100644 --- a/skills/ship/SKILL.md +++ b/skills/ship/SKILL.md @@ -40,10 +40,22 @@ $ARGUMENTS `git log --oneline main..HEAD`. The series must **already** be one atomic Conventional commit per task, each with the `Co-Authored-By` trailer (produced by `/build`'s implementers). +- Any orchestrator-authored `docs(): add design + research artifacts` commit + (produced by `/build` Phase 0 step 4's promotion of the project's design docs) is an + **expected series member** — it does not count against one-commit-per-task, and it does + not have to be the leading commit; a later refresh of the artifacts can land anywhere in + the range. Do not flag it as a stray or extra commit. - **Do NOT create a catch-all "final commit"** and do NOT squash the per-task series. - If there are stray **uncommitted** changes, **STOP and report** — that means a `/build` task did not self-commit; do not paper over it with a squash commit. The fix is to finish/redo that task in `/build`, not to absorb it here. + - **Targeted exception**: untracked files under `docs/plans//` with **no** + `docs():` commit anywhere in the series is the documented ceiling state for a + `/build` that couldn't commit the design artifacts (e.g. the root-anchored + trackability pre-check skipped them, or the commit itself failed — see `/build` Phase + 0 step 4). This is not a silently-skipped task commit — offer to commit the artifacts + now (`docs(): add design + research artifacts`, same trailer) rather than + hard-stopping the ship over it. - Verify (do not rewrite) format and atomicity. If messages are messy, *suggest* an interactive rebase but never force one. @@ -113,6 +125,11 @@ and proceed straight to Step 4. ## Design Reference [link to design doc or plan file if applicable] ``` + For **Design Reference**, point to the committed project copy — + `docs/plans//-design-draft.md` (canonical once `/build` Phase 0 step 4 + has committed it as part of the feature branch) — falling back to the legacy + `~/.claude/plans/-design-draft.md` only when no project copy was ever promoted + (e.g. a plans-dir-resolved build with no project docs to commit). 3. Report PR URL to the user ## Step 5: Offer `/compound` (HITL chain) From 7cf8452a0e96144a3a7c7ca1218afd518c1b7d0c Mon Sep 17 00:00:00 2001 From: Victor Kzam Date: Fri, 24 Jul 2026 16:07:40 +0100 Subject: [PATCH 10/19] fix(check): verify the design-scope guard in the post-install doctor and merge tests Co-Authored-By: Claude --- scripts/check.sh | 2 +- scripts/merge-settings.sh | 11 ++++++----- tests/check.bats | 5 +++-- tests/merge-settings.bats | 8 ++++---- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/scripts/check.sh b/scripts/check.sh index 0d4d617..b2fa246 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -13,7 +13,7 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd -P )" MANIFEST="$SCRIPT_DIR/manifest.txt" CLAUDE_MD_MARKER='