Ver Fonte

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 há 2 semanas atrás
pai
commit
cffb7c95b6

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

@@ -107,6 +107,11 @@ digraph process {
 
 
 ## Setup
 ## 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
 Ensure the work happens in an isolated workspace: use
 superpowers:using-git-worktrees to create one or verify the existing one.
 superpowers:using-git-worktrees to create one or verify the existing one.
 Never start implementation on a main/master branch without your human
 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.
 and the new one resumes from the same ledger.
 
 
 - Each plan owns a workspace: at skill start, run
 - 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
   prints the plan's git-ignored directory
   (`<repo-root>/.superpowers/sdd/<plan-basename>/`), home to every
   (`<repo-root>/.superpowers/sdd/<plan-basename>/`), home to every
   artifact for THIS plan: ledger, briefs, review packages. Another plan's
   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
   recollection. A ledger whose first line names a different plan file is
   another plan's progress: leave it and start your own, fresh.
   another plan's progress: leave it and start your own, fresh.
 - Create the ledger with its identity as the first line:
 - 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);
 - `git clean -fdx` will destroy the workspace (it's git-ignored scratch);
   if that happens, recover from `git log`.
   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
 ### 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.
   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:
   Read the brief for every task, including ones you remember from setup:
   what you remember is a summary, the brief has the exact values,
   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
 ### 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
 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
 tests, keeps the full output in the workspace, prints the tail, and — only
 if they pass — appends the completion line to the ledger:
 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
 ## 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.
 (MERGE_BASE = the commit the branch started from, e.g.
 `git merge-base main HEAD`) and review from the file it prints.
 `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
 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
 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
 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
 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.
 # only if the command succeeded — append the completion line to the ledger.
 # A failing command records nothing: the task is not complete.
 # 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.
 #   BASE is the SHA task-start printed; the completion line records BASE..HEAD.
 # Exit: the test command's exit status.
 # Exit: the test command's exit status.
 set -euo pipefail
 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
 # cut from. One tool call instead of two, because every call in an inline
 # session is a turn that re-reads the whole context.
 # 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:
 # Prints:
 #   brief: <path to the task's brief file>
 #   brief: <path to the task's brief file>
 #   base:  <full SHA of HEAD>
 #   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.
 a ledger file, not only in todos.
 
 
 - Each plan owns a workspace: at skill start, run this skill's
 - 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
   directory (under `<repo-root>/.superpowers/sdd/`), home to
   every artifact for THIS plan: ledger, briefs, reports, review packages.
   every artifact for THIS plan: ledger, briefs, reports, review packages.
   Another plan's directory is never yours to read or write.
   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
   ledger at the old flat path `.superpowers/sdd/progress.md` — is another
   plan's progress: leave it in place and start your own, fresh.
   plan's progress: leave it in place and start your own, fresh.
 - Create the ledger with its identity as the first line:
 - 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
 - The ledger is your recovery map: the commits it names exist in git even
   when your context no longer remembers creating them. After compaction,
   when your context no longer remembers creating them. After compaction,
   trust the ledger and `git log` over your own recollection.
   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.
 and fix-round diffs need it.
 
 
 - **Task brief:** before dispatching an implementer, run this skill's
 - **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
   uniquely named file and prints the path. Compose the dispatch so the
   brief stays the single source of
   brief stays the single source of
   requirements. Your dispatch should contain: (1) one line on where this
   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:
 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.
 **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.
 needed.
 
 
 - Hand the reviewer its diff as a file: run this skill's
 - 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`,
   it prints (or, without bash: `git log --oneline`, `git diff --stat`,
   and `git diff -U10` for the range, redirected to one uniquely named
   and `git diff -U10` for the range, redirected to one uniquely named
   file). The output never enters your own context, and the reviewer sees
   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
 covering test files in the fix message — a one-line fix does not need the
 whole suite.
 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
 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
 [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
 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
 ## Final Review
 
 
 The final whole-branch review gets a package too: run
 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
 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
 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
 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
 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.
 session's final-review fix wave cost more than all its tasks combined.
 Then run exactly one scoped re-review of the fix wave
 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)).
 [re-review-prompt.md](re-review-prompt.md)).
 Adjudicate any residual findings as in the task loop's breaker: park with
 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
 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
 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
 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
 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,
 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
 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
   - Self-review: Found I missed --force flag, added it
   - Committed
   - 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.
 Task reviewer: Spec ✅ - all requirements met, nothing extra.
   Strengths: Good test coverage, clean. Issues: None. Task quality: Approved.
   Strengths: Good test coverage, clean. Issues: None. Task quality: Approved.
 
 
