sillyspec 3.18.0 → 3.18.2

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.
@@ -52,27 +52,25 @@ const fixedPrefix = [
52
52
  optional: false
53
53
  },
54
54
  {
55
- name: '创建 worktree',
56
- prompt: `为本次执行创建隔离的 git worktree。
55
+ name: '确认 worktree 路径',
56
+ prompt: `确认当前 worktree 状态,提取隔离路径。
57
57
 
58
58
  ### 操作
59
- 1. 运行 \`sillyspec worktree create <change-name>\`
60
- 2. 记录输出的 worktree 路径
61
- 3. 后续所有子代理的 cwd 设为该 worktree 路径
62
- 4. 如果创建失败 → 报错并停止(不要在无隔离状态下继续)
59
+ 1. 运行 \`sillyspec worktree meta <change-name>\` 读取 meta.json
60
+ 2. 从输出中提取 worktreePath、branch、mode 字段
61
+ 3. 确认 worktree 目录存在(如果是 worktree/native-worktree 模式)
63
62
 
64
- ### 降级模式
65
- CLI 可能自动降级(sandbox 限制、已在 linked worktree 中):
66
- - \`mode: native-worktree\` 已在 linked worktree,直接复用
67
- - \`mode: in-place-fallback\` — git worktree add 失败,降级为 in-place + baseline protection
68
- - 这两种模式都会输出 worktree 路径和分支名,正常继续即可
63
+ ### 铁律
64
+ - **worktree 已由 CLI execute 阶段启动时自动创建,不要自行创建或跳过**
65
+ - **后续所有子代理的 cwd 必须设为该 worktree 路径**
66
+ - 如果 meta.json 不存在(说明创建失败),停止并报错
69
67
 
70
68
  ### 输出
71
- worktree 路径 + 分支名 + 模式(如果有)
69
+ worktree 路径 + 分支名 + 模式
72
70
 
73
71
  ### 完成后执行
74
- sillyspec run execute --done --output "worktree 路径 + 分支名"`,
75
- outputHint: 'worktree 路径 + 分支名',
72
+ sillyspec run execute --done --output "worktree 路径 + 分支名 + 模式"`,
73
+ outputHint: 'worktree 路径 + 分支名 + 模式',
76
74
  optional: false
77
75
  },
78
76
  {
@@ -1,5 +1,4 @@
1
1
  import { definition as brainstorm } from './brainstorm.js'
2
- import { definition as propose } from './propose.js'
3
2
  import { definition as plan } from './plan.js'
4
3
  import { definition as execute } from './execute.js'
5
4
  import { definition as verify } from './verify.js'
@@ -15,7 +14,6 @@ const auxiliaryFlag = { auxiliary: true }
15
14
 
16
15
  export const stageRegistry = {
17
16
  brainstorm,
18
- propose,
19
17
  plan,
20
18
  execute,
21
19
  verify,
@@ -84,22 +84,25 @@ needs_human_review: true | false
84
84
  ### 操作
85
85
  1. 读取 CODEBASE-OVERVIEW.md + 各子项目上下文
86
86
  2. 读取 proposal.md、design.md、requirements.md、tasks.md
87
- 3. 读取 CONVENTIONS.md、ARCHITECTURE.md、STACK.md
88
- 4. 读取 local.yaml 获取构建/测试命令
87
+ 3. 如果存在 decisions.md,必须读取并提取所有当前版本 D-xxx@vN 决策 ID
88
+ - 如果发现 priority=P0/P1 且 status=unresolved/blocking 的决策,停止生成计划,要求先回到 brainstorm 的 Design Grill 修正
89
+ - 如果发现 superseded 决策,只引用最新版本,不引用旧版本
90
+ 4. 读取 CONVENTIONS.md、ARCHITECTURE.md、STACK.md
91
+ 5. 读取 local.yaml 获取构建/测试命令
89
92
 
90
93
  ### 模块文档加载
91
- 5. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(不存在则跳过以下步骤)
92
- 6. 根据 design.md 的文件变更清单匹配 _module-map.yaml 中的模块
93
- 7. 读取匹配到的 \`.sillyspec/docs/<project>/modules/<module>.md\`
94
- 8. 将模块文档作为制定计划的上下文,确保计划符合模块当前设计
95
- 9. **利用模块依赖关系辅助分析**:
94
+ 6. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(不存在则跳过以下步骤)
95
+ 7. 根据 design.md 的文件变更清单匹配 _module-map.yaml 中的模块
96
+ 8. 读取匹配到的 \`.sillyspec/docs/<project>/modules/<module>.md\`
97
+ 9. 将模块文档作为制定计划的上下文,确保计划符合模块当前设计
98
+ 10. **利用模块依赖关系辅助分析**:
96
99
  - 用 depends_on 判断哪些模块会被间接影响
97
100
  - 用 used_by 判断变更会不会影响下游模块
98
101
  - 将依赖关系纳入 Wave 分组决策(依赖同一模块的任务尽量同 Wave)
99
102
  - 如果变更涉及多个有依赖关系的模块,在 plan.md 的任务总表中标注模块依赖
100
103
 
101
104
  ### 输出
102
- 已加载的文件清单(含模块文档 + 模块依赖关系摘要)`,
105
+ 已加载的文件清单(含 decisions.md 当前版本/未决项状态、模块文档 + 模块依赖关系摘要)`,
103
106
  outputHint: '文件清单',
104
107
  optional: false
105
108
  },
@@ -170,12 +173,17 @@ plan_level: light
170
173
  - 涉及的文件/模块清单
171
174
 
172
175
  ## Tasks
173
- - [ ] task-01: ...
176
+ - [ ] task-01: ...(覆盖:FR-01, D-001@v1)
174
177
  - [ ] task-02: ...
175
178
  - [ ] task-03: ...
176
179
 
177
180
  ## 验收
178
181
  - 具体可验证的验收条目
182
+
183
+ ## 覆盖矩阵(如存在 decisions.md)
184
+ | ID | 覆盖任务 | 验收证据 |
185
+ |---|---|---|
186
+ | D-001@v1 | task-01 | AC-01 |
179
187
  \`\`\`
180
188
 
181
189
  light 计划的约束:
@@ -184,6 +192,8 @@ light 计划的约束:
184
192
  - **禁止**泛泛风险 分析(如"需要充分测试")
185
193
  - **禁止**放实现细节(函数签名、代码示例)
186
194
  - 来源/目标直接引用已有文档,不重新生成
195
+ - 如果存在 decisions.md,所有当前版本 D-xxx@vN 必须在 Tasks 或覆盖矩阵中出现
196
+ - 如果存在 P0/P1 unresolved blocker,不生成 plan.md
187
197
  - 任务列表控制在 10 条以内
188
198
  - **任务必须使用 checkbox 格式**(\`- [ ] task-XX:\`),不要用纯编号列表(\`1. 2.\`),execute 阶段依赖此格式解析任务
189
199
 
@@ -207,18 +217,18 @@ plan_level: full
207
217
  > 技术不确定性高时才需要 Spike。无不确定性则跳过此节。
208
218
 
209
219
  ## Wave 1(并行,无依赖)
210
- - [ ] task-01: 添加用户创建接口
211
- - [ ] task-02: 添加角色创建接口
220
+ - [ ] task-01: 添加用户创建接口(覆盖:FR-01, D-001@v1)
221
+ - [ ] task-02: 添加角色创建接口(覆盖:FR-02)
212
222
 
213
223
  ## Wave 2(依赖 Wave 1)
214
224
  - [ ] task-03: 用户创建接口联调
215
225
 
216
226
  ## 任务总表
217
- | 编号 | 任务 | Wave | 优先级 | 依赖 | 说明 |
218
- |---|---|---|---|---|---|
219
- | task-01 | 添加用户创建接口 | W1 | P0 | — | ... |
220
- | task-02 | 添加角色创建接口 | W1 | P0 | — | ... |
221
- | task-03 | 用户创建接口联调 | W2 | P0 | task-01,02 | ... |
227
+ | 编号 | 任务 | Wave | 优先级 | 依赖 | 覆盖 FR/D | 说明 |
228
+ |---|---|---|---|---|---|---|
229
+ | task-01 | 添加用户创建接口 | W1 | P0 | — | FR-01, D-001@v1 | ... |
230
+ | task-02 | 添加角色创建接口 | W1 | P0 | — | FR-02 | ... |
231
+ | task-03 | 用户创建接口联调 | W2 | P0 | task-01,02 | FR-03 | ... |
222
232
 
223
233
  ## 关键路径
224
234
  task-01 → task-03(最长路径,决定最短交付周期)
@@ -226,6 +236,11 @@ task-01 → task-03(最长路径,决定最短交付周期)
226
236
  ## 全局验收标准
227
237
  - [ ] 所有单元测试通过
228
238
  - [ ] (brownfield)未配置新功能时行为不变
239
+
240
+ ## 覆盖矩阵(如存在 decisions.md)
241
+ | ID | 覆盖任务 | 验收证据 |
242
+ |---|---|---|
243
+ | D-001@v1 | task-01 | AC-01 |
229
244
  \`\`\`
230
245
 
231
246
  full 计划的约束:
@@ -234,6 +249,8 @@ full 计划的约束:
234
249
  - Mermaid 依赖关系图**仅当依赖关系非平凡时生成**(线性依赖或全并行时不生成)
235
250
  - **Wave 下的 checkbox 行必须保留**(execute 阶段解析依赖 \`- [ ] task-XX:\` 格式)
236
251
  - plan.md 包含 Wave 分组 + 任务总表 + 关键路径 + 全局验收标准,**不放实现细节**
252
+ - 如果存在 decisions.md,plan.md 必须包含当前版本 D-xxx@vN/FR-xxx 覆盖矩阵
253
+ - 如果存在 P0/P1 unresolved blocker,不生成 plan.md,输出阻塞清单
237
254
  - 实现细节写到后续的 tasks/task-NN.md 中
238
255
  - 每个任务编号格式:task-01、task-02 ...
239
256
  - 任务总表的优先级:P0(必须)/ P1(重要)/ P2(可选)
@@ -290,6 +307,8 @@ plan_level + 计划内容(none 级别输出建议操作)`,
290
307
  - [ ] 任务列表清晰且无实现细节
291
308
  - [ ] 任务使用 checkbox 格式(\`- [ ] task-XX:\`),不是纯编号列表
292
309
  - [ ] 验收标准具体可验证(非笼统表述)
310
+ - [ ] 如果存在 decisions.md,所有当前版本 D-xxx@vN 在 plan.md 中可追踪
311
+ - [ ] 不存在 P0/P1 unresolved blocker
293
312
  - [ ] 没有 Mermaid 图、估时、风险分析
294
313
  - [ ] 没有函数签名、代码示例等实现细节
295
314
  - [ ] plan.md 与 design.md 的文件变更清单一致
@@ -302,6 +321,8 @@ plan_level + 计划内容(none 级别输出建议操作)`,
302
321
  - [ ] 有任务总表(含优先级、依赖列,**无估时列**)
303
322
  - [ ] 有关键路径标注
304
323
  - [ ] 有全局验收标准
324
+ - [ ] 如果存在 decisions.md,任务总表或覆盖矩阵覆盖全部当前版本 D-xxx@vN
325
+ - [ ] 不存在 P0/P1 unresolved blocker
305
326
  - [ ] (brownfield)全局验收包含兼容性条款
306
327
  - [ ] 没有实现细节(接口定义、代码示例等不应该在 plan.md 里)
307
328
  - [ ] plan.md 与 design.md 的文件变更清单一致
@@ -413,6 +434,8 @@ priority: P0
413
434
  estimated_hours: N
414
435
  depends_on: []
415
436
  blocks: []
437
+ requirement_ids: [FR-01]
438
+ decision_ids: [D-001@v1]
416
439
  allowed_paths:
417
440
  - 允许修改的路径范围
418
441
  ---
@@ -422,6 +445,10 @@ allowed_paths:
422
445
  ## 修改文件(必填)
423
446
  - 精确到文件路径,列出所有需要新增或修改的文件
424
447
 
448
+ ## 覆盖来源
449
+ - Requirements: FR-xx(来自 requirements.md)
450
+ - Decisions: D-xxx@vN(如存在 decisions.md)
451
+
425
452
  ## 实现要求
426
453
  1. 具体做什么,写清楚
427
454
  2. ...
@@ -463,6 +490,8 @@ allowed_paths:
463
490
  - \`estimated_hours\`: 预估工时,单个 task ≤ 8h
464
491
  - \`depends_on\`: 依赖的前序 task 编号列表
465
492
  - \`blocks\`: 被本 task 阻塞的后续 task 编号列表
493
+ - \`requirement_ids\`: 本任务覆盖的 FR-xxx 列表
494
+ - \`decision_ids\`: 本任务覆盖的当前版本 D-xxx@vN 列表;无 decisions.md 时可为空数组
466
495
  - \`allowed_paths\`: AI executor 可以修改的文件路径范围(安全边界)
467
496
 
468
497
  ### 关键规则
@@ -471,6 +500,7 @@ allowed_paths:
471
500
  - 接口定义写到"搬砖工照着做"的程度
472
501
  - 边界处理至少覆盖 5 条规则
473
502
  - 验收标准用表格格式,每条可点击验证,禁止"功能可演示"类笼统表述
503
+ - 如果存在 decisions.md,不允许丢失当前版本 D-xxx@vN;无法覆盖的 D-xxx@vN 必须写入非目标或剩余风险
474
504
  - 写完后保存到文件
475
505
 
476
506
  ### 操作
@@ -518,6 +548,8 @@ priority: P0/P1/P2
518
548
  estimated_hours: N
519
549
  depends_on: [task-XX]
520
550
  blocks: [task-XX]
551
+ requirement_ids: [FR-XX]
552
+ decision_ids: [D-XXX@vN]
521
553
  allowed_paths:
522
554
  - ...
523
555
  ---
@@ -527,6 +559,10 @@ allowed_paths:
527
559
  ## 修改文件(必填)
528
560
  - 精确到文件路径
529
561
 
562
+ ## 覆盖来源
563
+ - Requirements: FR-xx
564
+ - Decisions: D-xxx@vN(如存在)
565
+
530
566
  ## 实现要求
531
567
  1. 具体做什么
532
568
 
@@ -560,6 +596,7 @@ allowed_paths:
560
596
  - 接口定义写到"搬砖工照着做"的程度
561
597
  - 边界处理至少 5 条
562
598
  - 验收标准用表格,禁止笼统表述
599
+ - 如果存在 decisions.md,不允许丢失当前版本 D-xxx@vN;无法覆盖的 D-xxx@vN 必须写入非目标或剩余风险
563
600
  - 写完后用 Write tool 保存到文件
564
601
  \`\`\``
565
602
  }).join('\n\n')
