首页
/ Superpowers SDD 计划级工作区:用结构化身份根治 Subagent-Driven Development 的跨计划账本冲突

Superpowers SDD 计划级工作区:用结构化身份根治 Subagent-Driven Development 的跨计划账本冲突

2026-09-06 10:23:27作者:庞队千Virginia

本文基于 superpowers 仓库的设计规格 2026-07-06-sdd-plan-scoped-workspace.md,完整解读 SDD(Subagent-Driven Development)持久进度工作区从"扁平单目录"演进为"每计划一个独立目录"(.superpowers/sdd/<plan-slug>/)的设计动机、三层修复方案、配套脚本接口与确定性测试/RED→GREEN 评估方法。读完后你能理解:为什么账本(ledger)缺少计划身份会在多计划工作树中导致任务被静默跳过、如何通过 sdd-workspace / task-brief / review-package 三个脚本实现结构性隔离,以及这套改动是如何用可复现的压力测试验证"修复没有破坏合法恢复"的。

背景:持久进度工作区的设计初衷与暴露的缺陷

SDD 技能(skills/subagent-driven-development/SKILL.md)的核心循环是:为计划中的每个任务派发一个全新实现者子代理,任务完成后做一次任务评审,全部任务完成后再做整分支评审。这个流程有一个昂贵的失败模式:控制器(controller)在会话上下文压缩(compaction)后丢失"做到哪了"的记忆,从而把已完成的任务重新派发一遍。为此,技能引入了持久进度工作区 .superpowers/sdd/(v6.0.0/v6.0.3 引入),其中 progress.md 账本记录每个任务的完成状态,SKILL.md 要求控制器在技能启动时检查账本、跳过已标记完成的任务、从第一个未完成的任务恢复。

但规格文档指出了两个结构性缺陷:工作区没有计划身份,也没有生命周期终点

  • 所有产物都只用裸任务编号命名(progress.mdtask-N-brief.mdtask-N-report.md),账本里没有记录"这个账本属于哪个计划文件";
  • 没有任何指令负责删除工作区,过期状态会永久残留并不断累积。

后果是:同一个工作树(worktree)中执行后续计划的新会话,会把上一个计划的账本当成自己的进度,按技能的字面指令跳过整批任务。

实际观察到的失败(serf 仓库,2026-06-22 → 2026-07-05)

规格记录了三类真实发生的事故,这是本次设计的事实基础:

  1. 跨计划冲突,只能临时绕开cc-plugin-marketplaces 工作树在一次会话周期内累积了 68 个文件、横跨三个计划。第二个计划(P2)的控制器被迫自创 progress-p2.mdp2-task-N-report.md 来躲避 P1 的账本;而 P2 的 brief 文件却悄悄覆盖了 P1 在默认路径上的同名文件;现场还留下一个被遗弃的 progress-p3.md 存根。
  2. Git 污染,发生三次:SDD 的临时文件被提交进版本库,需要两次清理提交(8305e340dc966261a5);serf 主分支至今仍跟踪着三个产物,其中包括一份在另一台机器上写成的报告——它现在会在每个新工作树里"显形"。一个后续计划的 task-1 报告还覆盖了无关的已跟踪文件,留下永久性的 git status 噪音。
  3. 自愈式 .gitignore 只在脚本运行时才写入:被观察到有控制器是手工追加账本的,从未创建 .gitignore;而一旦文件被 git 跟踪,gitignore 就完全失效。

根因判断

规格的根因表述非常凝练:身份在数据中无处安放,正确性依赖一个没有触发器的清理动作。任何只靠"计划结束时清理"的方案,恰好会在账本本来要存活下来的崩溃/压缩场景里失效。因此修复必须是结构性的——身份要写进路径和文件本身,而不是依赖纪律。

设计一:每计划一个工作区目录(结构性身份)

核心改动:工作区从 .superpowers/sdd/ 变为 .superpowers/sdd/<plan-slug>/,其中 <plan-slug> 是计划文件去掉 .md 扩展名后的文件名(basename)。superpowers 的计划文件本来就遵循带日期的 kebab-case 命名(如 2026-07-04-plugin-marketplaces-p1-backend-core),所以 slug 天然稳定且可区分。不同计划的产物从此不可能再互相覆盖;一个过期的兄弟目录是"惰性的"——因为没有任何指令会指向它。

sdd-workspace:工作区位置的唯一事实来源

解析工作区的脚本是 scripts/sdd-workspace。它的职责是"解析并创建某计划的产物目录,把绝对路径打印到 stdout"。核心实现逻辑(节选):

# Usage: sdd-workspace PLAN_FILE
set -euo pipefail

