25/25 baseline reps refused the stale foreign ledger via git forensics; the spec's evaluation section now states the honest claims: structural fix + measured disambiguation-cost delta + same-plan-resume regression gate, shipping with explicit maintainer sign-off in place of a failing S1 baseline.
10 KiB
SDD plan-scoped workspace — design
- Date: 2026-07-06
- Status: approved direction (Jesse, 2026-07-06); this spec captures the investigation's recommended fix
- Problem owner: subagent-driven-development skill (
skills/subagent-driven-development/)
Problem
SDD's durable-progress workspace (.superpowers/sdd/, introduced v6.0.0/v6.0.3) has
no plan identity and no end-of-life. Every artifact is keyed by bare task number
(progress.md, task-N-brief.md, task-N-report.md), and SKILL.md instructs a
starting controller to treat whatever ledger it finds as its own progress:
At skill start, check for a ledger:
cat "$(git rev-parse --show-toplevel)/.superpowers/sdd/progress.md". Tasks listed there as complete are DONE — do not re-dispatch them; resume at the first task not marked complete.
A fresh session executing a follow-up plan in the same worktree reads the previous plan's ledger as its own. A straight-line reading of the skill tells it to skip tasks. Nothing ever deletes the workspace, so the stale state persists indefinitely and accumulates.
Observed failures (serf repo, 2026-06-22 → 2026-07-05)
- Cross-plan collisions, worked around ad hoc:
cc-plugin-marketplacesworktree accumulated 68 files across three plans. The P2 controller had to inventprogress-p2.mdandp2-task-N-report.mdto dodge P1's ledger; P2's briefs silently overwrote P1's at the default paths; an abandonedprogress-p3.mdstub remains. - Git contamination, three times over: SDD scratch was committed and needed
two cleanup commits (
8305e340d,c966261a5); three artifacts are tracked on serf main today, including a report authored on a different machine that now materializes in every fresh worktree. A follow-up plan's task-1 report overwrote an unrelated tracked one, leaving permanentgit statusnoise. - The self-ignoring
.gitignoreis written only when a script runs. Controllers that hand-append the ledger (observed) never create it, and gitignore is powerless once a file is tracked.
Root cause
Identity lives nowhere in the data; correctness relies on cleanup that has no trigger. Any fix that relies on end-of-plan cleanup alone fails exactly in the crash/compaction cases the ledger exists to survive. Identity must be structural.
Design
1. Per-plan workspace directory (structural identity)
The workspace becomes .superpowers/sdd/<plan-slug>/, where <plan-slug> is
the plan file's basename without its .md extension (plan filenames are
already dated kebab-case, e.g. 2026-07-04-plugin-marketplaces-p1-backend-core).
Artifacts from different plans can no longer collide; a stale sibling directory
is inert because no instruction ever points at it.
Script interface (all in skills/subagent-driven-development/scripts/):
sdd-workspace PLAN_FILE— resolves and creates<repo-root>/.superpowers/sdd/<plan-slug>/, maintains the self-ignoring.gitignoreat.superpowers/sdd/.gitignore(parent level, content*), prints the plan directory's absolute path. Errors (exit 2) on missing argument or nonexistent plan file. Slug must be non-empty after stripping.task-brief PLAN_FILE N [OUTFILE]— signature unchanged; default OUTFILE moves to<workspace>/task-N-brief.mdviasdd-workspace PLAN_FILE.review-package PLAN_FILE BASE HEAD [OUTFILE]— gains PLAN_FILE as first argument; default OUTFILE moves to<workspace>/review-<base7>..<head7>.diff.
No compatibility path for the old flat layout: the scripts and SKILL.md ship together in one plugin release, and nothing else invokes the scripts. (Explicitly confirmed: no backward-compatibility handling.)
2. Ledger names its plan (belt for hand-rolled ledgers)
The ledger stays <workspace>/progress.md. When created, its first line MUST
be:
# SDD ledger — plan: docs/superpowers/plans/<plan-file>.md
SKILL.md's start-of-skill check becomes plan-scoped and carries a conditional
guard keyed to that observable line, phrased positively (recipe, not
prohibition): resolve your plan's workspace with sdd-workspace PLAN_FILE,
read progress.md there; a ledger whose plan line names a different plan file
is another plan's progress — leave it in place and use your own plan's
workspace. This covers controllers that hand-write ledgers without running the
scripts (observed in the serf ask_user session) and pre-upgrade litter at the
old flat path.
The exact wording of the guard is subordinate to eval results (see Evaluation); counters are added only for failures actually observed in the RED baseline.
3. Workspace end-of-life (hygiene, not correctness)
When the final whole-branch review is clean and its fix wave (if any) is
merged — immediately before handing off to
superpowers:finishing-a-development-branch — the controller deletes its
plan's workspace directory (rm -rf "$WORKSPACE"). The record of the work is
the git history; the ledger's job (mid-plan compaction recovery) is over.
Sibling directories are never touched: crashed or parallel plans own their own
dirs, and deliberately parked cross-plan artifacts (observed pattern:
WAVE1-HANDOFF.md) live directly under .superpowers/sdd/ untouched by any
plan's cleanup.
4. SKILL.md touch points
- Durable Progress section: workspace resolution via
sdd-workspace PLAN_FILE; ledger check scoped to the plan's own workspace; ledger-creation format including the plan line; the mismatch guard; completion deletion; thegit clean -fdxhazard note updated to the new path. - Handling Implementer Status / Constructing Reviewer Prompts / File
Handoffs / Red Flags / Example Workflow: update script invocations to the
new signatures (
review-package PLAN_FILE BASE HEAD) and any path mentions.implementer-prompt.mdandtask-reviewer-prompt.mdcontain no workspace paths (verified) and need no changes. - Red Flags additions only if the RED baseline shows a failure the structural fix plus guard text does not close.
Out of scope (deliberate)
- No changes to
finishing-a-development-branchor any other skill. - No git-level guards against committing
.superpowers/beyond the existing parent.gitignore. - No retroactive cleanup of the serf repo (separate follow-up).
- No legacy-layout migration or fallback reads.
Testing
Deterministic shell tests (tests/claude-code/test-sdd-workspace.sh, extended)
sdd-workspace PLANprints<root>/.superpowers/sdd/<slug>and creates it; errors without a plan arg; errors on missing plan file.- Two different plan files resolve to two distinct directories; artifacts
written via
task-briefland in their own plan's directory. review-package PLAN BASE HEADwrites under the plan's directory.- Parent
.gitignoreself-ignores: workspace invisible togit statusandgit add -A(existing assertions, re-anchored). - Linked-worktree distinctness (existing assertion, re-anchored).
- Existing suites
test-subagent-driven-development.sh/-integration.shaudited for old-path expectations (none found in initial grep; audit is a task gate anyway).
Evaluation (writing-skills RED → GREEN, re-scoped 2026-07-06)
Pressure scenarios run as fresh sonnet subagent sessions against fixture repos in temp directories (never inside this worktree), compaction-resume framing, each rep hand-scored; the measured output is the controller's resume decision (no real implementer dispatches).
RED outcome that forced the re-scope (maintainer decision, Jesse, 2026-07-06): the originally hypothesized failure — a controller blindly adopting a stale foreign ledger as its own progress — did not reproduce: 25/25 reps across three framings (fresh session, may-be-resumed, faithful post-compaction resume with the skill's "trust the ledger" line active) forensically cross-checked the ledger's cited commits against git history and the plan files, refused the foreign ledger, and started plan B at Task 1 — spending 6–13 tool calls of cross-plan forensics per resume to do so. Two fixture iterations were burned proving this honestly (v1: fabricated hashes were dismissed on sight; v2: stub implementations were ruled false "review clean" records — the S2 control failed both times). Full record in the committed eval docs.
Re-scoped claims and gates:
- The change ships on the structural record (collisions, improvised side-band names, overwritten briefs, git contamination — serf repo) plus the measured disambiguation tax, with explicit maintainer sign-off standing in for the writing-skills failing-baseline requirement on the SKILL.md text.
- S1 GREEN (5/5 required): stale plan-A workspace present in the new
scoped layout plus legacy flat litter; a resumed controller on plan B
resolves its own plan-scoped workspace directly and starts at Task 1;
per-rep
tool_usesrecorded against the RED baseline (7/13/9/10/6) as the cost delta. - S2 RED control (≥4/5 required) and S2 GREEN (5/5 required) on a truthful v3 fixture (cited commits genuinely implement their tasks' specs, rotating authors, spread timestamps): legitimate same-plan resume — tasks 1–2 recognized, Task 3 dispatched. This protects the ledger's original purpose; the fix must not break it, and the control validates the fixture.
Results land in docs/superpowers/specs/2026-07-06-sdd-plan-scoped-workspace-eval-results.md
and are summarized in the PR.
Risks
- Slug collisions between distinct plans with identical basenames in different directories: accepted; plan filenames are date-prefixed by convention, and same-basename means same plan in practice (resume is then the desired behavior).
- Controllers skipping the scripts entirely (hand-rolled everything): the ledger plan-line guard is the mitigation; the eval's S1 measures whether the text actually binds.
- Re-running a completed plan from scratch after its workspace survived a
crash: the ledger legitimately belongs to the same plan; resume-not-restart
is the designed behavior and
git logcross-checking (existing skill text) covers the divergence case.