@@ -591,8 +628,8 @@ ${subagentPrompts}
591
628
 
592
629
  ## 验收
593
630
  - 每个 task-N.md 文件存在且非空
594
- - 包含 YAML frontmatter(id、title、priority、depends_on、blocks、allowed_paths)
595
- - 包含所有必要章节:修改文件、实现要求、接口定义、边界处理(≥5条)、非目标、TDD 步骤、验收标准(表格格式)
631
+ - 包含 YAML frontmatter(id、title、priority、depends_on、blocks、requirement_ids、decision_ids、allowed_paths)
632
+ - 包含所有必要章节:修改文件、覆盖来源、实现要求、接口定义、边界处理(≥5条)、非目标、TDD 步骤、验收标准(表格格式)
596
633
  - 边界处理覆盖:null/空值、兼容性、异常处理、参数不可变、歧义场景`
597
634
 
598
635
  return {
@@ -1,3 +1,8 @@
1
+ /**
2
+ * @deprecated propose 阶段已移除入口(2026-06-14)。
3
+ * brainstorm 现在拥有四件套(design/proposal/requirements/tasks)的生成职责。
4
+ * 本文件保留以备未来需要恢复,但未注册到 stageRegistry。
5
+ */
1
6
  export const definition = {
2
7
  name: 'propose',
3
8
  title: '方案设计',
@@ -87,10 +92,29 @@ export const definition = {
87
92
  - **非功能需求**:兼容性、可回退、可测试、可扩展
88
93
 
89
94
  ### design.md 格式要求
90
- - **架构决策** + **文件变更清单表格** + **接口定义**
91
- - **兼容策略**(brownfield 必填):未配置新功能时行为不变、回退路径
92
- - **风险登记**表格:编号/风险/等级/应对策略
93
- - **自审**:需求覆盖、真实性、YAGNI、非目标
95
+
96
+ **必须包含的章节:**
97
+ 1. **背景**:为什么做、解决什么问题
98
+ 2. **设计目标**:要达成什么
99
+ 3. **非目标**:明确不做的事(防止 scope creep)
100
+ 4. **总体方案**:技术方案(分 Phase/Wave)
101
+ 5. **文件变更清单**(必填):
102
+
103
+ | 操作 | 文件路径 | 说明 |
104
+ |---|---|---|
105
+ | 新增 | src/xxx/NewFile.java | ... |
106
+ | 修改 | src/xxx/ExistingFile.java | 新增 xx 方法 |
107
+
108
+ 6. **接口定义**:方法签名、数据结构(代码类任务必填)
109
+ 7. **数据模型**(如涉及):表结构/字段变更
110
+ 8. **兼容策略**(brownfield 必填):未配置新功能时行为不变、新旧逻辑的回退路径
111
+ 9. **风险登记**:
112
+
113
+ | 编号 | 风险 | 等级 | 应对策略 |
114
+ |---|---|---|---|
115
+ | R-01 | ... | P0/P1/P2 | ... |
116
+
117
+ 10. **自审**:需求覆盖、约束一致性、真实性、YAGNI、验收标准、非目标清晰、兼容策略、风险识别
94
118
 
95
119
  ### tasks.md 格式要求
96
120
  - 任务列表(只列名称,不展开步骤)
@@ -119,9 +143,11 @@ export const definition = {
119
143
  ### 操作
120
144
  检查以下各项:
121
145
  - [ ] proposal.md 有动机、关键问题、变更范围、不在范围内、成功标准
146
+ - [ ] design.md 有背景、设计目标、非目标
122
147
  - [ ] design.md 有文件变更清单表格
123
148
  - [ ] design.md 有兼容策略(brownfield 时)
124
149
  - [ ] design.md 有风险登记表格
150
+ - [ ] design.md 有自审
125
151
  - [ ] requirements.md 有角色表
126
152
  - [ ] requirements.md 有 FR 编号和 Given/When/Then 用户场景
127
153
  - [ ] tasks.md 每个 task 有文件路径
@@ -72,24 +72,27 @@ quicklog 已创建(必须放在输出的第一行确认)+ 任务理解 + 上
72
72
  prompt: `Git 暂存并更新任务记录。
