sillyspec 3.20.2 → 3.20.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +21 -21
  2. package/.claude/skills/sillyspec-auto/SKILL.md +83 -83
  3. package/.claude/skills/sillyspec-brainstorm/SKILL.md +44 -44
  4. package/.claude/skills/sillyspec-commit/SKILL.md +106 -106
  5. package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
  6. package/.claude/skills/sillyspec-doctor/SKILL.md +31 -31
  7. package/.claude/skills/sillyspec-execute/SKILL.md +30 -30
  8. package/.claude/skills/sillyspec-explore/SKILL.md +109 -109
  9. package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
  10. package/.claude/skills/sillyspec-plan/SKILL.md +21 -21
  11. package/.claude/skills/sillyspec-propose/SKILL.md +21 -21
  12. package/.claude/skills/sillyspec-quick/SKILL.md +21 -21
  13. package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
  14. package/.claude/skills/sillyspec-scan/SKILL.md +21 -21
  15. package/.claude/skills/sillyspec-state/SKILL.md +54 -54
  16. package/.claude/skills/sillyspec-status/SKILL.md +21 -21
  17. package/.claude/skills/sillyspec-verify/SKILL.md +21 -21
  18. package/.claude/skills/sillyspec-workspace/SKILL.md +157 -157
  19. package/.husky/pre-push +13 -13
  20. package/CLAUDE.md +18 -18
  21. package/README.md +198 -188
  22. package/SKILL.md +90 -91
  23. package/bin/sillyspec.js +2 -2
  24. package/docs/brainstorm-plan-contract.md +64 -64
  25. package/docs/plan-execute-contract.md +123 -123
  26. package/docs/platform-scan-protocol.md +298 -298
  27. package/docs/revision-mode.md +115 -115
  28. package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +99 -99
  29. package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +218 -218
  30. package/docs/sillyspec/file-lifecycle/stage-artifacts.md +167 -167
  31. package/docs/sillyspec/file-lifecycle/storage-and-state.md +148 -148
  32. package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +211 -193
  33. package/docs/sillyspec/file-lifecycle.md +125 -125
  34. package/docs/workflow-contract-regression.md +106 -106
  35. package/docs/worktree-isolation.md +252 -252
  36. package/package.json +40 -40
  37. package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
  38. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
  39. package/packages/dashboard/dist/index.html +16 -16
  40. package/packages/dashboard/index.html +15 -15
  41. package/packages/dashboard/package-lock.json +2384 -2384
  42. package/packages/dashboard/package.json +25 -25
  43. package/packages/dashboard/server/executor.js +86 -86
  44. package/packages/dashboard/server/index.js +588 -588
  45. package/packages/dashboard/server/parser.js +526 -526
  46. package/packages/dashboard/server/watcher.js +344 -344
  47. package/packages/dashboard/src/App.vue +558 -558
  48. package/packages/dashboard/src/components/ActionBar.vue +93 -93
  49. package/packages/dashboard/src/components/CommandPalette.vue +96 -96
  50. package/packages/dashboard/src/components/DetailPanel.vue +137 -137
  51. package/packages/dashboard/src/components/LogStream.vue +65 -65
  52. package/packages/dashboard/src/components/PipelineStage.vue +95 -95
  53. package/packages/dashboard/src/components/PipelineView.vue +156 -156
  54. package/packages/dashboard/src/components/ProjectList.vue +210 -210
  55. package/packages/dashboard/src/components/StageBadge.vue +67 -67
  56. package/packages/dashboard/src/components/StepCard.vue +94 -94
  57. package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -48
  58. package/packages/dashboard/src/components/detail/GitDetail.vue +61 -61
  59. package/packages/dashboard/src/components/detail/TechDetail.vue +43 -43
  60. package/packages/dashboard/src/composables/useDashboard.js +170 -170
  61. package/packages/dashboard/src/composables/useKeyboard.js +119 -119
  62. package/packages/dashboard/src/composables/useWebSocket.js +129 -129
  63. package/packages/dashboard/src/main.js +8 -8
  64. package/packages/dashboard/src/style.css +132 -132
  65. package/packages/dashboard/vite.config.js +18 -18
  66. package/src/brainstorm-postcheck.js +158 -158
  67. package/src/change-list.js +52 -52
  68. package/src/change-risk-profile.js +352 -352
  69. package/src/classify-change.js +73 -73
  70. package/src/constants.js +70 -70
  71. package/src/contract-matrix.js +278 -278
  72. package/src/db.js +201 -201
  73. package/src/endpoint-extractor.js +315 -315
  74. package/src/hooks/claude-pre-tool-use.cjs +125 -125
  75. package/src/hooks/worktree-guard.js +653 -653
  76. package/src/index.js +922 -900
  77. package/src/init.js +431 -431
  78. package/src/knowledge-match.js +130 -130
  79. package/src/migrate.js +117 -117
  80. package/src/modules.js +482 -482
  81. package/src/progress.js +1734 -1734
  82. package/src/run.js +3465 -3358
  83. package/src/scan-postcheck.js +387 -383
  84. package/src/setup.js +398 -398
  85. package/src/stage-contract.js +700 -700
  86. package/src/stages/archive.js +160 -160
  87. package/src/stages/brainstorm-auto.js +229 -229
  88. package/src/stages/brainstorm.js +645 -645
  89. package/src/stages/doctor.js +365 -365
  90. package/src/stages/execute.js +625 -625
  91. package/src/stages/explore.js +34 -34
  92. package/src/stages/index.js +29 -29
  93. package/src/stages/knowledge.js +498 -498
  94. package/src/stages/plan-postcheck.js +511 -513
  95. package/src/stages/plan.js +582 -582
  96. package/src/stages/propose.js +174 -174
  97. package/src/stages/quick.js +82 -82
  98. package/src/stages/scan.js +558 -558
  99. package/src/stages/status.js +65 -65
  100. package/src/stages/verify.js +322 -322
  101. package/src/sync.js +497 -497
  102. package/src/task-review.js +346 -346
  103. package/src/workflow.js +785 -785
  104. package/src/worktree-apply.js +549 -549
  105. package/src/worktree-deps.js +185 -0
  106. package/src/worktree.js +982 -932
  107. package/templates/workflows/archive-impact.yaml +79 -79
  108. package/templates/workflows/scan-docs.yaml +132 -132
  109. package/test/brainstorm-plan-contract.test.mjs +273 -273
  110. package/test/check-syntax.mjs +26 -26
  111. package/test/contract-artifacts.test.mjs +323 -323
  112. package/test/decision-supersede.test.mjs +277 -277
  113. package/test/knowledge-match.test.mjs +231 -231
  114. package/test/plan-execute-contract.test.mjs +330 -330
  115. package/test/plan-optimization.test.mjs +572 -572
  116. package/test/platform-artifacts.test.mjs +166 -166
  117. package/test/platform-failure-samples.test.mjs +199 -199
  118. package/test/platform-recovery-chain.test.mjs +167 -167
  119. package/test/platform-recovery.test.mjs +136 -136
  120. package/test/platform-scan-p0.test.mjs +168 -168
  121. package/test/revision-v1.test.mjs +1145 -1145
  122. package/test/run-scan-project-parse.test.mjs +200 -200
  123. package/test/run-tests.mjs +48 -48
  124. package/test/scan-knowledge.test.mjs +175 -175
  125. package/test/scan-paths.test.mjs +68 -68
  126. package/test/scan-postcheck.test.mjs +197 -197
  127. package/test/spec-dir.test.mjs +206 -206
  128. package/test/stage-contract.test.mjs +299 -299
  129. package/test/stage-definitions.test.mjs +39 -39
  130. package/test/wait-gates.test.mjs +496 -496
  131. package/test/worktree-deps-provision.test.mjs +148 -0
  132. package/test/worktree-guard.test.mjs +71 -71
  133. package/test/worktree-native-overlay.test.mjs +188 -188