@@ -543,7 +547,7 @@ Implementer: [No questions]
   - 8/8 tests passing
   - 8/8 tests passing
   - Committed
   - 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 ❌:
 Task reviewer: Spec ❌:
   - Missing: Progress reporting (spec says "report every 100 items")
   - Missing: Progress reporting (spec says "report every 100 items")
   Issues (Important): Magic number (100)
   Issues (Important): Magic number (100)
@@ -552,7 +556,7 @@ Task reviewer: Spec ❌:
 Implementer: Added progress reporting, extracted PROGRESS_INTERVAL constant.
 Implementer: Added progress reporting, extracted PROGRESS_INTERVAL constant.
   Re-ran test/recovery.test.js — 10/10 passing. Fix report appended.
   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).
 Re-reviewer: Missing progress reporting — ADDRESSED (src/recovery.js:41).
   Magic number — ADDRESSED (src/recovery.js:7). New breakage: none.
   Magic number — ADDRESSED (src/recovery.js:7). New breakage: none.
   Verdict: all findings addressed.
   Verdict: all findings addressed.
@@ -563,7 +567,7 @@ Re-reviewer: Missing progress reporting — ADDRESSED (src/recovery.js:41).
 ...
 ...
 
 
 [After all tasks]
 [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.
 Final reviewer: All requirements met. Deferred minors triaged: none block merge.
 
 
 [Delete this plan's workspace — the record now lives in git]
 [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
 plan=$1
 base=$2
 base=$2
 head=$3
 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 "$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; }
 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
 fi
 
 
 plan=$1
 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" != ".." ] \
 [ -n "$slug" ] && [ "$slug" != "." ] && [ "$slug" != ".." ] \
   || { echo "cannot derive a workspace name from: $plan" >&2; exit 2; }
   || { 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
 plan=$1
 n=$2
 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
 if [ $# -eq 3 ]; then
   out=$3
   out=$3
@@ -27,6 +27,14 @@ else
   out="$dir/task-${n}-brief.md"
   out="$dir/task-${n}-brief.md"
 fi
 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" '
 awk -v n="$n" '
   /^```/ { infence = !infence }
   /^```/ { infence = !infence }
   !infence && /^#+[ \t]+Task[ \t]+[0-9]+/ {
   !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.
 **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)
 - (User preferences for plan location override this default)
 
 
 ## Scope Check
 ## Scope Check
@@ -51,9 +55,9 @@ independently testable deliverable.
 - "Run the tests and make sure they pass" - step
 - "Run the tests and make sure they pass" - step
 - "Commit" - 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
 ```markdown
 # [Feature Name] Implementation Plan
 # [Feature Name] Implementation Plan
@@ -101,7 +105,7 @@ before the next task starts.]
 ---
 ---
 ```
 ```
 
 
-## Task Structure
+## Task File (`NN-<task-name>.md`)
 
 
 ````markdown
 ````markdown
 ### Task N: [Component Name]
 ### Task N: [Component Name]
@@ -174,7 +178,7 @@ opposite failure, and the self-review catches both.
 
 
 ## Self-Review
 ## 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. 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-worktree-path-policy.sh"
     "test-sdd-workspace.sh"
     "test-sdd-workspace.sh"
     "test-executing-plans-scripts.sh"
     "test-executing-plans-scripts.sh"
+    "test-plan-directories.sh"
     "test-subagent-driven-development.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 "$@"