73
73
 
74
74
  ### 操作
75
- 1. \`git add -A\` 暂存改动文件(不要 commit,由用户通过统一提交工具处理)
76
- 2. 更新 Step 1 创建的记录:
75
+ 1. 查看 \`git status --porcelain\`,确认只包含本次 quick 相关文件
76
+ 2. 使用 \`git add -- <file...>\` 暂存本次 quick 实际修改的文件(不要 commit,由用户通过统一提交工具处理)
77
+ - 禁止使用 \`git add -A\`
78
+ - 不要暂存 quick 开始前就已存在的无关改动
79
+ 3. 更新 Step 1 创建的记录:
77
80
  - 无 \`--change\`:找到对应 ql-ID 的条目,将「状态:进行中」改为「状态:已完成」,补充实际改动文件和结果摘要
78
81
  - 有 \`--change\`:勾选 tasks.md 中对应的 task checkbox
79
- 3. QUICKLOG 轮转:超过 500 行则重命名为 \`QUICKLOG-<USER>-YYYY-MM-DD.md\`(日期取最后一条记录的日期)。新文件从空开始,ql-ID 需扫描同目录所有 QUICKLOG 文件中当天最大序号 +1
80
- 4. 如果发现项目特有的坑,追加到 \`.sillyspec/knowledge/uncategorized.md\`
81
- 5. 任务比预期复杂 → 建议用完整流程
82
+ 4. QUICKLOG 轮转:超过 500 行则重命名为 \`QUICKLOG-<USER>-YYYY-MM-DD.md\`(日期取最后一条记录的日期)。新文件从空开始,ql-ID 需扫描同目录所有 QUICKLOG 文件中当天最大序号 +1
83
+ 5. 如果发现项目特有的坑,追加到 \`.sillyspec/knowledge/uncategorized.md\`
84
+ 6. 任务比预期复杂 → 建议用完整流程
82
85
 
83
86
  ### 模块文档同步
84
- 6. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(不存在则跳过以下步骤)
85
- 7. 对比本次修改的文件(\`git diff --name-only\`)与模块映射
86
- 8. 如果命中模块 → 直接同步模块文档:
87
+ 7. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(不存在则跳过以下步骤)
88
+ 8. 对比本次修改的文件(\`git diff --name-only HEAD\`)与模块映射
89
+ 9. 如果命中模块 → 直接同步模块文档:
87
90
  - 读取对应的 \`.sillyspec/docs/<project>/modules/<module>.md\`(如不存在则新建)
88
91
  - 根据本次改动内容更新模块文档(正文描述当前状态,底部变更索引追加本次 ql-ID)
89
92
  - 变更索引格式:\`- ql-YYYYMMDD-NNN-XXXX | <一句话描述>\`
90
93
  - 写入模块文档
91
- - 将更新的模块文件加入 \`git add\`
92
- 9. 未命中任何模块 → 跳过,不做额外操作
94
+ - 使用 \`git add -- <module-doc>\` 暂存更新的模块文件
95
+ 10. 未命中任何模块 → 跳过,不做额外操作
93
96
 
94
97
  ### 输出
95
98
  暂存确认 + 记录路径 + 模块文档同步结果(如有)`,