@@ -1,625 +1,625 @@
1
- import { existsSync, readFileSync } from 'fs'
2
- import path from 'path'
3
-
4
- /**
5
- * 校验 plan.md 是否满足 execute 执行契约
6
- * @param {string} planContent - plan.md 文件内容
7
- * @returns {{ ok: boolean, errors: string[], warnings: string[], tasks: object[], waves: object[] }}
8
- */
9
- export function validatePlanForExecute(planContent) {
10
- const errors = []
11
- const warnings = []
12
-
13
- if (!planContent || !planContent.trim()) {
14
- return { ok: false, errors: ['plan.md 内容为空'], warnings, tasks: [], waves: [] }
15
- }
16
-
17
- const waves = parseWavesFromPlan(planContent)
18
-
19
- // 收集所有 task
20
- const allTasks = []
21
- for (const wave of waves) {
22
- for (const task of wave.tasks) {
23
- allTasks.push(task)
24
- }
25
- }
26
-
27
- // 检查 1: 至少有一个 checkbox task
28
- if (allTasks.length === 0) {
29
- errors.push('plan.md 中没有找到 checkbox task(格式: "- [ ] task-XX: 任务名")')
30
- return { ok: false, errors, warnings, tasks: allTasks, waves }
31
- }
32
-
33
- // 检查 2: task id 唯一性
34
- const idCounts = {}
35
- for (const task of allTasks) {
36
- if (task.index != null) {
37
- const key = `task-${task.index}`
38
- idCounts[key] = (idCounts[key] || 0) + 1
39
- }
40
- }
41
- for (const [id, count] of Object.entries(idCounts)) {
42
- if (count > 1) {
43
- errors.push(`task id 重复: ${id} 出现 ${count} 次`)
44
- }
45
- }
46
-
47
- // 检查 3: task id 连续性(从 1 开始)
48
- const ids = allTasks
49
- .map(t => t.index)
50
- .filter(i => i != null)
51
- .sort((a, b) => a - b)
52
- if (ids.length > 0) {
53
- const expected = Array.from({ length: ids.length }, (_, i) => ids[0] + i)
54
- // 只检查以 task-01 起始的情况(常见模式)
55
- if (ids[0] === 1) {
56
- for (let i = 0; i < ids.length; i++) {
57
- if (ids[i] !== i + 1) {
58
- errors.push(`task id 不连续: 期望 task-${String(i + 1).padStart(2, '0')}, 实际 task-${String(ids[i]).padStart(2, '0')}`)
59
- break
60
- }
61
- }
62
- }
63
- }
64
-
65
- // 检查 4: task name 非空
66
- for (const task of allTasks) {
67
- if (!task.name || !task.name.trim()) {
68
- errors.push(`task-${String(task.index || '?').padStart(2, '0')}: 任务名为空`)
69
- }
70
- }
71
-
72
- // 检查 5: task 无 id 的 warning(不限制只在有 id 时检查)
73
- for (const wave of waves) {
74
- for (const task of wave.tasks) {
75
- if (task.index == null) {
76
- warnings.push(`Wave ${wave.index}: task "${task.name}" 没有 task id(建议格式 task-XX: 名称)`)
77
- }
78
- }
79
- }
80
-
81
- return { ok: errors.length === 0, errors, warnings, tasks: allTasks, waves }
82
- }
83
-
84
- export const definition = {
85
- name: 'execute',
86
- title: '波次执行',
87
- description: '子代理并行 + 强制 TDD + 两阶段审查',
88
- steps: [] // 动态构建,由 buildExecuteSteps() 生成
89
- }
90
-
91
- // 固定前缀步骤定义
92
- const fixedPrefix = [
93
- {
94
- name: '状态检查',
95
- prompt: `检查当前状态,确认可以执行 execute。
96
-
97
- ### 操作
98
- 1. 运行 \`sillyspec progress show\`
99
- 2. 确认 currentStage 为 execute
100
- 3. 如果不是 → 检查是否有未完成的 tasks.md
101
- 4. 确认执行范围($ARGUMENTS 指定 wave/task 或全部)
102
-
103
- ### 输出
104
- 当前状态 + 执行范围确认`,
105
- outputHint: '当前状态 + 执行范围',
106
- optional: false
107
- },
108
- {
109
- name: '加载上下文',
110
- prompt: `加载计划、设计和代码库上下文。
111
-
112
- ### 操作
113
- 1. 读取 tasks.md(执行计划)
114
- 2. 读取 design.md(技术方案)
115
- 3. 读取 CONVENTIONS.md、ARCHITECTURE.md
116
- 4. 读取 local.yaml(构建命令)
117
- 5. 加载 CODEBASE-OVERVIEW.md
118
-
119
- ### 模块文档加载
120
- 6. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(不存在则跳过以下步骤)
121
- 7. 根据 plan.md 中的任务文件路径匹配 _module-map.yaml 中的模块
122
- 8. 读取匹配到的 \`.sillyspec/docs/<project>/modules/<module>.md\`
123
- 9. 实现代码时遵循模块文档中描述的接口约定、数据流和依赖关系
124
- 10. **利用模块索引快速定位源码**:
125
- - 用 entrypoints 字段直接找到模块对外 API 的源码位置
126
- - 用 main_symbols 字段找到核心类/函数的定义位置
127
- - 子代理优先读模块卡片理解语义,再读 entrypoints/main_symbols 对应的源码
128
-
129
- ### 符号影响面扩展检查
130
- 11. **符号影响面扫描**(Critical — execute 前必做):
131
- - 读取所有 tasks/task-NN.md,提取每个任务涉及的修改文件
132
- - 对每个修改文件,检查是否涉及以下变更类型:
133
- - class 构造函数参数变更(新增/删除/修改参数)
134
- - 接口(interface)定义变更
135
- - DTO / 类型定义变更
136
- - API client 方法签名变更
137
- - 函数/方法签名变更(参数增删改)
138
- - 如果涉及上述变更类型,执行调用点搜索:
139
- \`\`\`bash
140
- rg "new ClassName\(" src/
141
- rg "ClassName\(" src/
142
- rg "methodName\(" src/
143
- rg "import.*from.*filePath" src/
144
- \`\`\`
145
- - 将搜索到的调用点与 plan.md 和 tasks/task-NN.md 的 allowed_paths 对比
146
- - **发现调用点不在任何 task 的 allowed_paths 中 → 直接阻断 execute**
147
- - 报告:列出每个受影响符号、调用点位置、是否在任务范围内
148
- - 如果调用点不在范围内但任务明确写了"不改原因",记录但不阻断
149
-
150
- ### 输出
151
- 已加载的上下文摘要(含模块文档 + 源码锚点)`,
152
- outputHint: '上下文摘要',
153
- optional: false
154
- },
155
- {
156
- name: '确认 worktree 路径',
157
- prompt: `确认当前 worktree 状态,提取隔离路径。
158
-
159
- ### 操作
160
- 1. 运行 \`sillyspec worktree meta <change-name>\` 读取 meta.json
161
- 2. 从输出中提取 worktreePath、branch、mode 字段
162
- 3. 确认 worktree 目录存在(如果是 worktree/native-worktree 模式)
163
-
164
- ### 铁律
165
- - **worktree 已由 CLI 在 execute 阶段启动时自动创建,不要自行创建或跳过**
166
- - **后续所有子代理的 cwd 必须设为该 worktree 路径**
167
- - 如果 meta.json 不存在(说明创建失败),停止并报错
168
- - **不要自行检查 git dirty/uncommitted 状态来判断是否可以进入 worktree,CLI 已自动处理**
169
-
170
- ### 输出
171
- worktree 路径 + 分支名 + 模式
172
-
173
- ### 完成后执行
174
- sillyspec run execute --done --output "worktree 路径 + 分支名 + 模式"`,
175
- outputHint: 'worktree 路径 + 分支名 + 模式',
176
- optional: false
177
- },
178
- {
179
- name: '确认执行范围',
180
- prompt: `解析任务,确认执行范围和确认模式。
181
-
182
- ### 操作
183
- 1. 从 plan 中解析 Wave 分组和任务列表
184
- 2. 根据任务描述关键词为每个 Task 建议模型:
185
- - 架构/复杂推理 → 最强模型
186
- - 常规实现 → 中等模型
187
- - 简单修改 → 快速模型
188
- - 文档/写作 → 写作模型
189
- 3. 用户在 tasks.md 中的 [model:xxx] 标签优先
190
- 4. 读取 \`--confirm-mode\` 参数(由 CLI 传入,不需要询问用户):
191
- - wave — 每个 Wave 完成后展示结果(默认)
192
- - task — 每个 Task 完成后展示结果
193
- - auto — 全部自动执行
194
- 5. 查询知识库:读取 \`.sillyspec/knowledge/INDEX.md\`,根据 Task 关键词匹配
195
-
196
- ### 知识命中报告
197
- {KNOWLEDGE_HIT_REPORT}
198
-
199
- 如上所示的知识条目与本次任务相关。请阅读这些条目以获取项目约定和已知模式。
200
- 如无命中条目(Status: no matches),跳过本节。
201
-
202
- ### 铁律
203
- - **不要询问用户确认频率**,确认模式由 CLI \`--confirm-mode\` 参数决定
204
- - 如果未检测到 \`--confirm-mode\`,默认使用 wave 模式`,
205
- outputHint: 'Wave 分组 + 模型分配',
206
- optional: false
207
- }
208
- ]
209
-
210
- // 全局验收步骤定义
211
- const acceptanceSteps = [
212
- {
213
- name: '对照设计检查',
214
- mode: 'acceptance',
215
- prompt: `对照 design.md 检查所有实现是否与设计一致。
216
-
217
- ### 执行方式
218
- 本步骤由当前 agent 汇总执行,不需要为每个检查项启动独立子代理。
219
- 如需深入验证某个模块,可启动单个 QA 子代理统一处理。
220
-
221
- ### 操作
222
- 1. 读取 design.md(技术方案)
223
- 2. 逐一对照 design.md 中的设计要点与实际代码实现
224
- 3. 检查接口签名、数据结构、模块划分是否一致
225
- 4. 记录偏差项(偏差 ≠ 错误,可能是合理的实现调整)
226
-
227
- ### 输出
228
- 检查清单:每项设计要点的实现状态 ✅/⚠️/❌ + 偏差说明`,
229
- outputHint: '设计对照检查清单',
230
- optional: false
231
- },
232
- {
233
- name: '运行测试',
234
- mode: 'acceptance',
235
- prompt: `运行所有测试,验证代码质量。
236
-
237
- ### 执行方式
238
- 本步骤由当前 agent 执行,不需要启动独立子代理。
239
-
240
- ### 操作
241
- 1. 读取 local.yaml 获取构建和测试命令
242
- 2. 运行测试套件(单元测试、集成测试)
243
- 3. 运行 lint 检查
244
- 4. 如果有测试失败 → 分析原因,标注是代码问题还是测试本身的问题
245
- 5. 汇总测试结果
246
-
247
- ### 输出
248
- 测试结果摘要:通过/失败/跳过数量 + 失败项分析`,
249
- outputHint: '测试结果摘要',
250
- optional: false
251
- },
252
- {
253
- name: '代码审查',
254
- mode: 'acceptance',
255
- prompt: `对本次变更进行代码审查。
256
-
257
- ### 执行方式
258
- 本步骤由当前 agent 或一个 QA agent 汇总执行,不需要为每个文件启动独立子代理。
259
-
260
- ### 操作
261
- 1. 检查 git diff 查看所有变更
262
- 2. 审查要点:
263
- - 代码风格是否符合 CONVENTIONS.md
264
- - 是否有明显的 bug 或安全漏洞
265
- - 是否有未处理的 TODO/FIXME
266
- - 错误处理是否完善
267
- - 是否有冗余代码或可简化的逻辑
268
- 3. 对照 ARCHITECTURE.md 检查架构合规性
269
-
270
- ### 输出
271
- 审查结果:问题列表(严重程度 + 建议修复方式)+ 总体评价`,
272
- outputHint: '代码审查结果',
273
- optional: true
274
- }
275
- ]
276
-
277
- // 固定后缀步骤定义
278
- const fixedSuffix = [
279
- {
280
- name: '知识库审阅',
281
- prompt: `检查本轮执行产生的新知识。
282
-
283
- ### 操作
284
- 1. 检查 \.sillyspec/knowledge/uncategorized.md\` 中待确认条目
285
- 2. 如有 → 提示用户审阅
286
- 3. 用户确认后改为 [已确认],可归类到专题文件
287
-
288
- ### 输出
289
- 新知识条目数量 + 审阅提示(或"无新知识")`,
290
- outputHint: '知识条目数量',
291
- optional: true
292
- },
293
- {
294
- name: '完成确认',
295
- prompt: `所有任务完成后的收尾。
296
-
297
- 先检查当前 worktree 的隔离模式:
298
- \`\`\`bash
299
- node -e "import('./src/worktree.js').then(w => { const wm = new w.WorktreeManager(); const m = wm.getMeta('<change-name>'); console.log(m ? JSON.stringify({mode: m.mode, path: m.worktreePath}) : 'no meta'); })"
300
- \`\`\`
301
-
302
- ### 操作(mode = worktree,SillySpec 创建的隔离 worktree)
303
-
304
- **自动审计流程(不需要用户确认代码):**
305
-
306
- 1. 运行 \`sillyspec worktree assess <change-name>\` 自动风险审计
307
- 2. 系统自动检查:
308
- - patch --check 是否通过
309
- - 变更是否在 allowed_paths 内
310
- - 主工作区 baseline 是否变化
311
- - 是否有高风险文件(lockfile/migration/配置/入口)
312
- - diff 规模是否异常
313
- 3. 输出 Apply Decision:
314
-
315
- \`\`\`
316
- Worktree Apply Decision
317
- ────────────────────────
318
- Decision: SAFE | WARNING | BLOCKED
319
- Changed files: N
320
- Additions: +N Deletions: -N
321
- Risky files: none | <list>
322
- Action: auto-applied | blocked
323
- \`\`\`
324
-
325
- 4. **SAFE** → 自动 \`sillyspec worktree apply <change-name>\` + cleanup
326
- 5. **WARNING** → 自动 apply(有警告但不阻断)+ cleanup
327
- 6. **BLOCKED** → 不 apply,输出原因,提示用户检查:
328
- - \`sillyspec worktree diff <change-name>\` 查看具体变更
329
- - \`sillyspec worktree cleanup <change-name>\` 丢弃
330
- 7. 建议下一步:\`sillyspec run verify\`
331
-
332
- ### 操作(mode = native-worktree,用户已有的 linked worktree)
333
- 1. 同上自动审计流程
334
- 2. SAFE/WARNING → \`sillyspec worktree apply <change-name>\`
335
- 3. **不要运行 cleanup**
336
- 4. 输出 Worktree: kept
337
- 5. 建议下一步:\`sillyspec run verify\`
338
-
339
- ### 操作(mode = in-place-fallback,降级模式无隔离目录)
340
- 1. 展示本次执行摘要(\`git diff\` 查看变更)
341
- 2. 跳过 apply 和 cleanup
342
- 3. 输出 Worktree: none
343
- 4. 建议下一步:\`sillyspec run verify\`
344
-
345
- ### 输出
346
- Apply Decision + 下一步建议
347
-
348
- ### 注意
349
- - 完成后运行 \`sillyspec run execute --done\` 即可自动推进阶段`,
350
- outputHint: 'apply 结果',
351
- optional: false
352
- }
353
- ]
354
-
355
- /**
356
- * 从 plan 文件解析 Wave 分组
357
- */
358
- function parseWavesFromPlan(planContent) {
359
- const waves = []
360
- const lines = planContent.split('\n')
361
- let currentWave = null
362
- let currentTask = null
363
-
364
- for (const line of lines) {
365
- const waveMatch = line.match(/^#+\s*Wave\s+(\d+)/i)
366
- if (waveMatch) {
367
- currentWave = { index: parseInt(waveMatch[1]), tasks: [] }
368
- currentTask = null
369
- waves.push(currentWave)
370
- continue
371
- }
372
-
373
- if (!currentWave) continue
374
-
375
- const taskMatch = line.match(/^[-*]\s*\[[ x]\]\s*(.+)/)
376
- if (taskMatch) {
377
- const taskNoMatch = taskMatch[1].match(/\btask-(\d+)\b/i)
378
- currentTask = {
379
- index: taskNoMatch ? parseInt(taskNoMatch[1], 10) : null,
380
- name: taskMatch[1].trim(),
381
- file: '',
382
- steps: '',
383
- reference: ''
384
- }
385
- // 兼容旧格式:任务名后跟 (文件路径)
386
- const fileMatch = taskMatch[1].match(/\(([^)]+)\)$/)
387
- if (fileMatch) {
388
- currentTask.file = fileMatch[1]
389
- currentTask.name = taskMatch[1].replace(/\([^)]+\)$/, '').trim()
390
- }
391
- currentWave.tasks.push(currentTask)
392
- continue
393
- }
394
-
395
- // 解析子行信息(修改/参考/步骤)
396
- if (currentTask) {
397
- const modMatch = line.match(/^\s+-\s*修改:\s*(.+)/)
398
- if (modMatch) { currentTask.file = modMatch[1].trim(); continue }
399
-
400
- const refMatch = line.match(/^\s+-\s*参考:\s*(.+)/)
401
- if (refMatch) { currentTask.reference = refMatch[1].trim(); continue }
402
-
403
- const stepMatch = line.match(/^\s+-\s*步骤:/)
404
- if (stepMatch) { currentTask.steps = line.replace(/^\s+-\s*步骤:\s*/, '').trim(); continue }
405
-
406
- // 步骤续行(数字开头的子步骤)
407
- if (currentTask.steps && line.match(/^\s+\d+\./)) {
408
- currentTask.steps += '\n' + line.trim()
409
- }
410
- }
411
- }
412
-
413
- return waves
414
- }
415
-
416
- /**
417
- * 为 Wave 生成 prompt(强制子代理执行)
418
- */
419
- function buildWavePrompt(wave, waveIndex, changeDir, worktreePath) {
420
- // ── Contract Matrix:检查是否有 provider/consumer 契约需要注入 ──
421
- let contractInjection = ''
422
- if (changeDir) {
423
- try {
424
- const { buildContractMatrix, buildConsumerInjection } = require('../contract-matrix.js')
425
- const planFile = path.join(changeDir, 'plan.md')
426
- if (existsSync(planFile)) {
427
- const planContent = readFileSync(planFile, 'utf8')
428
- const contracts = buildContractMatrix(planContent, changeDir)
429
- if (contracts.length > 0) {
430
- // 收集本 wave 所有 task 的注入内容
431
- const waveTasks = wave.tasks.map((t, ti) => {
432
- const num = String(t.index || (ti + 1)).padStart(2, '0')
433
- return `task-${num}`
434
- })
435
- const relevantContracts = contracts.filter(c => waveTasks.includes(c.consumer))
436
- if (relevantContracts.length > 0) {
437
- contractInjection = `
438
- ### API Contract Matrix
439
- 本 Wave 存在前端/后端跨 task 契约:
440
- ${relevantContracts.map(c => `- **${c.consumer}** 消费 **${c.provider}** 产出的 API`).join('\n')}
441
- `
442
- // 为每个 consumer task 生成详细注入
443
- for (const taskName of waveTasks) {
444
- const injection = buildConsumerInjection(changeDir, join(changeDir, '..', '..'), taskName, contracts)
445
- if (injection) {
446
- contractInjection += `
447
- ### 子代理 ${taskName} 的契约注入
448
- 为 ${taskName} 启动子代理时,在子代理 prompt 末尾追加以下内容:
449
-
450
- <contract-injection>
451
- ${injection}
452
- </contract-injection>
453
- `
454
- }
455
- }
456
- }
457
- }
458
- }
459
- } catch {}
460
- }
461
-
462
- // 构建任务摘要(不再内联完整蓝图,减少上下文污染)
463
- const taskSummary = wave.tasks.map((t, ti) => {
464
- const taskNum = String(t.index || (ti + 1)).padStart(2, '0')
465
- const taskRelPath = changeDir
466
- ? `.sillyspec/changes/${path.basename(changeDir)}/tasks/task-${taskNum}.md`
467
- : `task-${taskNum}.md`
468
- const fileInfo = t.file ? ` (${t.file})` : ''
469
- return `task-${taskNum}: ${t.name}${fileInfo} → ${taskRelPath}`
470
- }).join('\n')
471
-
472
- const taskList = wave.tasks.map((t, ti) => {
473
- const taskNum = String(t.index || (ti + 1)).padStart(2, '0')
474
- let s = `- [ ] ${t.name}`
475
- if (t.file) s += ` (${t.file})`
476
- return s
477
- }).join('\n')
478
-
479
- const worktreeSection = (worktreePath)
480
- ? `
481
- ### 工作目录(必须严格遵守)
482
-
483
- 调用 Task 工具启动子代理时,**workdir 参数是强制必传的**。
484
- 不传 workdir 会导致子代理把文件写到主工作区而非 worktree,破坏隔离。
485
-
486
- \`\`\`json
487
- {
488
- "subagent_type": "general",
489
- "workdir": "${worktreePath}",
490
- "prompt": "在此编写任务描述..."
491
- }
492
- \`\`\`
493
-
494
- ### 注意
495
- 蓝图文件(tasks.md / design.md / proposal.md / requirements.md)在主工作区 .sillyspec/changes/<change>/ 下,它们可能不在 worktree 中。读取蓝图时使用主工作区路径,不要拼接到 worktree 路径下。
496
- `
497
- : ''
498
-
499
- return `## Wave ${waveIndex}: 执行以下任务
500
-
501
- ## 执行方式(必须严格遵守)
502
-
503
- **每个任务必须由独立子代理执行,你不要自己写代码。**
504
-
505
- 你的角色是调度者 + 审查者:
506
- 1. 为每个任务启动一个子代理(Agent tool),同 Wave 内可并行
507
- 2. 子代理完成后审查结果
508
- 3. 勾选 plan.md 中的 checkbox
509
- 4. 记录改动文件和测试结果
510
-
511
- ${worktreeSection}
512
- ### 任务摘要(按需读取完整蓝图)
513
- 为每个任务启动子代理时,**只需告知任务目标和蓝图文件路径,让子代理按需读取**:
514
-
515
- ${taskSummary}
516
-
517
- 子代理 prompt 要点:
518
- 1. 任务目标(简短描述)
519
- 2. 蓝图文件路径(让子代理自行读取详情)
520
- 3. 编码铁律:先读后写、TDD、不编造方法、只做蓝图里写的事、遵守边界处理规则、不超出 allowed_paths
521
- 4. 如存在模块文档(.sillyspec/docs/*/modules/),按需读取涉及模块的 <module>.md 参考接口约定和数据流
522
-
523
- ### Wave 开始前
524
- 1. 读取 design.md 的「编码铁律」章节(如果存在),严格遵守
525
- 2. 读取 plan.md 了解全局任务划分和依赖关系
526
- 3. 确认本 Wave 的输入/输出契约(前置 Wave 产出了什么,本 Wave 需要消费什么)
527
- 4. 检查前置 Wave 的产出是否完整(文件是否存在、测试是否通过)
528
- 5. **上下文分层加载**:
529
- - 🔥 热上下文:design.md 编码铁律 + 当前 Wave 任务(必须加载)
530
- - 🌡️ 温上下文:CONVENTIONS.md + ARCHITECTURE.md(需要时加载)
531
- - ❄️ 冷上下文:其他变更的 design.md、历史 plan.md(不要主动加载,除非明确需要)
532
- ${contractInjection}
533
- ### 本 Wave 任务
534
- ${taskList}
535
-
536
- ### 调度要求
537
- 1. **同一 Wave 内的任务必须并行启动子代理,禁止串行等待。** Wave 的定义就是"无依赖、可并行",不要自行分析依赖关系。如果有依赖应该在 plan.md 的不同 Wave 中。
538
- 2. **Reverse Sync**:子代理报告实现与 design.md 不一致时,先检查是代码错了还是文档有遗漏
539
- 3. **不要频繁编译!** 编译很慢,只在以下情况运行:
540
- - 写了大量代码后需要验证语法正确性
541
- - 最后一个 Wave 完成后做一次全量编译验证
542
- - 用户明确要求编译时
543
- 4. 每个任务完成后:
544
- - **先写 review.json 再勾选 checkbox**(见下方 Task Review Gate)
545
- - 记录改动文件和测试结果
546
- 5. 遇到 BLOCKED → 记录原因,选择:重试/跳过/停止
547
-
548
- ### Task Review Gate(必须执行,不可跳过)
549
-
550
- 每个子代理完成后、勾选 checkbox **之前**,你必须创建 task review。
551
-
552
- **操作步骤:**
553
- 1. 读取当前 task 的 git diff(从 task 开始到完成的变更)
554
- 2. 对照 plan.md 中该 task 的描述和 tasks/task-XX.md(如果存在)检查实现是否符合要求
555
- 3. 写入 review.json 文件
556
- 4. **只有 review.json 写入成功后,才允许勾选 plan.md 中的 checkbox**
557
-
558
- **review.json 路径:**
559
-
560
- task-XX 对应:.sillyspec/.runtime/execute-runs/{EXECUTE_RUN_ID}/tasks/task-XX/review.json
561
-
562
- 本 execute run 的固定 ID 是:{EXECUTE_RUN_ID}
563
- **所有 task 的 review.json 必须使用这个 ID,不要自行创建新目录。**
564
-
565
- **review.json 必填字段:**
566
-
567
- { "schemaVersion": 1, "task": "task-XX", "base": "<git-base-commit>", "head": "<git-head-commit>",
568
- "changedFiles": ["src/foo.js"], "specVerdict": "pass|fail|cannot_verify",
569
- "qualityVerdict": "pass|fail|cannot_verify", "reviewerNotes": "评审说明",
570
- "requiredEvidence": [] }
571
-
572
- **评审铁律:**
573
- - 不信任 implementer 自报结果,对照 diff 和 task brief 验证
574
- - 只看当前 task 的 diff,不做全仓库漫游审查
575
- - \`cannot_verify\` 只在确实无法验证且有待补充证据时使用,且 requiredEvidence 必须非空
576
- - \`sillyspec run execute --done\` 会校验所有 task 的 review.json,缺失或 fail 会阻断完成
577
-
578
- ### 完成后
579
- 1. 为每个后端 router task,扫描变更文件提取 API 端点 artifact:
580
- - 在变更文件中搜索所有 router 注册路径(@router.get/post/put/delete)
581
- - 将端点清单写入 .sillyspec/.runtime/contract-artifacts/<task-name>/endpoints.json
582
- - 格式: { "task": "task-XX", "type": "backend_endpoints", "endpoints": [{ "method": "GET", "path": "/api/ppm/xxx" }] }
583
- 2. 运行 sillyspec run execute --done --input "用户原始反馈" --output "Wave ${waveIndex} 结果摘要"`
584
- }
585
-
586
- /**
587
- * 动态构建 execute 步骤列表
588
- * @param {string|null} planFilePath - plan 文件路径,null 则用默认 3 Wave
589
- * @param {{ worktreePath?: string, noWorktree?: boolean }} options
590
- * @returns {Array} 步骤列表
591
- */
592
- export function buildExecuteSteps(planFilePath = null, options = {}) {
593
- const noWorktree = !!options.noWorktree
594
- let waves
595
- let changeDir = null
596
-
597
- if (planFilePath && existsSync(planFilePath)) {
598
- const planContent = readFileSync(planFilePath, 'utf8')
599
- // Plan → Execute 契约由 plan 阶段完成时的 postcheck 把关(run.js completeStep),
600
- // 此处只负责解析 waves,避免 buildExecuteSteps 与进程退出耦合。
601
- waves = parseWavesFromPlan(planContent)
602
- changeDir = path.dirname(planFilePath)
603
- }
604
-
605
- // 没解析出 Wave(plan 不存在或不含可识别 task)→ 默认 3 Wave(向后兼容)
606
- if (!waves || waves.length === 0) {
607
- waves = []
608
- for (let i = 1; i <= 3; i++) {
609
- waves.push({ index: i, tasks: [{ name: `默认任务 ${i}`, file: 'TBD' }] })
610
- }
611
- }
612
-
613
- // 尝试获取 worktree 路径(可能由前缀步骤创建)
614
- const worktreePath = options.worktreePath || null
615
-
616
- const waveSteps = waves.map((wave, i) => ({
617
- name: `Wave ${i + 1} 执行`,
618
- mode: 'implementation',
619
- prompt: buildWavePrompt(wave, i + 1, changeDir, worktreePath),
620
- outputHint: `Wave ${i + 1} 执行结果`,
621
- optional: false
622
- }))
623
-
624
- return [...fixedPrefix, ...waveSteps, ...acceptanceSteps, ...fixedSuffix]
625
- }
1
+ import { existsSync, readFileSync } from 'fs'
2
+ import path from 'path'
3
+
4
+ /**
5
+ * 校验 plan.md 是否满足 execute 执行契约
6
+ * @param {string} planContent - plan.md 文件内容
7
+ * @returns {{ ok: boolean, errors: string[], warnings: string[], tasks: object[], waves: object[] }}
8
+ */
9
+ export function validatePlanForExecute(planContent) {
10
+ const errors = []
11
+ const warnings = []
12
+
13
+ if (!planContent || !planContent.trim()) {
14
+ return { ok: false, errors: ['plan.md 内容为空'], warnings, tasks: [], waves: [] }
15
+ }
16
+
17
+ const waves = parseWavesFromPlan(planContent)
18
+
19
+ // 收集所有 task
20
+ const allTasks = []
21
+ for (const wave of waves) {
22
+ for (const task of wave.tasks) {
23
+ allTasks.push(task)
24
+ }
25
+ }
26
+
27
+ // 检查 1: 至少有一个 checkbox task
28
+ if (allTasks.length === 0) {
29
+ errors.push('plan.md 中没有找到 checkbox task(格式: "- [ ] task-XX: 任务名")')
30
+ return { ok: false, errors, warnings, tasks: allTasks, waves }
31
+ }
32
+
33
+ // 检查 2: task id 唯一性
34
+ const idCounts = {}
35
+ for (const task of allTasks) {
36
+ if (task.index != null) {
37
+ const key = `task-${task.index}`
38
+ idCounts[key] = (idCounts[key] || 0) + 1
39
+ }
40
+ }
41
+ for (const [id, count] of Object.entries(idCounts)) {
42
+ if (count > 1) {
43
+ errors.push(`task id 重复: ${id} 出现 ${count} 次`)
44
+ }
45
+ }
46
+
47
+ // 检查 3: task id 连续性(从 1 开始)
48
+ const ids = allTasks
49
+ .map(t => t.index)
50
+ .filter(i => i != null)
51
+ .sort((a, b) => a - b)
52
+ if (ids.length > 0) {
53
+ const expected = Array.from({ length: ids.length }, (_, i) => ids[0] + i)
54
+ // 只检查以 task-01 起始的情况(常见模式)
55
+ if (ids[0] === 1) {
56
+ for (let i = 0; i < ids.length; i++) {
57
+ if (ids[i] !== i + 1) {
58
+ errors.push(`task id 不连续: 期望 task-${String(i + 1).padStart(2, '0')}, 实际 task-${String(ids[i]).padStart(2, '0')}`)
59
+ break
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ // 检查 4: task name 非空
66
+ for (const task of allTasks) {
67
+ if (!task.name || !task.name.trim()) {
68
+ errors.push(`task-${String(task.index || '?').padStart(2, '0')}: 任务名为空`)
69
+ }
70
+ }
71
+
72
+ // 检查 5: task 无 id 的 warning(不限制只在有 id 时检查)
73
+ for (const wave of waves) {
74
+ for (const task of wave.tasks) {
75
+ if (task.index == null) {
76
+ warnings.push(`Wave ${wave.index}: task "${task.name}" 没有 task id(建议格式 task-XX: 名称)`)
77
+ }
78
+ }
79
+ }
80
+
81
+ return { ok: errors.length === 0, errors, warnings, tasks: allTasks, waves }
82
+ }
83
+
84
+ export const definition = {
85
+ name: 'execute',
86
+ title: '波次执行',
87
+ description: '子代理并行 + 强制 TDD + 两阶段审查',
88
+ steps: [] // 动态构建,由 buildExecuteSteps() 生成
89
+ }
90
+
91
+ // 固定前缀步骤定义
92
+ const fixedPrefix = [
93
+ {
94
+ name: '状态检查',
95
+ prompt: `检查当前状态,确认可以执行 execute。
96
+
97
+ ### 操作
98
+ 1. 运行 \`sillyspec progress show\`
99
+ 2. 确认 currentStage 为 execute
100
+ 3. 如果不是 → 检查是否有未完成的 tasks.md
101
+ 4. 确认执行范围($ARGUMENTS 指定 wave/task 或全部)
102
+
103
+ ### 输出
104
+ 当前状态 + 执行范围确认`,
105
+ outputHint: '当前状态 + 执行范围',
106
+ optional: false
107
+ },
108
+ {
109
+ name: '加载上下文',
110
+ prompt: `加载计划、设计和代码库上下文。
111
+
112
+ ### 操作
113
+ 1. 读取 tasks.md(执行计划)
114
+ 2. 读取 design.md(技术方案)
115
+ 3. 读取 CONVENTIONS.md、ARCHITECTURE.md
116
+ 4. 读取 local.yaml(构建命令)
117
+ 5. 加载 CODEBASE-OVERVIEW.md
118
+
119
+ ### 模块文档加载
120
+ 6. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(不存在则跳过以下步骤)
121
+ 7. 根据 plan.md 中的任务文件路径匹配 _module-map.yaml 中的模块
122
+ 8. 读取匹配到的 \`.sillyspec/docs/<project>/modules/<module>.md\`
123
+ 9. 实现代码时遵循模块文档中描述的接口约定、数据流和依赖关系
124
+ 10. **利用模块索引快速定位源码**:
125
+ - 用 entrypoints 字段直接找到模块对外 API 的源码位置
126
+ - 用 main_symbols 字段找到核心类/函数的定义位置
127
+ - 子代理优先读模块卡片理解语义,再读 entrypoints/main_symbols 对应的源码
128
+
129
+ ### 符号影响面扩展检查
130
+ 11. **符号影响面扫描**(Critical — execute 前必做):
131
+ - 读取所有 tasks/task-NN.md,提取每个任务涉及的修改文件
132
+ - 对每个修改文件,检查是否涉及以下变更类型:
133
+ - class 构造函数参数变更(新增/删除/修改参数)
134
+ - 接口(interface)定义变更
135
+ - DTO / 类型定义变更
136
+ - API client 方法签名变更
137
+ - 函数/方法签名变更(参数增删改)
138
+ - 如果涉及上述变更类型,执行调用点搜索:
139
+ \`\`\`bash
140
+ rg "new ClassName\(" src/
141
+ rg "ClassName\(" src/
142
+ rg "methodName\(" src/
143
+ rg "import.*from.*filePath" src/
144
+ \`\`\`
145
+ - 将搜索到的调用点与 plan.md 和 tasks/task-NN.md 的 allowed_paths 对比
146
+ - **发现调用点不在任何 task 的 allowed_paths 中 → 直接阻断 execute**
147
+ - 报告:列出每个受影响符号、调用点位置、是否在任务范围内
148
+ - 如果调用点不在范围内但任务明确写了"不改原因",记录但不阻断
149
+
150
+ ### 输出
151
+ 已加载的上下文摘要(含模块文档 + 源码锚点)`,
152
+ outputHint: '上下文摘要',
153
+ optional: false
154
+ },
155
+ {
156
+ name: '确认 worktree 路径',
157
+ prompt: `确认当前 worktree 状态,提取隔离路径。
158
+
159
+ ### 操作
160
+ 1. 运行 \`sillyspec worktree meta <change-name>\` 读取 meta.json
161
+ 2. 从输出中提取 worktreePath、branch、mode 字段
162
+ 3. 确认 worktree 目录存在(如果是 worktree/native-worktree 模式)
163
+
164
+ ### 铁律
165
+ - **worktree 已由 CLI 在 execute 阶段启动时自动创建,不要自行创建或跳过**
166
+ - **后续所有子代理的 cwd 必须设为该 worktree 路径**
167
+ - 如果 meta.json 不存在(说明创建失败),停止并报错
168
+ - **不要自行检查 git dirty/uncommitted 状态来判断是否可以进入 worktree,CLI 已自动处理**
169
+
170
+ ### 输出
171
+ worktree 路径 + 分支名 + 模式
172
+
173
+ ### 完成后执行
174
+ sillyspec run execute --done --output "worktree 路径 + 分支名 + 模式"`,
175
+ outputHint: 'worktree 路径 + 分支名 + 模式',
176
+ optional: false
177
+ },
178
+ {
179
+ name: '确认执行范围',
180
+ prompt: `解析任务,确认执行范围和确认模式。
181
+
182
+ ### 操作
183
+ 1. 从 plan 中解析 Wave 分组和任务列表
184
+ 2. 根据任务描述关键词为每个 Task 建议模型:
185
+ - 架构/复杂推理 → 最强模型
186
+ - 常规实现 → 中等模型
187
+ - 简单修改 → 快速模型
188
+ - 文档/写作 → 写作模型
189
+ 3. 用户在 tasks.md 中的 [model:xxx] 标签优先
190
+ 4. 读取 \`--confirm-mode\` 参数(由 CLI 传入,不需要询问用户):
191
+ - wave — 每个 Wave 完成后展示结果(默认)
192
+ - task — 每个 Task 完成后展示结果
193
+ - auto — 全部自动执行
194
+ 5. 查询知识库:读取 \`.sillyspec/knowledge/INDEX.md\`,根据 Task 关键词匹配
195
+
196
+ ### 知识命中报告
197
+ {KNOWLEDGE_HIT_REPORT}
198
+
199
+ 如上所示的知识条目与本次任务相关。请阅读这些条目以获取项目约定和已知模式。
200
+ 如无命中条目(Status: no matches),跳过本节。
201
+
202
+ ### 铁律
203
+ - **不要询问用户确认频率**,确认模式由 CLI \`--confirm-mode\` 参数决定
204
+ - 如果未检测到 \`--confirm-mode\`,默认使用 wave 模式`,
205
+ outputHint: 'Wave 分组 + 模型分配',
206
+ optional: false
207
+ }
208
+ ]
209
+
210
+ // 全局验收步骤定义
211
+ const acceptanceSteps = [
212
+ {
213
+ name: '对照设计检查',
214
+ mode: 'acceptance',
215
+ prompt: `对照 design.md 检查所有实现是否与设计一致。
216
+
217
+ ### 执行方式
218
+ 本步骤由当前 agent 汇总执行,不需要为每个检查项启动独立子代理。
219
+ 如需深入验证某个模块,可启动单个 QA 子代理统一处理。
220
+
221
+ ### 操作
222
+ 1. 读取 design.md(技术方案)
223
+ 2. 逐一对照 design.md 中的设计要点与实际代码实现
224
+ 3. 检查接口签名、数据结构、模块划分是否一致
225
+ 4. 记录偏差项(偏差 ≠ 错误,可能是合理的实现调整)
226
+
227
+ ### 输出
228
+ 检查清单:每项设计要点的实现状态 ✅/⚠️/❌ + 偏差说明`,
229
+ outputHint: '设计对照检查清单',
230
+ optional: false
231
+ },
232
+ {
233
+ name: '运行测试',
234
+ mode: 'acceptance',
235
+ prompt: `运行所有测试,验证代码质量。
236
+
237
+ ### 执行方式
238
+ 本步骤由当前 agent 执行,不需要启动独立子代理。
239
+
240
+ ### 操作
241
+ 1. 读取 local.yaml 获取构建和测试命令
242
+ 2. 运行测试套件(单元测试、集成测试)
243
+ 3. 运行 lint 检查
244
+ 4. 如果有测试失败 → 分析原因,标注是代码问题还是测试本身的问题
245
+ 5. 汇总测试结果
246
+
247
+ ### 输出
248
+ 测试结果摘要:通过/失败/跳过数量 + 失败项分析`,
249
+ outputHint: '测试结果摘要',
250
+ optional: false
251
+ },
252
+ {
253
+ name: '代码审查',
254
+ mode: 'acceptance',
255
+ prompt: `对本次变更进行代码审查。
256
+
257
+ ### 执行方式
258
+ 本步骤由当前 agent 或一个 QA agent 汇总执行,不需要为每个文件启动独立子代理。
259
+
260
+ ### 操作
261
+ 1. 检查 git diff 查看所有变更
262
+ 2. 审查要点:
263
+ - 代码风格是否符合 CONVENTIONS.md
264
+ - 是否有明显的 bug 或安全漏洞
265
+ - 是否有未处理的 TODO/FIXME
266
+ - 错误处理是否完善
267
+ - 是否有冗余代码或可简化的逻辑
268
+ 3. 对照 ARCHITECTURE.md 检查架构合规性
269
+
270
+ ### 输出
271
+ 审查结果:问题列表(严重程度 + 建议修复方式)+ 总体评价`,
272
+ outputHint: '代码审查结果',
273
+ optional: true
274
+ }
275
+ ]
276
+
277
+ // 固定后缀步骤定义
278
+ const fixedSuffix = [
279
+ {
280
+ name: '知识库审阅',
281
+ prompt: `检查本轮执行产生的新知识。
282
+
283
+ ### 操作
284
+ 1. 检查 \.sillyspec/knowledge/uncategorized.md\` 中待确认条目
285
+ 2. 如有 → 提示用户审阅
286
+ 3. 用户确认后改为 [已确认],可归类到专题文件
287
+
288
+ ### 输出
289
+ 新知识条目数量 + 审阅提示(或"无新知识")`,
290
+ outputHint: '知识条目数量',
291
+ optional: true
292
+ },
293
+ {
294
+ name: '完成确认',
295
+ prompt: `所有任务完成后的收尾。
296
+
297
+ 先检查当前 worktree 的隔离模式:
298
+ \`\`\`bash
299
+ node -e "import('./src/worktree.js').then(w => { const wm = new w.WorktreeManager(); const m = wm.getMeta('<change-name>'); console.log(m ? JSON.stringify({mode: m.mode, path: m.worktreePath}) : 'no meta'); })"
300
+ \`\`\`
301
+
302
+ ### 操作(mode = worktree,SillySpec 创建的隔离 worktree)
303
+
304
+ **自动审计流程(不需要用户确认代码):**
305
+
306
+ 1. 运行 \`sillyspec worktree assess <change-name>\` 自动风险审计
307
+ 2. 系统自动检查:
308
+ - patch --check 是否通过
309
+ - 变更是否在 allowed_paths 内
310
+ - 主工作区 baseline 是否变化
311
+ - 是否有高风险文件(lockfile/migration/配置/入口)
312
+ - diff 规模是否异常
313
+ 3. 输出 Apply Decision:
314
+
315
+ \`\`\`
316
+ Worktree Apply Decision
317
+ ────────────────────────
318
+ Decision: SAFE | WARNING | BLOCKED
319
+ Changed files: N
320
+ Additions: +N Deletions: -N
321
+ Risky files: none | <list>
322
+ Action: auto-applied | blocked
323
+ \`\`\`
324
+
325
+ 4. **SAFE** → 自动 \`sillyspec worktree apply <change-name>\` + cleanup
326
+ 5. **WARNING** → 自动 apply(有警告但不阻断)+ cleanup
327
+ 6. **BLOCKED** → 不 apply,输出原因,提示用户检查:
328
+ - \`sillyspec worktree diff <change-name>\` 查看具体变更
329
+ - \`sillyspec worktree cleanup <change-name>\` 丢弃
330
+ 7. 建议下一步:\`sillyspec run verify\`
331
+
332
+ ### 操作(mode = native-worktree,用户已有的 linked worktree)
333
+ 1. 同上自动审计流程
334
+ 2. SAFE/WARNING → \`sillyspec worktree apply <change-name>\`
335
+ 3. **不要运行 cleanup**
336
+ 4. 输出 Worktree: kept
337
+ 5. 建议下一步:\`sillyspec run verify\`
338
+
339
+ ### 操作(mode = in-place-fallback,降级模式无隔离目录)
340
+ 1. 展示本次执行摘要(\`git diff\` 查看变更)
341
+ 2. 跳过 apply 和 cleanup
342
+ 3. 输出 Worktree: none
343
+ 4. 建议下一步:\`sillyspec run verify\`
344
+
345
+ ### 输出
346
+ Apply Decision + 下一步建议
347
+
348
+ ### 注意
349
+ - 完成后运行 \`sillyspec run execute --done\` 即可自动推进阶段`,
350
+ outputHint: 'apply 结果',
351
+ optional: false
352
+ }
353
+ ]
354
+
355
+ /**
356
+ * 从 plan 文件解析 Wave 分组
357
+ */
358
+ function parseWavesFromPlan(planContent) {
359
+ const waves = []
360
+ const lines = planContent.split('\n')
361
+ let currentWave = null
362
+ let currentTask = null
363
+
364
+ for (const line of lines) {
365
+ const waveMatch = line.match(/^#+\s*Wave\s+(\d+)/i)
366
+ if (waveMatch) {
367
+ currentWave = { index: parseInt(waveMatch[1]), tasks: [] }
368
+ currentTask = null
369
+ waves.push(currentWave)
370
+ continue
371
+ }
372
+
373
+ if (!currentWave) continue
374
+
375
+ const taskMatch = line.match(/^[-*]\s*\[[ x]\]\s*(.+)/)
376
+ if (taskMatch) {
377
+ const taskNoMatch = taskMatch[1].match(/\btask-(\d+)\b/i)
378
+ currentTask = {
379
+ index: taskNoMatch ? parseInt(taskNoMatch[1], 10) : null,
380
+ name: taskMatch[1].trim(),
381
+ file: '',
382
+ steps: '',
383
+ reference: ''
384
+ }
385
+ // 兼容旧格式:任务名后跟 (文件路径)
386
+ const fileMatch = taskMatch[1].match(/\(([^)]+)\)$/)
387
+ if (fileMatch) {
388
+ currentTask.file = fileMatch[1]
389
+ currentTask.name = taskMatch[1].replace(/\([^)]+\)$/, '').trim()
390
+ }
391
+ currentWave.tasks.push(currentTask)
392
+ continue
393
+ }
394
+
395
+ // 解析子行信息(修改/参考/步骤)
396
+ if (currentTask) {
397
+ const modMatch = line.match(/^\s+-\s*修改:\s*(.+)/)
398
+ if (modMatch) { currentTask.file = modMatch[1].trim(); continue }
399
+
400
+ const refMatch = line.match(/^\s+-\s*参考:\s*(.+)/)
401
+ if (refMatch) { currentTask.reference = refMatch[1].trim(); continue }
402
+
403
+ const stepMatch = line.match(/^\s+-\s*步骤:/)
404
+ if (stepMatch) { currentTask.steps = line.replace(/^\s+-\s*步骤:\s*/, '').trim(); continue }
405
+
406
+ // 步骤续行(数字开头的子步骤)
407
+ if (currentTask.steps && line.match(/^\s+\d+\./)) {
408
+ currentTask.steps += '\n' + line.trim()
409
+ }
410
+ }
411
+ }
412
+
413
+ return waves
414
+ }
415
+
416
+ /**
417
+ * 为 Wave 生成 prompt(强制子代理执行)
418
+ */
419
+ function buildWavePrompt(wave, waveIndex, changeDir, worktreePath) {
420
+ // ── Contract Matrix:检查是否有 provider/consumer 契约需要注入 ──
421
+ let contractInjection = ''
422
+ if (changeDir) {
423
+ try {
424
+ const { buildContractMatrix, buildConsumerInjection } = require('../contract-matrix.js')
425
+ const planFile = path.join(changeDir, 'plan.md')
426
+ if (existsSync(planFile)) {
427
+ const planContent = readFileSync(planFile, 'utf8')
428
+ const contracts = buildContractMatrix(planContent, changeDir)
429
+ if (contracts.length > 0) {
430
+ // 收集本 wave 所有 task 的注入内容
431
+ const waveTasks = wave.tasks.map((t, ti) => {
432
+ const num = String(t.index || (ti + 1)).padStart(2, '0')
433
+ return `task-${num}`
434
+ })
435
+ const relevantContracts = contracts.filter(c => waveTasks.includes(c.consumer))
436
+ if (relevantContracts.length > 0) {
437
+ contractInjection = `
438
+ ### API Contract Matrix
439
+ 本 Wave 存在前端/后端跨 task 契约:
440
+ ${relevantContracts.map(c => `- **${c.consumer}** 消费 **${c.provider}** 产出的 API`).join('\n')}
441
+ `
442
+ // 为每个 consumer task 生成详细注入
443
+ for (const taskName of waveTasks) {
444
+ const injection = buildConsumerInjection(changeDir, join(changeDir, '..', '..'), taskName, contracts)
445
+ if (injection) {
446
+ contractInjection += `
447
+ ### 子代理 ${taskName} 的契约注入
448
+ 为 ${taskName} 启动子代理时,在子代理 prompt 末尾追加以下内容:
449
+
450
+ <contract-injection>
451
+ ${injection}
452
+ </contract-injection>
453
+ `
454
+ }
455
+ }
456
+ }
457
+ }
458
+ }
459
+ } catch {}
460
+ }
461
+
462
+ // 构建任务摘要(不再内联完整蓝图,减少上下文污染)
463
+ const taskSummary = wave.tasks.map((t, ti) => {
464
+ const taskNum = String(t.index || (ti + 1)).padStart(2, '0')
465
+ const taskRelPath = changeDir
466
+ ? `.sillyspec/changes/${path.basename(changeDir)}/tasks/task-${taskNum}.md`
467
+ : `task-${taskNum}.md`
468
+ const fileInfo = t.file ? ` (${t.file})` : ''
469
+ return `task-${taskNum}: ${t.name}${fileInfo} → ${taskRelPath}`
470
+ }).join('\n')
471
+
472
+ const taskList = wave.tasks.map((t, ti) => {
473
+ const taskNum = String(t.index || (ti + 1)).padStart(2, '0')
474
+ let s = `- [ ] ${t.name}`
475
+ if (t.file) s += ` (${t.file})`
476
+ return s
477
+ }).join('\n')
478
+
479
+ const worktreeSection = (worktreePath)
480
+ ? `
481
+ ### 工作目录(必须严格遵守)
482
+
483
+ 调用 Task 工具启动子代理时,**workdir 参数是强制必传的**。
484
+ 不传 workdir 会导致子代理把文件写到主工作区而非 worktree,破坏隔离。
485
+
486
+ \`\`\`json
487
+ {
488
+ "subagent_type": "general",
489
+ "workdir": "${worktreePath}",
490
+ "prompt": "在此编写任务描述..."
491
+ }
492
+ \`\`\`
493
+
494
+ ### 注意
495
+ 蓝图文件(tasks.md / design.md / proposal.md / requirements.md)在主工作区 .sillyspec/changes/<change>/ 下,它们可能不在 worktree 中。读取蓝图时使用主工作区路径,不要拼接到 worktree 路径下。
496
+ `
497
+ : ''
498
+
499
+ return `## Wave ${waveIndex}: 执行以下任务
500
+
501
+ ## 执行方式(必须严格遵守)
502
+
503
+ **每个任务必须由独立子代理执行,你不要自己写代码。**
504
+
505
+ 你的角色是调度者 + 审查者:
506
+ 1. 为每个任务启动一个子代理(Agent tool),同 Wave 内可并行
507
+ 2. 子代理完成后审查结果
508
+ 3. 勾选 plan.md 中的 checkbox
509
+ 4. 记录改动文件和测试结果
510
+
511
+ ${worktreeSection}
512
+ ### 任务摘要(按需读取完整蓝图)
513
+ 为每个任务启动子代理时,**只需告知任务目标和蓝图文件路径,让子代理按需读取**:
514
+
515
+ ${taskSummary}
516
+
517
+ 子代理 prompt 要点:
518
+ 1. 任务目标(简短描述)
519
+ 2. 蓝图文件路径(让子代理自行读取详情)
520
+ 3. 编码铁律:先读后写、TDD、不编造方法、只做蓝图里写的事、遵守边界处理规则、不超出 allowed_paths
521
+ 4. 如存在模块文档(.sillyspec/docs/*/modules/),按需读取涉及模块的 <module>.md 参考接口约定和数据流
522
+
523
+ ### Wave 开始前
524
+ 1. 读取 design.md 的「编码铁律」章节(如果存在),严格遵守
525
+ 2. 读取 plan.md 了解全局任务划分和依赖关系
526
+ 3. 确认本 Wave 的输入/输出契约(前置 Wave 产出了什么,本 Wave 需要消费什么)
527
+ 4. 检查前置 Wave 的产出是否完整(文件是否存在、测试是否通过)
528
+ 5. **上下文分层加载**:
529
+ - 🔥 热上下文:design.md 编码铁律 + 当前 Wave 任务(必须加载)
530
+ - 🌡️ 温上下文:CONVENTIONS.md + ARCHITECTURE.md(需要时加载)
531
+ - ❄️ 冷上下文:其他变更的 design.md、历史 plan.md(不要主动加载,除非明确需要)
532
+ ${contractInjection}
533
+ ### 本 Wave 任务
534
+ ${taskList}
535
+
536
+ ### 调度要求
537
+ 1. **同一 Wave 内的任务必须并行启动子代理,禁止串行等待。** Wave 的定义就是"无依赖、可并行",不要自行分析依赖关系。如果有依赖应该在 plan.md 的不同 Wave 中。
538
+ 2. **Reverse Sync**:子代理报告实现与 design.md 不一致时,先检查是代码错了还是文档有遗漏
539
+ 3. **不要频繁编译!** 编译很慢,只在以下情况运行:
540
+ - 写了大量代码后需要验证语法正确性
541
+ - 最后一个 Wave 完成后做一次全量编译验证
542
+ - 用户明确要求编译时
543
+ 4. 每个任务完成后:
544
+ - **先写 review.json 再勾选 checkbox**(见下方 Task Review Gate)
545
+ - 记录改动文件和测试结果
546
+ 5. 遇到 BLOCKED → 记录原因,选择:重试/跳过/停止
547
+
548
+ ### Task Review Gate(必须执行,不可跳过)
549
+
550
+ 每个子代理完成后、勾选 checkbox **之前**,你必须创建 task review。
551
+
552
+ **操作步骤:**
553
+ 1. 读取当前 task 的 git diff(从 task 开始到完成的变更)
554
+ 2. 对照 plan.md 中该 task 的描述和 tasks/task-XX.md(如果存在)检查实现是否符合要求
555
+ 3. 写入 review.json 文件
556
+ 4. **只有 review.json 写入成功后,才允许勾选 plan.md 中的 checkbox**
557
+
558
+ **review.json 路径:**
559
+
560
+ task-XX 对应:.sillyspec/.runtime/execute-runs/{EXECUTE_RUN_ID}/tasks/task-XX/review.json
561
+
562
+ 本 execute run 的固定 ID 是:{EXECUTE_RUN_ID}
563
+ **所有 task 的 review.json 必须使用这个 ID,不要自行创建新目录。**
564
+
565
+ **review.json 必填字段:**
566
+
567
+ { "schemaVersion": 1, "task": "task-XX", "base": "<git-base-commit>", "head": "<git-head-commit>",
568
+ "changedFiles": ["src/foo.js"], "specVerdict": "pass|fail|cannot_verify",
569
+ "qualityVerdict": "pass|fail|cannot_verify", "reviewerNotes": "评审说明",
570
+ "requiredEvidence": [] }
571
+
572
+ **评审铁律:**
573
+ - 不信任 implementer 自报结果,对照 diff 和 task brief 验证
574
+ - 只看当前 task 的 diff,不做全仓库漫游审查
575
+ - \`cannot_verify\` 只在确实无法验证且有待补充证据时使用,且 requiredEvidence 必须非空
576
+ - \`sillyspec run execute --done\` 会校验所有 task 的 review.json,缺失或 fail 会阻断完成
577
+
578
+ ### 完成后
579
+ 1. 为每个后端 router task,扫描变更文件提取 API 端点 artifact:
580
+ - 在变更文件中搜索所有 router 注册路径(@router.get/post/put/delete)
581
+ - 将端点清单写入 .sillyspec/.runtime/contract-artifacts/<task-name>/endpoints.json
582
+ - 格式: { "task": "task-XX", "type": "backend_endpoints", "endpoints": [{ "method": "GET", "path": "/api/ppm/xxx" }] }
583
+ 2. 运行 sillyspec run execute --done --input "用户原始反馈" --output "Wave ${waveIndex} 结果摘要"`
584
+ }
585
+
586
+ /**
587
+ * 动态构建 execute 步骤列表
588
+ * @param {string|null} planFilePath - plan 文件路径,null 则用默认 3 Wave
589
+ * @param {{ worktreePath?: string, noWorktree?: boolean }} options
590
+ * @returns {Array} 步骤列表
591
+ */
592
+ export function buildExecuteSteps(planFilePath = null, options = {}) {
593
+ const noWorktree = !!options.noWorktree
594
+ let waves
595
+ let changeDir = null
596
+
597
+ if (planFilePath && existsSync(planFilePath)) {
598
+ const planContent = readFileSync(planFilePath, 'utf8')
599
+ // Plan → Execute 契约由 plan 阶段完成时的 postcheck 把关(run.js completeStep),
600
+ // 此处只负责解析 waves,避免 buildExecuteSteps 与进程退出耦合。
601
+ waves = parseWavesFromPlan(planContent)
602
+ changeDir = path.dirname(planFilePath)
603
+ }
604
+
605
+ // 没解析出 Wave(plan 不存在或不含可识别 task)→ 默认 3 Wave(向后兼容)
606
+ if (!waves || waves.length === 0) {
607
+ waves = []
608
+ for (let i = 1; i <= 3; i++) {
609
+ waves.push({ index: i, tasks: [{ name: `默认任务 ${i}`, file: 'TBD' }] })
610
+ }
611
+ }
612
+
613
+ // 尝试获取 worktree 路径(可能由前缀步骤创建)
614
+ const worktreePath = options.worktreePath || null
615
+
616
+ const waveSteps = waves.map((wave, i) => ({
617
+ name: `Wave ${i + 1} 执行`,
618
+ mode: 'implementation',
619
+ prompt: buildWavePrompt(wave, i + 1, changeDir, worktreePath),
620
+ outputHint: `Wave ${i + 1} 执行结果`,
621
+ optional: false
622
+ }))
623
+
624
+ return [...fixedPrefix, ...waveSteps, ...acceptanceSteps, ...fixedSuffix]
625
+ }