Просмотр исходного кода

Plans as directories, and a boundary check before each next plan

A plan is a directory: 00-header.md plus one NN-<task>.md per task. The
brief for task N is the header plus that file; sdd-workspace, task-brief,
review-package, task-start and task-done take a file or a directory.
Before starting the next plan in a set, executors run
scripts/plan-boundary, which names every identifier the plan consumes
from earlier plans that the code as built does not contain, in the next
plan and in every later plan's Plan Set entry, and fix the plan until it
prints clean. Measured: the directory form executes the same as the file
form (inline 9/9, SDD 6/6, same cost) and plans at the same volume; the
gate kept a five-plan set consistent with the code 2/2 against a planted
cross-plan naming conflict, where a per-ruling duty managed 0/2.
Jesse Vincent 2 недель назад
Родитель
Сommit
cffb7c95b6

+ 16 - 6
skills/executing-plans/SKILL.md

@@ -107,6 +107,11 @@ digraph process {
 
 ## Setup
 
+A plan is one file, or a directory (`00-header.md` plus one `NN-<task>.md`
+per task, the form writing-plans writes). Every script below takes either;
+"the plan" means the whole of it, and "the header" means the part above
+Task 1 or `00-header.md`.
+
 Ensure the work happens in an isolated workspace: use
 superpowers:using-git-worktrees to create one or verify the existing one.
 Never start implementation on a main/master branch without your human
@@ -123,7 +128,7 @@ The workspace and ledger are shared with superpowers:subagent-driven-development
 and the new one resumes from the same ledger.
 
 - Each plan owns a workspace: at skill start, run
-  `../subagent-driven-development/scripts/sdd-workspace PLAN_FILE` — it
+  `../subagent-driven-development/scripts/sdd-workspace PLAN` — it
   prints the plan's git-ignored directory
   (`<repo-root>/.superpowers/sdd/<plan-basename>/`), home to every
   artifact for THIS plan: ledger, briefs, review packages. Another plan's
@@ -136,7 +141,7 @@ and the new one resumes from the same ledger.
   recollection. A ledger whose first line names a different plan file is
   another plan's progress: leave it and start your own, fresh.
 - Create the ledger with its identity as the first line:
-  `# SDD ledger — plan: <plan file path>`.
+  `# SDD ledger — plan: <plan path>`.
 - `git clean -fdx` will destroy the workspace (it's git-ignored scratch);
   if that happens, recover from `git log`.
 
@@ -169,7 +174,7 @@ in the workspace and read its tail; read a brief, not the whole plan.
 
 ### 1. Take the task
 
-- Run this skill's `scripts/task-start PLAN_FILE N`. It prints the brief
+- Run this skill's `scripts/task-start PLAN N`. It prints the brief
   path and BASE (the commit the task's range is cut from) in one call.
   Read the brief for every task, including ones you remember from setup:
   what you remember is a summary, the brief has the exact values,
@@ -221,7 +226,7 @@ the claim. If any item is missing, the task is not complete: finish it.
 
 ### 4. Complete the task
 
-Run this skill's `scripts/task-done PLAN_FILE N BASE -- <test command>`
+Run this skill's `scripts/task-done PLAN N BASE -- <test command>`
 with the test command the brief names for the whole task. It runs the
 tests, keeps the full output in the workspace, prints the tail, and — only
 if they pass — appends the completion line to the ledger:
@@ -233,7 +238,7 @@ mark the todo complete and take the next task.
 
 ## Final Review
 
-Run `../subagent-driven-development/scripts/review-package PLAN_FILE MERGE_BASE HEAD`
+Run `../subagent-driven-development/scripts/review-package PLAN MERGE_BASE HEAD`
 (MERGE_BASE = the commit the branch started from, e.g.
 `git merge-base main HEAD`) and review from the file it prints.
 
@@ -301,7 +306,12 @@ partner's behalf — and the findings you chose not to act on — reach them.
 
 When a plan follows this one in the Plan Set, finishing this plan means
 starting that one, under the same method and in this session: the set was
-reviewed once, and only the four stops stop you.
+reviewed once, and only the four stops stop you. Before its Task 1, run
+`scripts/plan-boundary NEXT_PLAN` (this skill's directory): it names
+every identifier the plan's Consumes lines take from earlier plans that
+the code as built does not contain. Fix each one in the plan file, ledger
+it as a ruling, and re-run until it prints `boundary: clean`. The plan runs
+as written after that, so a name it still gets wrong is executed wrong.
 
 When the final review is clean and its fixes are committed, delete this
 plan's workspace directory — the git history is the record now. Sibling

+ 59 - 0
skills/executing-plans/scripts/plan-boundary

@@ -0,0 +1,59 @@
+#!/usr/bin/env bash
+# Check the next plan against the code as built before starting it.
+#
+# Collects every backticked identifier in the plan's task-level `Consumes`
+# lines and in its own Plan Set entry, drops the ones this plan itself
+# produces (its `Produces` lines and the functions/types its code blocks
+# define), and reports each remaining name that does not occur in the
+# repository's source outside plans/ and docs/. Then, for every plan that
+# comes after it in the same directory, checks the names its Plan Set entry
+# attributes to plans already complete ("from plan K: ...", K before the next
+# plan), so a ruling in the plan just finished cannot leave a later plan
+# consuming a name the code does not have. A hit is a plan that says what the
+# code does not: fix the plan, re-run until it prints "boundary: clean".
+#
+# A plan is a file or a directory (00-header.md plus NN-<task>.md files).
+#
+# Usage: plan-boundary NEXT_PLAN
+# Exit: 0 when clean, 1 when any consumed name is missing, 2 on usage error.
+set -euo pipefail
+[ $# -eq 1 ] || { echo "usage: plan-boundary NEXT_PLAN" >&2; exit 2; }
+next=${1%/}; [ -e "$next" ] || { echo "no such plan: $next" >&2; exit 2; }
+root=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
+stop='^(and|or|the|from|of|in|a|an|to|with|for|int|string|bool|byte|rune|error|nil|true|false|func|type|struct|var|const|Plan|Task|Tasks|plan|plans|md|s|m|g|t|x|y|dt|None|self|str|list|dict|float|Decimal|date|datetime|time|Duration)$'
+idents() {  # backticked spans on stdin -> identifiers, one per line; paths and files skipped
+  grep -o '`[^`]*`' | tr -d '`' | grep -v -e '/' -e '\.md$' | grep -o '[A-Za-z_][A-Za-z0-9_]*' | sort -u
+}
+plan_text() { if [ -d "$1" ]; then cat "$1"/00-header.md "$1"/[0-9][0-9]-*.md 2>/dev/null; else cat "$1"; fi; }
+in_code() { git -C "$root" grep -q -w -- "$1" -- ':!plans/*' ':!docs/*' ':!*.md' 2>/dev/null; }
+missing=0; found=0
+check() {  # names on stdin, label
+  local n; while IFS= read -r n; do [ -n "$n" ] || continue
+    if in_code "$n"; then found=$((found+1)); else echo "missing in code: $n  (consumed by $1)"; missing=1; fi
+  done
+}
+# 1. the next plan: task-level Consumes plus its own Plan Set entry, minus what it produces
+name=$(basename "$next"); text=$(plan_text "$next")
+body=$(printf '%s\n' "$text" | awk '/^## Plan Set/{skip=1;next} /^## /{skip=0} !skip')
+own=$(printf '%s\n' "$text" | awk '/^## Plan Set/{f=1;next} /^## /{f=0} f' | grep -F -- "$name" || true)
+consumes=$( { printf '%s\n' "$body" | grep -i -A8 'Consumes'; printf '%s\n' "$own"; } | idents | grep -v -E "$stop" || true)
+produces=$( { printf '%s\n' "$body" | grep -i -A8 'Produces' | idents; printf '%s\n' "$body" | awk '/^```/{f=!f;next} f' | grep -o -E '^(func(tion)? (\([^)]*\) )?[A-Za-z_][A-Za-z0-9_]*|type [A-Za-z_][A-Za-z0-9_]*|class [A-Za-z_][A-Za-z0-9_]*|def [A-Za-z_][A-Za-z0-9_]*)' | awk '{print $NF}'; } | sort -u || true)
+check "$name" < <(comm -23 <(echo "$consumes") <(echo "$produces"))   # no pipe: check must set missing/found in this shell
+# 2. every later plan in the same directory: names its Plan Set entry attributes to plans already complete
+parent=$(dirname "$next"); idx=0; k=0
+for p in $(ls -d "$parent"/* 2>/dev/null | sort); do k=$((k+1)); [ "${p%/}" = "$next" ] && idx=$k; done
+if [ "$idx" -gt 0 ]; then k=0
+  for p in $(ls -d "$parent"/* 2>/dev/null | sort); do
+    k=$((k+1)); [ "$k" -gt "$idx" ] || continue
+    pn=$(basename "$p"); entry=$(plan_text "$p" | awk '/^## Plan Set/{f=1;next} /^## /{f=0} f' | grep -F -- "$pn" || true)
+    [ -n "$entry" ] || continue
+    # segments "from plan K: `a`, `b`" (or "Plan K's `a`") for K before the next plan
+    names=$(printf '%s\n' "$entry" | grep -o -i -E "(from plans? [0-9]+[^\`]*|plan [0-9]+'s)[^;.]*" | while IFS= read -r seg; do
+      kk=$(printf '%s' "$seg" | grep -o -E '[0-9]+' | head -1); [ -n "$kk" ] && [ "$kk" -lt "$idx" ] || continue
+      printf '%s\n' "$seg" | idents | grep -v -E "$stop" || true
+    done | sort -u)
+    check "$pn (from plans before $name)" <<< "$names"
+  done
+fi
+[ $missing -eq 0 ] && { echo "boundary: clean ($found consumed names found in code)"; exit 0; }
+exit 1

+ 1 - 1
skills/executing-plans/scripts/task-done

@@ -4,7 +4,7 @@
 # only if the command succeeded — append the completion line to the ledger.
 # A failing command records nothing: the task is not complete.
 #
-# Usage: task-done PLAN_FILE TASK_NUMBER BASE -- TEST_COMMAND [ARGS...]
+# Usage: task-done PLAN TASK_NUMBER BASE -- TEST_COMMAND [ARGS...]   (PLAN: file or directory)
 #   BASE is the SHA task-start printed; the completion line records BASE..HEAD.
 # Exit: the test command's exit status.
 set -euo pipefail

+ 1 - 1
skills/executing-plans/scripts/task-start

@@ -5,7 +5,7 @@
 # cut from. One tool call instead of two, because every call in an inline
 # session is a turn that re-reads the whole context.
 #
-# Usage: task-start PLAN_FILE TASK_NUMBER
+# Usage: task-start PLAN TASK_NUMBER   (PLAN: the plan file, or a plan directory)
 # Prints:
 #   brief: <path to the task's brief file>
 #   base:  <full SHA of HEAD>

+ 17 - 13
skills/subagent-driven-development/SKILL.md

@@ -134,7 +134,7 @@ sequences — the single most expensive failure observed. Track progress in
 a ledger file, not only in todos.
 
 - Each plan owns a workspace: at skill start, run this skill's
-  `bash scripts/sdd-workspace PLAN_FILE` — it prints the plan's git-ignored
+  `bash scripts/sdd-workspace PLAN` — it prints the plan's git-ignored
   directory (under `<repo-root>/.superpowers/sdd/`), home to
   every artifact for THIS plan: ledger, briefs, reports, review packages.
   Another plan's directory is never yours to read or write.
@@ -146,7 +146,7 @@ a ledger file, not only in todos.
   ledger at the old flat path `.superpowers/sdd/progress.md` — is another
   plan's progress: leave it in place and start your own, fresh.
 - Create the ledger with its identity as the first line:
-  `# SDD ledger — plan: <plan file path>`.
+  `# SDD ledger — plan: <plan path>`.
 - The ledger is your recovery map: the commits it names exist in git even
   when your context no longer remembers creating them. After compaction,
   trust the ledger and `git log` over your own recollection.
@@ -249,7 +249,7 @@ Record BASE (`git rev-parse HEAD`) before dispatching — the review package
 and fix-round diffs need it.
 
 - **Task brief:** before dispatching an implementer, run this skill's
-  `bash scripts/task-brief PLAN_FILE N` — it extracts the task's full text to a
+  `bash scripts/task-brief PLAN N` — it extracts the task's full text to a
   uniquely named file and prints the path. Compose the dispatch so the
   brief stays the single source of
   requirements. Your dispatch should contain: (1) one line on where this
@@ -287,7 +287,7 @@ Template: [implementer-prompt.md](implementer-prompt.md)
 
 Implementer subagents report one of four statuses. Handle each appropriately:
 
-**DONE:** Generate the review package (`bash scripts/review-package PLAN_FILE BASE HEAD`, from this skill's directory — it prints the unique file path it wrote; BASE is the commit you recorded before dispatching the implementer — never `HEAD~1`, which silently drops all but the last commit of a multi-commit task), then dispatch the task reviewer with the printed path.
+**DONE:** Generate the review package (`bash scripts/review-package PLAN BASE HEAD`, from this skill's directory — it prints the unique file path it wrote; BASE is the commit you recorded before dispatching the implementer — never `HEAD~1`, which silently drops all but the last commit of a multi-commit task), then dispatch the task reviewer with the printed path.
 
 **DONE_WITH_CONCERNS:** The implementer completed the work but flagged doubts. Read the concerns before proceeding. If the concerns are about correctness or scope, address them before review. If they're observations (e.g., "this file is getting large"), note them and proceed to review.
 
@@ -314,7 +314,7 @@ required. Implementer self-review never replaces the task review; both are
 needed.
 
 - Hand the reviewer its diff as a file: run this skill's
-  `bash scripts/review-package PLAN_FILE BASE HEAD` and pass the reviewer the file path
+  `bash scripts/review-package PLAN BASE HEAD` and pass the reviewer the file path
   it prints (or, without bash: `git log --oneline`, `git diff --stat`,
   and `git diff -U10` for the range, redirected to one uniquely named
   file). The output never enters your own context, and the reviewer sees
@@ -393,7 +393,7 @@ output; dispatch the re-review once all three are present. Name the
 covering test files in the fix message — a one-line fix does not need the
 whole suite.
 
-**The re-review is scoped.** Run `bash scripts/review-package PLAN_FILE FIX_BASE HEAD`
+**The re-review is scoped.** Run `bash scripts/review-package PLAN FIX_BASE HEAD`
 where FIX_BASE is the head the previous review saw, and dispatch
 [re-review-prompt.md](re-review-prompt.md) with the findings list, the
 brief, the report file, and the printed diff path. The re-reviewer verdicts
@@ -445,7 +445,7 @@ parked-with-ruling at the cap.
 ## Final Review
 
 The final whole-branch review gets a package too: run
-`bash scripts/review-package PLAN_FILE MERGE_BASE HEAD` (MERGE_BASE = the commit the
+`bash scripts/review-package PLAN MERGE_BASE HEAD` (MERGE_BASE = the commit the
 branch started from, e.g. `git merge-base main HEAD`) and include the
 printed path in the final review dispatch, so the final reviewer reads
 one file instead of re-deriving the branch diff with git commands. Dispatch
@@ -460,7 +460,7 @@ with the complete findings list — not one fixer per finding.
 Per-finding fixers each rebuild context and re-run suites; a real
 session's final-review fix wave cost more than all its tasks combined.
 Then run exactly one scoped re-review of the fix wave
-(`bash scripts/review-package PLAN_FILE FIX_BASE HEAD` over the fix range,
+(`bash scripts/review-package PLAN FIX_BASE HEAD` over the fix range,
 [re-review-prompt.md](re-review-prompt.md)).
 Adjudicate any residual findings as in the task loop's breaker: park with
 rulings, or rule on the load-bearing ones and ledger what you decided. Only
@@ -481,7 +481,11 @@ made in secret. Then, under "Remaining plans", the Plan Set lines after this
 plan (`None` if there are none): this plan being complete is not the
 project being complete. When a plan follows this one, finishing this plan
 means starting that one, under the same method and in this session: the
-set was reviewed once, and only the four stops stop you.
+set was reviewed once, and only the four stops stop you. Before its Task 1,
+run `../executing-plans/scripts/plan-boundary NEXT_PLAN`: it names
+every identifier the plan's Consumes lines take from earlier plans that the
+code as built does not contain. Fix each one in the plan file, ledger it as
+a ruling, and re-run until it prints `boundary: clean`.
 
 When the final whole-branch review is clean and its fixes are merged,
 delete this plan's workspace (`rm -rf <workspace>`) — the git history is
@@ -528,7 +532,7 @@ Implementer: [Later]
   - Self-review: Found I missed --force flag, added it
   - Committed
 
-[Run review-package PLAN_FILE BASE HEAD; dispatch task reviewer with the printed path]
+[Run review-package PLAN BASE HEAD; dispatch task reviewer with the printed path]
 Task reviewer: Spec ✅ - all requirements met, nothing extra.
   Strengths: Good test coverage, clean. Issues: None. Task quality: Approved.
 
@@ -543,7 +547,7 @@ Implementer: [No questions]
   - 8/8 tests passing
   - Committed
 
-[Run review-package PLAN_FILE BASE HEAD; dispatch task reviewer with the printed path]
+[Run review-package PLAN BASE HEAD; dispatch task reviewer with the printed path]
 Task reviewer: Spec ❌:
   - Missing: Progress reporting (spec says "report every 100 items")
   Issues (Important): Magic number (100)
@@ -552,7 +556,7 @@ Task reviewer: Spec ❌:
 Implementer: Added progress reporting, extracted PROGRESS_INTERVAL constant.
   Re-ran test/recovery.test.js — 10/10 passing. Fix report appended.
 
-[Run review-package PLAN_FILE FIX_BASE HEAD; dispatch scoped re-review]
+[Run review-package PLAN FIX_BASE HEAD; dispatch scoped re-review]
 Re-reviewer: Missing progress reporting — ADDRESSED (src/recovery.js:41).
   Magic number — ADDRESSED (src/recovery.js:7). New breakage: none.
   Verdict: all findings addressed.
@@ -563,7 +567,7 @@ Re-reviewer: Missing progress reporting — ADDRESSED (src/recovery.js:41).
 ...
 
 [After all tasks]
-[Run review-package PLAN_FILE MERGE_BASE HEAD; dispatch final code-reviewer, most capable model]
+[Run review-package PLAN MERGE_BASE HEAD; dispatch final code-reviewer, most capable model]
 Final reviewer: All requirements met. Deferred minors triaged: none block merge.
 
 [Delete this plan's workspace — the record now lives in git]

+ 1 - 1
skills/subagent-driven-development/scripts/review-package

@@ -17,7 +17,7 @@ fi
 plan=$1
 base=$2
 head=$3
-[ -f "$plan" ] || { echo "no such plan file: $plan" >&2; exit 2; }
+[ -e "$plan" ] || { echo "no such plan: $plan" >&2; exit 2; }
 
 git rev-parse --verify --quiet "$base" >/dev/null || { echo "bad BASE: $base" >&2; exit 2; }
 git rev-parse --verify --quiet "$head" >/dev/null || { echo "bad HEAD: $head" >&2; exit 2; }

+ 2 - 2
skills/subagent-driven-development/scripts/sdd-workspace

@@ -36,9 +36,9 @@ if [ $# -ne 1 ]; then
 fi
 
 plan=$1
-[ -f "$plan" ] || { echo "no such plan file: $plan" >&2; exit 2; }
+[ -e "$plan" ] || { echo "no such plan: $plan" >&2; exit 2; }
 
-slug=$(basename "$plan" .md)
+slug=$(basename "$plan" .md)   # a plan directory keeps its own name
 [ -n "$slug" ] && [ "$slug" != "." ] && [ "$slug" != ".." ] \
   || { echo "cannot derive a workspace name from: $plan" >&2; exit 2; }
 

+ 9 - 1
skills/subagent-driven-development/scripts/task-brief

@@ -16,7 +16,7 @@ fi
 
 plan=$1
 n=$2
-[ -f "$plan" ] || { echo "no such plan file: $plan" >&2; exit 2; }
+[ -e "$plan" ] || { echo "no such plan: $plan" >&2; exit 2; }
 
 if [ $# -eq 3 ]; then
   out=$3
@@ -27,6 +27,14 @@ else
   out="$dir/task-${n}-brief.md"
 fi
 
+if [ -d "$plan" ]; then
+  # directory plan: the shared header, then this task's own file
+  task=$(ls "$plan"/[0-9][0-9]-*.md 2>/dev/null | awk -v n="$n" '{ f=$0; sub(/.*\//,"",f); if (f+0==n) print $0 }' | head -1)
+  [ -n "$task" ] || { echo "task ${n} not found in ${plan} (no [0-9][0-9]-*.md numbered ${n})" >&2; exit 3; }
+  { [ -f "$plan/00-header.md" ] && cat "$plan/00-header.md"; echo; cat "$task"; } > "$out"
+  echo "wrote ${out}: $(wc -l < "$out" | tr -d ' ') lines"
+  exit 0
+fi
 awk -v n="$n" '
   /^```/ { infence = !infence }
   !infence && /^#+[ \t]+Task[ \t]+[0-9]+/ {

+ 9 - 5
skills/writing-plans/SKILL.md

@@ -13,7 +13,11 @@ Write implementation plans for an engineer who has not seen this codebase or thi
 
 **Context:** If working in an isolated worktree, it should have been created via the `superpowers:using-git-worktrees` skill at execution time.
 
-**Save plans to:** `docs/superpowers/plans/YYYY-MM-DD-<feature-name>.md`
+**Save plans to:** `docs/superpowers/plans/YYYY-MM-DD-<feature-name>/`, a
+directory: `00-header.md` holds everything above the first task (the header
+below), and each task is its own file, `NN-<task-name>.md`, numbered in
+execution order from `01`. A task file is what an implementer reads; the
+header is what every task shares. Nothing is repeated between them.
 - (User preferences for plan location override this default)
 
 ## Scope Check
@@ -51,9 +55,9 @@ independently testable deliverable.
 - "Run the tests and make sure they pass" - step
 - "Commit" - step
 
-## Plan Document Header
+## Plan Header (`00-header.md`)
 
-**Every plan MUST start with this header:**
+**Every plan's header file MUST contain this:**
 
 ```markdown
 # [Feature Name] Implementation Plan
@@ -101,7 +105,7 @@ before the next task starts.]
 ---
 ```
 
-## Task Structure
+## Task File (`NN-<task-name>.md`)
 
 ````markdown
 ### Task N: [Component Name]
@@ -174,7 +178,7 @@ opposite failure, and the self-review catches both.
 
 ## Self-Review
 
-After writing the complete plan, look at the spec with fresh eyes and check the plan against it. This is a checklist you run yourself — not a subagent dispatch.
+After writing the complete plan (header and every task file), look at the spec with fresh eyes and check the plan against it; `cat` the directory in order to see it whole. This is a checklist you run yourself — not a subagent dispatch.
 
 **1. Spec coverage:** Skim each section/requirement in the spec. Can you point to a task that implements it? List any gaps.
 

+ 1 - 0
tests/claude-code/run-skill-tests.sh

@@ -77,6 +77,7 @@ tests=(
     "test-worktree-path-policy.sh"
     "test-sdd-workspace.sh"
     "test-executing-plans-scripts.sh"
+    "test-plan-directories.sh"
     "test-subagent-driven-development.sh"
 )
 

+ 79 - 0
tests/claude-code/test-plan-directories.sh

@@ -0,0 +1,79 @@
+#!/usr/bin/env bash
+# Tests for plan directories: task-brief assembles the header and one task
+# file; sdd-workspace names the workspace after the directory; plan-boundary
+# flags a name the next plan consumes that the code does not have, checks the
+# later plans' attributed names too, and reports clean once the plan is fixed.
+set -euo pipefail
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
+SDD="$REPO_ROOT/skills/subagent-driven-development/scripts"
+EP="$REPO_ROOT/skills/executing-plans/scripts"
+FAILURES=0; TEST_ROOT=""
+pass() { echo "  [PASS] $1"; }
+fail() { echo "  [FAIL] $1"; FAILURES=$((FAILURES + 1)); }
+cleanup() { if [[ -n "$TEST_ROOT" && -d "$TEST_ROOT" ]]; then rm -r -- "$TEST_ROOT"; fi; }
+main() {
+    echo "=== Test: plan directories ==="
+    TEST_ROOT="$(mktemp -d)"; trap cleanup EXIT
+    git init -q -b main "$TEST_ROOT/repo"; local repo; repo="$(cd "$TEST_ROOT/repo" && git rev-parse --show-toplevel)"
+    local git_id=(-c user.email=t@example.com -c user.name=t -c commit.gpgsign=false)
+    mkdir -p "$repo/plans/1-engine" "$repo/plans/2-terminal" "$repo/plans/3-effects" "$repo/pkg"
+    cat > "$repo/plans/1-engine/00-header.md" <<'MD'
+# Engine Plan
+**Goal:** the engine.
+## Plan Set
+1. `plans/1-engine` — engine. Consumes nothing.
+2. `plans/2-terminal` — terminal. Consumes from plan 1: `NewGame`, `Advance`.
+3. `plans/3-effects` — effects. Consumes from plan 1: `Advance`; from plan 2: `Canvas`.
+MD
+    cat > "$repo/plans/1-engine/01-core.md" <<'MD'
+### Task 1: Core
+**Interfaces:**
+- Consumes: nothing.
+- Produces: `NewGame() *Game`, `(g *Game) Advance(dt)`.
+- [ ] **Step 1:** write it.
+MD
+    for d in 2-terminal 3-effects; do cp "$repo/plans/1-engine/00-header.md" "$repo/plans/$d/00-header.md"; done
+    cat > "$repo/plans/2-terminal/01-app.md" <<'MD'
+### Task 1: App
+**Interfaces:**
+- Consumes: `NewGame`, `Advance` (plan 1).
+- Produces: `Canvas`.
+MD
+    cat > "$repo/plans/3-effects/01-fx.md" <<'MD'
+### Task 1: FX
+**Interfaces:**
+- Consumes: `Advance` (plan 1), `Canvas` (plan 2).
+- Produces: `World`.
+MD
+    # the engine as built calls it Tick, not Advance
+    printf 'package pkg\ntype Game struct{}\nfunc NewGame() *Game { return &Game{} }\nfunc (g *Game) Tick(dt int) {}\n' > "$repo/pkg/game.go"
+    (cd "$repo" && git add -A && git "${git_id[@]}" commit -q -m "engine")
+
+    local out
+    out="$(cd "$repo" && "$SDD/task-brief" plans/2-terminal 1 "$TEST_ROOT/brief.md")"
+    if grep -q '^# Engine Plan' "$TEST_ROOT/brief.md" && grep -q '^### Task 1: App' "$TEST_ROOT/brief.md" && ! grep -q 'Task 1: FX' "$TEST_ROOT/brief.md"; then
+        pass "task-brief on a directory writes the header plus the one task file"
+    else fail "task-brief on a directory writes the header plus the one task file: $out"; fi
+
+    out="$(cd "$repo" && "$SDD/sdd-workspace" plans/2-terminal)"
+    if [[ "$out" == */.superpowers/sdd/2-terminal ]]; then pass "sdd-workspace names the workspace after the plan directory"; else fail "sdd-workspace names the workspace after the plan directory: $out"; fi
+
+    local rc=0
+    out="$(cd "$repo" && "$EP/plan-boundary" plans/2-terminal 2>&1)" || rc=$?
+    if [[ "$rc" -eq 1 && "$out" == *"missing in code: Advance  (consumed by 2-terminal)"* ]]; then
+        pass "plan-boundary flags a consumed name the code does not have"
+    else fail "plan-boundary flags a consumed name the code does not have (rc=$rc): $out"; fi
+    if [[ "$out" == *"missing in code: Advance  (consumed by 3-effects (from plans before 2-terminal))"* && "$out" != *"Canvas"* ]]; then
+        pass "plan-boundary checks later plans' names from completed plans only"
+    else fail "plan-boundary checks later plans' names from completed plans only: $out"; fi
+    if [[ "$out" != *"NewGame"* ]]; then pass "plan-boundary accepts a consumed name the code has"; else fail "plan-boundary accepts a consumed name the code has"; fi
+
+    (cd "$repo" && perl -pi -e 's/Advance/Tick/g' plans/*/00-header.md plans/2-terminal/01-app.md plans/3-effects/01-fx.md)
+    rc=0; out="$(cd "$repo" && "$EP/plan-boundary" plans/2-terminal 2>&1)" || rc=$?
+    if [[ "$rc" -eq 0 && "$out" == boundary:\ clean* ]]; then pass "plan-boundary reports clean once the plans match the code"; else fail "plan-boundary reports clean once the plans match the code (rc=$rc): $out"; fi
+
+    echo
+    if [[ "$FAILURES" -eq 0 ]]; then echo "PASS"; else echo "FAIL ($FAILURES)"; exit 1; fi
+}
+main "$@"