@@ -122,6 +122,18 @@ export const definition = {
122
122
  对每个项目分别执行(将 \`<project>\` 替换为实际项目名)
123
123
  4. 如果检查报告有失败项,按报告中的角色和文件重试失败的部分
124
124
 
125
+ ### 覆盖保护
126
+ - 生成每份 scan 文档时,frontmatter 必须包含:
127
+ \`\`\`yaml
128
+ ---
129
+ source_commit: <git-head-short>
130
+ updated_at: <now-iso-datetime>
131
+ generator: sillyspec-scan
132
+ ---
133
+ \`\`\`
134
+ - 覆盖已有 scan 文档前先读取旧 frontmatter;如果旧文档的 \`source_commit\` 与当前 HEAD 不一致,或旧文档 \`updated_at\` 晚于本次 scan 开始时间,不要覆盖。
135
+ - 如果用户明确传入 \`--force-rescan\`,允许覆盖,但仍需写入新的 \`source_commit\` 和 \`updated_at\`。
136
+
125
137
  ### 子代理上下文注入
126
138
  启动每个子代理前,将以下信息拼入子代理 prompt:
127
139
  - 项目名(直接用实际项目名)
@@ -53,20 +53,23 @@ export const definition = {
53
53
  prompt: `加载规范文件并确认。
54
54
 
55
55
  ### 操作
56
- 1. 读取 proposal.md、design.md、tasks.md、requirements.md
57
- 2. 加载项目信息:\`cat .sillyspec/projects/*.yaml 2>/dev/null\`
58
- 3. 加载本地配置:\`cat .sillyspec/local.yaml 2>/dev/null\`(构建命令、测试命令、lint 命令等)
59
- 4. 加载代码规范:\`cat .sillyspec/docs/<project>/scan/CONVENTIONS.md 2>/dev/null\`
60
- 5. 标注每个文件的存在/不存在状态
56
+ 1. 读取 proposal.md、design.md、tasks.md、requirements.md、plan.md
57
+ 2. 如果存在 decisions.md,必须读取并提取所有当前版本 D-xxx@vN 决策 ID
58
+ - 如果存在 P0/P1 unresolved/blocking 决策,验证结论不能为 PASS
59
+ - 如果发现 superseded 决策被下游引用,标记为 ⚠️ stale decision reference
60
+ 3. 加载项目信息:\`cat .sillyspec/projects/*.yaml 2>/dev/null\`
61
+ 4. 加载本地配置:\`cat .sillyspec/local.yaml 2>/dev/null\`(构建命令、测试命令、lint 命令等)
62
+ 5. 加载代码规范:\`cat .sillyspec/docs/<project>/scan/CONVENTIONS.md 2>/dev/null\`
63
+ 6. 标注每个文件的存在/不存在状态
61
64
 
62
65
  ### 模块文档加载
63
- 6. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(不存在则跳过以下步骤)
64
- 7. 根据 design.md 的文件变更清单匹配 _module-map.yaml 中的模块
65
- 8. 读取匹配到的 \`.sillyspec/docs/<project>/modules/<module>.md\`
66
- 9. **检查模块索引可信度**:如果相关模块的 needs_review 为 true,提示"该模块索引可能不可信,需要回看模块卡片或源码"
66
+ 7. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(不存在则跳过以下步骤)
67
+ 8. 根据 design.md 的文件变更清单匹配 _module-map.yaml 中的模块
68
+ 9. 读取匹配到的 \`.sillyspec/docs/<project>/modules/<module>.md\`
69
+ 10. **检查模块索引可信度**:如果相关模块的 needs_review 为 true,提示"该模块索引可能不可信,需要回看模块卡片或源码"
67
70
 
68
71
  ### 输出
69
- 文件加载确认清单(含模块文档 + 索引可信度)`,
72
+ 文件加载确认清单(含 decisions.md 当前版本/未决项状态、模块文档 + 索引可信度)`,
70
73
  outputHint: '文件确认清单',
71
74
  optional: false
72
75
  },
@@ -123,9 +126,17 @@ grep -rl "<关键词>" <源码目录>/ --include="*.java" --include="*.js" --inc
123
126
  2. 对每个 task,检查对应模块目录下是否存在测试文件(*test*、*spec*、*Test*、*Spec*)
124
127
  3. 没有测试文件的 task 标记为 ⚠️ 缺少测试
125
128
 
129
+ **探针 4:决策追踪覆盖探针(如存在 decisions.md)**
130
+ 1. 从 decisions.md 提取所有当前版本 D-xxx@vN
131
+ 2. 检查 requirements.md 是否引用每个 D-xxx@vN,并映射到 FR-xxx
132
+ 3. 检查 plan.md 或 tasks/task-NN.md 是否引用每个 FR-xxx/D-xxx@vN
133
+ 4. 检查本步骤收集的实现证据是否能回指到对应 D-xxx@vN/FR-xxx
134
+ 5. 任意 D-xxx@vN 无下游覆盖时标记为 ⚠️ 决策未闭环
135
+ 6. 任意 P0/P1 unresolved/blocking 决策标记为 FAIL blocker
136
+
126
137
  ### 探针结果处理
127
- - 将三个探针的结果汇总为「探针报告」
128
- - 如果探针发现问题(未实现标记、关键词缺失、测试缺失),在最终验证报告中明确标注
138
+ - 将四个探针的结果汇总为「探针报告」
139
+ - 如果探针发现问题(未实现标记、关键词缺失、测试缺失、决策未闭环),在最终验证报告中明确标注
129
140
  - 探针发现的问题不等同于验证失败,但必须在报告中列出
130
141
 
131
142
  ### 设计一致性检查
@@ -136,9 +147,10 @@ grep -rl "<关键词>" <源码目录>/ --include="*.java" --include="*.js" --inc
136
147
  4. API 设计是否符合
137
148
  5. **Reverse Sync 检查**:如果发现实现合理但 design.md 未覆盖,先更新 design.md 补充遗漏
138
149
  6. **模块文档一致性检查**:如果在"加载规范并锚定"步骤中加载了模块文档,检查实现是否符合模块文档描述的当前设计(特别关注接口签名、数据流、依赖关系)。不符合时标记 ⚠️(不阻断,模块文档可能未及时更新)
150
+ 7. **决策链路检查**:如果存在 decisions.md,输出 D-xxx@vN → FR-xxx → task-xx → evidence 的追踪矩阵;缺失项必须列为风险
139
151
 
140
152
  ### 输出
141
- 探针报告 + 设计一致性检查结果 + 模块文档一致性检查结果`,
153
+ 探针报告 + 设计一致性检查结果 + 模块文档一致性检查结果 + 决策追踪矩阵(如有)`,
142
154
  outputHint: '设计一致性报告',
143
155
  optional: false
144
156
  },
@@ -210,6 +222,12 @@ PASS / PASS WITH NOTES / FAIL
210
222
  - 未实现标记扫描:...
211
223
  - 关键词覆盖:...
212
224
  - 测试覆盖:...
225
+ - 决策追踪覆盖:...
226
+
227
+ ## 决策追踪矩阵(如存在 decisions.md)
228
+ | 决策 ID | FR | Task | Evidence | 状态 |
229
+ |---|---|---|---|---|
230
+ | D-001@v1 | FR-01 | task-01 | test/file/path | PASS/PARTIAL/MISSING |
213
231
 
214
232
  ## 测试结果
215
233
  (测试套件执行结果)
@@ -13,6 +13,7 @@ import { join, dirname } from 'path'
13
13
  import { existsSync, mkdirSync, rmSync, readFileSync, readdirSync, writeFileSync } from 'fs'
14
14
  import { fileURLToPath } from 'url'
15
15
  import { randomUUID } from 'crypto'
16
+ import { tmpdir } from 'os'
16
17
 
17
18
  const __dirname = dirname(fileURLToPath(import.meta.url))
18
19
  const passed = []
@@ -33,11 +34,19 @@ function cleanup(dir) {
33
34
  try { rmSync(dir, { recursive: true, force: true }) } catch {}
34
35
  }
35
36
 
37
+ function hasPathSegments(value, segments) {
38
+ const parts = value.split(/[\\/]+/)
39
+ for (let i = 0; i <= parts.length - segments.length; i++) {
40
+ if (segments.every((segment, offset) => parts[i + offset] === segment)) return true
41
+ }
42
+ return false
43
+ }
44
+
36
45
  // ── 测试 1:saveWorkflowRun 平台模式路径正确 ──
37
46
  console.log('\n=== Test 1: saveWorkflowRun 平台模式写入路径 ===')
38
47
  {
39
48
  const { saveWorkflowRun } = await import('../src/workflow.js')
40
- const tmpRoot = `/tmp/test-artifacts-${randomUUID().slice(0, 8)}`
49
+ const tmpRoot = join(tmpdir(), `test-artifacts-${randomUUID().slice(0, 8)}`)
41
50
  const runtimeRoot = join(tmpRoot, 'runtime')
42
51
  const scanRunId = 'scan-20260614-test-001'
43
52
 
@@ -63,8 +72,8 @@ console.log('\n=== Test 1: saveWorkflowRun 平台模式写入路径 ===')
63
72
  assert('workflow-runs 目录存在', existsSync(expectedDir))
64
73
  assert('workflow-runs 文件存在', existsSync(saved), `路径: ${saved}`)
65
74
  assert('路径在 runtime-root 下', saved.startsWith(runtimeRoot), `路径: ${saved}`)
66
- assert('路径包含 scan-runs', saved.includes('scan-runs'), `路径: ${saved}`)
67
- assert('路径包含 scanRunId', saved.includes(scanRunId), `路径: ${saved}`)
75
+ assert('路径包含 scan-runs', hasPathSegments(saved, ['scan-runs', scanRunId, 'workflow-runs']), `路径: ${saved}`)
76
+ assert('路径包含 scanRunId', hasPathSegments(saved, [scanRunId]), `路径: ${saved}`)
68
77
 
69
78
  // 验证 JSON 内容
70
79
  const content = JSON.parse(readFileSync(saved, 'utf8'))
@@ -81,7 +90,7 @@ console.log('\n=== Test 1: saveWorkflowRun 平台模式写入路径 ===')
81
90
  console.log('\n=== Test 2: saveWorkflowRun 本地模式写入路径 ===')
82
91
  {
83
92
  const { saveWorkflowRun } = await import('../src/workflow.js')
84
- const tmpCwd = `/tmp/test-artifacts-local-${randomUUID().slice(0, 8)}`
93
+ const tmpCwd = join(tmpdir(), `test-artifacts-local-${randomUUID().slice(0, 8)}`)
85
94
  const sillyspecDir = join(tmpCwd, '.sillyspec', '.runtime', 'workflow-runs')
86
95
 
87
96
  const result = {
@@ -102,7 +111,7 @@ console.log('\n=== Test 2: saveWorkflowRun 本地模式写入路径 ===')
102
111
  })
103
112
 
104
113
  assert('本地模式文件存在', existsSync(saved))
105
- assert('本地路径在 .sillyspec/.runtime 下', saved.includes('.sillyspec/.runtime/workflow-runs'), `路径: ${saved}`)
114
+ assert('本地路径在 .sillyspec/.runtime 下', hasPathSegments(saved, ['.sillyspec', '.runtime', 'workflow-runs']), `路径: ${saved}`)
106
115
 
107
116
  const content = JSON.parse(readFileSync(saved, 'utf8'))
108
117
  assert('本地 JSON status = fail', content.status === 'fail')
@@ -13,6 +13,7 @@
13
13
  import { join, basename } from 'path'
14
14
  import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs'
15
15
  import { randomUUID } from 'crypto'
16
+ import { tmpdir } from 'os'
16
17
  import { SCAN_STATUS, CHECK_SEVERITY } from '../src/constants.js'
17
18
 
18
19
  const passed = []
@@ -34,8 +35,8 @@ function cleanup(dir) {
34
35
  }
35
36
 
36
37
  function setup(name) {
37
- const base = `/tmp/failure-test-${name}-${randomUUID().slice(0, 8)}`
38
- const spec = `/tmp/failure-test-spec-${name}-${randomUUID().slice(0, 8)}`
38
+ const base = join(tmpdir(), `failure-test-${name}-${randomUUID().slice(0, 8)}`)
39
+ const spec = join(tmpdir(), `failure-test-spec-${name}-${randomUUID().slice(0, 8)}`)
39
40
  mkdirSync(base, { recursive: true })
40
41
  mkdirSync(spec, { recursive: true })
41
42
  writeFileSync(join(base, 'package.json'), '{}')
@@ -16,6 +16,7 @@ import { execSync } from 'child_process'
16
16
  import { fileURLToPath } from 'url'
17
17
  import { dirname } from 'path'
18
18
  import { randomUUID } from 'crypto'
19
+ import { tmpdir } from 'os'
19
20
 
20
21
  const __dirname = dirname(fileURLToPath(import.meta.url))
21
22
  const binCLI = join(__dirname, '..', 'src', 'index.js')
@@ -48,9 +49,9 @@ function run(cmd, opts = {}) {
48
49
  // ── 测试 1:pointer 文件创建和内容 ──
49
50
  console.log('\n=== Test 1: pointer 文件创建 ===')
50
51
  {
51
- const tmpCwd = `/tmp/recovery-test-${randomUUID().slice(0, 8)}`
52
- const tmpSpec = `/tmp/recovery-test-spec-${randomUUID().slice(0, 8)}`
53
- const tmpRuntime = `/tmp/recovery-test-rt-${randomUUID().slice(0, 8)}`
52
+ const tmpCwd = join(tmpdir(), `recovery-test-${randomUUID().slice(0, 8)}`)
53
+ const tmpSpec = join(tmpdir(), `recovery-test-spec-${randomUUID().slice(0, 8)}`)
54
+ const tmpRuntime = join(tmpdir(), `recovery-test-rt-${randomUUID().slice(0, 8)}`)
54
55
 
55
56
  try {
56
57
  mkdirSync(tmpCwd, { recursive: true })
@@ -82,9 +83,9 @@ console.log('\n=== Test 1: pointer 文件创建 ===')
82
83
  // ── 测试 2:--done 不带参数能恢复 ──
83
84
  console.log('\n=== Test 2: --done 恢复平台参数 ===')
84
85
  {
85
- const tmpCwd = `/tmp/recovery-test2-${randomUUID().slice(0, 8)}`
86
- const tmpSpec = `/tmp/recovery-test2-spec-${randomUUID().slice(0, 8)}`
87
- const tmpRuntime = `/tmp/recovery-test2-rt-${randomUUID().slice(0, 8)}`
86
+ const tmpCwd = join(tmpdir(), `recovery-test2-${randomUUID().slice(0, 8)}`)
87
+ const tmpSpec = join(tmpdir(), `recovery-test2-spec-${randomUUID().slice(0, 8)}`)
88
+ const tmpRuntime = join(tmpdir(), `recovery-test2-rt-${randomUUID().slice(0, 8)}`)
88
89
  const scanRunId = `scan-${Date.now()}`
89
90
 
90
91
  try {
@@ -125,8 +126,8 @@ console.log('\n=== Test 3: manifest 路径字段 ===')
125
126
  // ── 测试 4:异常 pointer 检测 ──
126
127
  console.log('\n=== Test 4: 异常 pointer 残留检测 ===')
127
128
  {
128
- const tmpCwd = `/tmp/recovery-test4-${randomUUID().slice(0, 8)}`
129
- const tmpSpec = `/tmp/recovery-test4-spec-${randomUUID().slice(0, 8)}`
129
+ const tmpCwd = join(tmpdir(), `recovery-test4-${randomUUID().slice(0, 8)}`)
130
+ const tmpSpec = join(tmpdir(), `recovery-test4-spec-${randomUUID().slice(0, 8)}`)
130
131
 
131
132
  try {
132
133
  mkdirSync(tmpCwd, { recursive: true })
@@ -147,7 +148,7 @@ console.log('\n=== Test 4: 异常 pointer 残留检测 ===')
147
148
  // 模拟有效 pointer(手动修复)
148
149
  writeFileSync(pointerPath, JSON.stringify({
149
150
  specRoot: tmpSpec,
150
- runtimeRoot: '/tmp/fake-rt',
151
+ runtimeRoot: join(tmpdir(), 'fake-rt'),
151
152
  workspaceId: 'test',
152
153
  scanRunId: 'scan-test',
153
154
  savedAt: new Date().toISOString(),
@@ -6,6 +6,7 @@ import { join, resolve, dirname, basename } from 'path'
6
6
  import { existsSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'fs'
7
7
  import { fileURLToPath, pathToFileURL } from 'url'
8
8
  import { execSync } from 'child_process'
9
+ import { tmpdir } from 'os'
9
10
 
10
11
  const __filename = fileURLToPath(import.meta.url)
11
12
  const __dirname = dirname(__filename)
@@ -21,16 +22,23 @@ function assert(cond, msg) {
21
22
 
22
23
  const P = 'recover'
23
24
  function setup(name) {
24
- const d = join('/tmp', `${P}-${name}`)
25
+ const d = join(tmpdir(), `${P}-${name}`)
25
26
  mkdirSync(d, { recursive: true })
26
27
  return d
27
28
  }
28
29
  function spec(name) {
29
- const d = join('/tmp', `${P}-${name}-spec`)
30
+ const d = join(tmpdir(), `${P}-${name}-spec`)
30
31
  mkdirSync(d, { recursive: true })
31
32
  return d
32
33
  }
33
34
  function clean(...dirs) { for (const d of dirs) try { rmSync(d, { recursive: true, force: true }) } catch {} }
35
+ function hasPathSegments(value, segments) {
36
+ const parts = value.split(/[\\/]+/)
37
+ for (let i = 0; i <= parts.length - segments.length; i++) {
38
+ if (segments.every((segment, offset) => parts[i + offset] === segment)) return true
39
+ }
40
+ return false
41
+ }
34
42
 
35
43
  function run(cmd) {
36
44
  return execSync(cmd, { encoding: 'utf8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'] })
@@ -127,8 +135,8 @@ console.log('\n=== Test 5: specDir 缺文档 → 校验失败,路径不含 .si
127
135
  assert(!result.ok, `specDir 缺文档: ok=${result.ok}`)
128
136
  assert(result.errors.length > 0, `有 errors`)
129
137
  const errMsg = result.errors[0]
130
- assert(!errMsg.includes('.sillyspec/docs'), `路径不含 .sillyspec: ${errMsg}`)
131
- assert(errMsg.includes('/docs/'), `路径含 /docs/: ${errMsg}`)
138
+ assert(!hasPathSegments(errMsg, ['.sillyspec', 'docs']), `路径不含 .sillyspec: ${errMsg}`)
139
+ assert(hasPathSegments(errMsg, ['docs', proj, 'scan']), `路径含 docs/${proj}/scan: ${errMsg}`)
132
140
  clean(cwd, sd)
133
141
  }
134
142
 
@@ -149,7 +157,7 @@ console.log('\n=== Test 7: 非平台模式缺文档 → 路径含 .sillyspec ===
149
157
  const result = runValidators('scan', cwd, 'default', { projectName: proj })
150
158
  assert(!result.ok, `非平台缺文档: ok=${result.ok}`)
151
159
  const errMsg = result.errors[0]
152
- assert(errMsg.includes('.sillyspec/docs'), `路径含 .sillyspec/docs/: ${errMsg}`)
160
+ assert(hasPathSegments(errMsg, ['.sillyspec', 'docs']), `路径含 .sillyspec/docs: ${errMsg}`)
153
161
  clean(cwd)
154
162
  }
155
163
 
@@ -142,11 +142,14 @@ function assert(label, condition, detail) {
142
142
  {
143
143
  const { definition } = await import('../src/stages/quick.js')
144
144
  const step1Prompt = definition.steps[0].prompt
145
+ const step3Prompt = definition.steps[2].prompt
145
146
 
146
147
  assert('quick step 1 包含 ⛔ 标记', step1Prompt.includes('⛔'))
147
148
  assert('quick step 1 包含「不能跳过」', step1Prompt.includes('不能跳过'))
148
149
  assert('quick step 1 包含 quicklog 未创建 warning', step1Prompt.includes('quicklog 未创建'))
149
150
  assert('quick step 1 输出要求 quicklog 第一行', step1Prompt.includes('第一行确认'))
151
+ assert('quick step 3 禁止 git add -A', step3Prompt.includes('禁止使用 `git add -A`'))
152
+ assert('quick step 3 使用 scoped git add', step3Prompt.includes('git add -- <file...>'))
150
153
 
151
154
  // run.js 审计包含 quicklog 检查
152
155
  const runSrc = await readFile(join(__dirname, '..', 'src', 'run.js'), 'utf8')