if [ $# -ne 1 ]; then
  echo "usage: sdd-workspace PLAN_FILE" >&2
  exit 2
fi

plan=$1
[ -f "$plan" ] || { echo "no such plan file: $plan" >&2; exit 2; }

slug=$(basename "$plan" .md)
[ -n "$slug" ] && [ "$slug" != "." ] && [ "$slug" != ".." ] \
  || { echo "cannot derive a workspace name from: $plan" >&2; exit 2; }

root=$(git rev-parse --show-toplevel)
base="$root/.superpowers/sdd"
dir="$base/$slug"
mkdir -p "$dir"
printf '*\n' > "$base/.gitignore"
cd "$dir" && pwd

从源码可以确认几个设计决策:

  • 参数契约:缺少参数、计划文件不存在、或 slug 剥离 .md 后为空,一律以 exit 2 报错;成功时打印计划目录的绝对路径。
  • 自愈式 .gitignore 放在父级:脚本每次运行时向 .superpowers/sdd/.gitignore(注意是父目录,不是各计划目录)写入一行 *,使所有计划的工作区整体对 git statusgit add -A 不可见,且不需要修改任何已跟踪文件。这同时回答了规格中"为什么 .gitignore 要放在父级"的问题:只要任何脚本跑过一次,全部兄弟目录都被忽略。
  • 为什么放在工作树而不是 .git/:脚本头注释明确说明——Claude Code 把 .git/ 视为受保护路径并拒绝代理写入,会导致实现者子代理无法写报告文件;放在工作树里配合自忽略的 .gitignore 才能两全。
  • 单一事实来源task-briefreview-package 都通过调用 sdd-workspace 来解析目录(见下),杜绝三个脚本各自硬编码路径产生漂移。

三个脚本的新接口

规格为 skills/subagent-driven-development/scripts/ 下三个脚本定义了新接口,当前仓库中三者均已落地:

脚本 签名 默认输出位置
sdd-workspace sdd-workspace PLAN_FILE 打印 <repo-root>/.superpowers/sdd/<plan-slug>/ 绝对路径
task-brief task-brief PLAN_FILE N [OUTFILE] <workspace>/task-N-brief.md
review-package review-package PLAN_FILE BASE HEAD [OUTFILE] <workspace>/review-<base7>..<head7>.diff

task-brief 的签名不变,只是默认 OUTFILE 经由 sdd-workspace 落到本计划的目录下;它用 awk 按 Task N 标题从计划文件中抽取该任务全文,保证实现者"一次读取"就能拿到完整需求,任务文本不必穿过控制器上下文。review-package新增了 PLAN_FILE 作为第一参数(这是向后不兼容的签名变化),生成包含提交列表、--stat 摘要和 -U10 完整 diff 的评审包,并按提交范围命名,使修复轮次的再评审天然得到不同的文件。

规格明确声明:不为旧的扁平布局保留兼容路径。理由是脚本与 SKILL.md 在同一个插件版本中一起发布,且没有别的东西调用这些脚本——这一点在规格中被标注为"已明确确认:无向后兼容处理"。

设计二:账本首行显式写明所属计划(手工账本的兜底)

目录隔离防住了"走脚本"的控制器,但现实中存在不跑脚本、手工写账本的控制器(在 serf 的 ask_user 会话中被实际观察到)。因此账本文件 <workspace>/progress.md 在创建时的第一行必须是:

# SDD ledger — plan: docs/superpowers/plans/<plan-file>.md

相应地,SKILL.md 的启动检查被改写为"计划作用域 + 条件守卫"。当前 SKILL.md 的 Setup 一节(约 L122–L140)的实际表述是正向的配方式指令而非禁令:

  1. 技能启动时运行 scripts/sdd-workspace PLAN_FILE,打印出本计划的 git 忽略目录,它是本计划全部产物(账本、brief、报告、评审包)的家;"另一个计划的目录永远轮不到你读或写"。
  2. 检查 <workspace>/progress.md首行写明你的计划文件的账本中,带 Task <N>: complete 行的任务是 DONE,不要重新派发;最后一个任务是修复轮次的则说明卡在修复循环中间,从下一轮恢复。首行写明的是另一个计划文件、或残留在旧扁平路径 .superpowers/sdd/progress.md 的账本——那是别的计划的进度:原地保留,自己新开一份。

这个守卫恰好覆盖两类情况:绕过脚本手工记账的控制器,以及升级前遗留在旧扁平路径上的脏数据。规格还保留了一条方法论约束:守卫的具体措辞服从评估结果(见下文 Evaluation),只为 RED 基线中真实观测到的失败追加计数器,不做臆防。

设计三:工作区生命周期终点(是卫生问题,不是正确性问题)

规格刻意区分了"正确性"与"卫生":目录隔离已经保证了正确性,删除只是收尾。

时机是:最终整分支评审干净、且其修复波次(如有)已合并——在移交给 finishing-a-development-branch 技能之前——控制器删除自己计划的工作区目录(rm -rf "$WORKSPACE")。理由:工作的记录此刻已经在 git 历史里,账本的存在意义(计划中途的压缩恢复)已经用尽。

SKILL.md 的 Finish 一节(约 L416–L421)落地了这一条:"当最终整分支评审干净且修复已合并,删除本计划的工作区——记录现在在 git 里。兄弟目录属于其他计划,不要动。"这条"不碰兄弟目录"规则同样保护了被观察到的一类人为留存物:跨计划交接文件(如 WAVE1-HANDOFF.md)直接放在 .superpowers/sdd/ 根下,不属于任何计划的清理范围。

设计四:SKILL.md 的触点清单

规格列出了 SKILL.md 中需要改动的位置,当前仓库中均已体现:

  • Durable Progress / Setup:工作区解析走 sdd-workspace PLAN_FILE;账本检查限定在本计划工作区内;账本创建格式含计划身份首行;失配守卫;完成时删除;git clean -fdx 危险提示更新为新路径(工作区是 git 忽略的临时区,被 git clean -fdx 清掉后从 git log 恢复)。
  • Handling Implementer Status / Constructing Reviewer Prompts / File Handoffs / Red Flags / Example Workflow:脚本调用全部更新为新签名 review-package PLAN_FILE BASE HEADSKILL.md 的 Example Workflow 一节现在演示了完整链路:sdd-workspace 解析 → task-brief 生成 → review-package PLAN_FILE BASE HEAD 出包 → 账本记账 → 最终删除工作区。
  • 规格验证过 implementer-prompt.mdtask-reviewer-prompt.md 不含任何工作区路径,无需改动。
  • Red Flags 表格只在 RED 基线显示出"结构修复 + 守卫文本"都关不掉的失败时才追加。

明确不做的事(Out of scope)

规格用一整节划定了边界,避免范围蔓延:

  • 不改动 finishing-a-development-branch 或任何其他技能;
  • 除现有的父级 .gitignore 外,不引入针对 .superpowers/ 提交的其他 git 级防护;
  • 不回头清理 serf 仓库的历史污染(单独立项跟进);
  • 不做旧布局迁移或回退读取。

确定性测试:test-sdd-workspace.sh

规格要求扩展 tests/claude-code/test-sdd-workspace.sh,当前该文件已实现全部断言,并在临时目录中的干净 git 仓库上运行,覆盖:

  • 参数校验sdd-workspace 无参数、或缺失计划文件,均以 exit 2 报错;
  • 每计划解析plan-a.mdplan-b.md 解析到 <repo>/.superpowers/sdd/plan-aplan-b 两个互不相同的已存在目录;
  • 自忽略验证.superpowers/sdd/.gitignore 内容为 *;写入 artifact.mdgit status --porcelain 不出现 .superpowersgit add -A 之后暂存区里也没有它;
  • 产物落位task-brief plan-a.md 1 的输出路径必须是 <repo>/.superpowers/sdd/plan-a/task-1-brief.md
  • review-package 新签名review-package plan-a.md HEAD~1 HEAD 写出 <repo>/.superpowers/sdd/plan-a/review-*.diff;不带 PLAN_FILE 的旧调用方式以 exit 2 报错;显式 OUTFILE 参数被尊重;
  • 链接工作树隔离git worktree add 出的第二个工作树解析出自己根下的 .superpowers/sdd/plan-a(与主工作树不同路径),且同样对 git status 不可见——这条断言重新锚定到新布局上,确认"每工作树、每计划"的双维度隔离。

规格还要求对既有的 test-subagent-driven-development.sh / -integration.sh 做旧路径期望审计(初次 grep 未发现,审计本身是任务门禁)。

RED → GREEN 评估:为什么"结构修复"在没有复现假设失败的情况下仍然值得发布

这部分是本规格最有方法论价值的内容,结果完整记录在 2026-07-06-sdd-plan-scoped-workspace-eval-results.md

方法

压力场景以全新 sonnet 子代理会话跑在临时目录中的夹具仓库上(绝不在本工作树内),采用"压缩后恢复"的会话框架,每次重复人工评分;被测输出是控制器的恢复决策(不真正派发实现者)。

RED 基线意外:假设的失败没有复现

最初假设的失败——控制器盲目把陈旧的外部账本当作自己的进度——没有复现:跨三种会话框架(新会话、可能已恢复、带技能"信任账本"指令的忠实压缩后恢复)共 25/25 次重复,控制器都通过法证手段核对了账本引用的提交与 git 历史及计划文件,拒绝外部账本并从 Task 1 开始——代价是每次恢复花 6–13 次工具调用做跨计划法证。为诚实证明这一点,烧掉了两代夹具:v1(伪造哈希被一眼识破)、v2(桩实现被判定为虚假的"评审通过"记录),S2 对照组两次都失败。

因此规格重新界定(re-scope)了主张:改动凭结构性记录(冲突、临时旁路命名、被覆盖的 brief、git 污染——都来自 serf 仓库)加上实测的鉴别成本发布,由维护者签核代替 writing-skills 方法论中"失败基线"的要求。重新界定后的门禁:

  • S1 GREEN(要求 5/5):夹具同时存在旧扁平账本残留和计划 A 的已完成计划级工作区(升级后最坏情况);恢复中的控制器在计划 B 上直接解析自己的计划级工作区并从 Task 1 开始;每次重复记录 tool_uses,与 RED 基线(7/13/9/10/6)对比成本差。
  • S2 RED 对照(要求 ≥4/5)与 S2 GREEN(要求 5/5):使用"诚实"的 v3 夹具(引用的提交真实实现了各任务的规格、轮换作者、时间戳分散),验证合法的同计划恢复——识别任务 1–2 已完成、派发 Task 3。这一条保护的是账本的原始用途:修复不能破坏它,对照组同时验证夹具本身的可信度。

实际结果

评估文档的结果表:

被测文本 夹具 结果
S1 RED 发布版 v1+v2+probe,三种框架 15/15 拒绝误采;平均 9.0 次工具调用做跨计划法证
S1 GREEN 本分支 v3 scoped 5/5,全部经结构判定(工作区 + 身份首行),无一靠提交内容法证
S2 RED(对照) 发布版 v3 flat 5/5,验证夹具有效
S2 GREEN 本分支 v3 scoped 5/5,合法恢复未被破坏

评估文档对成本对比的处理值得注意:S1 GREEN 的原始工具调用数(均值 9.6)并没有低于 RED probe(均值 9.0),文档对此做了诚实披露——GREEN 夹具含有更多陈旧物(三处账本位置),且判定方式的本质区别在于:probe 轮的控制器必须靠跨计划提交/计划文件法证来断定"账本是谁的",GREEN 轮的控制器按结构判定(解析自己的工作区、核对身份首行),误归因在新布局下从机制上不可能发生。这才是承重结论,而非调用数下降。

评估还发现并披露了夹具生成器自身的 bug:ci 计数器在命令替换子shell中被自增导致不生效,使所有提交塌缩到同一作者、同一时间戳——恰好是使 v2 对照组失效的"夹具制造的历史"破绽;被计划文本中 Step 1 的自检门禁(引用哈希可解析 + 两个作者跨两天)在场景运行前抓住,用"把计数器持久化到文件"的一行级修复解决。

风险与接受理由

规格对三类已知风险给出了明确处置:

  • 不同目录下同名计划文件的 slug 冲突:接受。计划文件名按惯例带日期前缀,实践中同 basename 即同一计划(此时"恢复"正是期望行为)。
  • 控制器完全绕过脚本、全手工记账:缓解手段是账本身份首行守卫;S1 评估测量文本指令是否真正约束行为。
  • 计划完成后工作区残留、又从头重跑同一计划:账本合法地属于同一计划,"恢复而非重启"就是设计行为;分叉场景由既有的 git log 交叉核对(技能原文已有)覆盖。

参考文件

路径 内容
docs/superpowers/specs/2026-07-06-sdd-plan-scoped-workspace.md 本文主体:设计规格(问题、根因、四层设计、边界、测试与风险)
docs/superpowers/specs/2026-07-06-sdd-plan-scoped-workspace-eval-results.md RED→GREEN 评估完整记录:夹具迭代、逐字回复引用、成本表
docs/superpowers/plans/2026-07-06-sdd-plan-scoped-workspace.md 该设计的实施计划
skills/subagent-driven-development/scripts/sdd-workspace 工作区解析脚本(唯一事实来源)
skills/subagent-driven-development/scripts/task-brief 任务 brief 抽取脚本
skills/subagent-driven-development/scripts/review-package 评审包生成脚本(新签名)
skills/subagent-driven-development/SKILL.md SDD 技能定义,含新守卫文本与 Example Workflow
tests/claude-code/test-sdd-workspace.sh 工作区脚本的确定性 shell 测试套件

这套设计给出的通用启示是:当持久化状态会被多个"逻辑实体"(此处是计划)共享时,身份必须编码在路径结构里并由文件首行自证,清理策略只能作为卫生手段而非正确性前提;同时,一个无法复现的"假设失败"并不妨碍基于真实结构性事故和可测量成本发布改动——前提是像本规格这样,用对照组守住原机制仍有效的回归线。

登录后查看全文
热门项目推荐
相关项目推荐