sillyspec 3.25.4 → 3.25.6

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 (41) hide show
  1. package/.claude/skills/sillyspec-brainstorm/SKILL.md +27 -1
  2. package/.claude/skills/sillyspec-execute/SKILL.md +56 -0
  3. package/.claude/skills/sillyspec-plan/SKILL.md +26 -0
  4. package/.claude/skills/sillyspec-propose/SKILL.md +26 -0
  5. package/.claude/skills/sillyspec-scan/SKILL.md +12 -7
  6. package/.claude/skills/sillyspec-verify/SKILL.md +2 -2
  7. package/package.json +1 -1
  8. package/src/change-list.js +114 -12
  9. package/src/change-risk-profile.js +36 -1
  10. package/src/constants.js +11 -0
  11. package/src/index.js +4 -1
  12. package/src/machine-interface.js +5 -1
  13. package/src/progress/stage-machine.js +17 -14
  14. package/src/run/command.js +58 -3
  15. package/src/run/complete-handlers.js +9 -3
  16. package/src/run/complete.js +130 -33
  17. package/src/run/gates.js +37 -0
  18. package/src/run/prompt.js +20 -0
  19. package/src/run/quick-audit.js +8 -1
  20. package/src/run/scan-profile.js +29 -10
  21. package/src/run/shared.js +161 -4
  22. package/src/run/stage.js +4 -0
  23. package/src/scan-postcheck.js +25 -11
  24. package/src/stage-contract.js +39 -8
  25. package/src/stage-review.js +16 -5
  26. package/src/stages/archive.js +13 -9
  27. package/src/stages/brainstorm-auto.js +3 -2
  28. package/src/stages/brainstorm.js +13 -16
  29. package/src/stages/execute.js +4 -3
  30. package/src/stages/plan-postcheck.js +1 -1
  31. package/src/stages/plan.js +13 -0
  32. package/src/stages/propose.js +3 -2
  33. package/src/stages/quick.js +31 -31
  34. package/src/stages/scan.js +2 -1
  35. package/src/stages/status.js +2 -2
  36. package/src/stages/verify.js +19 -8
  37. package/src/task-review.js +308 -3
  38. package/src/verify-postcheck.js +353 -38
  39. package/src/worktree-apply.js +134 -67
  40. package/src/worktree.js +106 -32
  41. package/templates/prompts/verify-probes.md +13 -2
@@ -8,8 +8,8 @@ export const definition = {
8
8
  name: '理解任务',
9
9
  prompt: `解析任务参数,加载项目上下文。
10
10
 
11
- ### 📌 本 quick 会话 sessionId: <quick-session-id>
12
- - CLI 是短进程,run 与 done 是独立进程,sessionId 靠 --change 跨进程传递
11
+ ### 📌 本 quick 会话 sessionId: <quick-session-id>
12
+ - CLI 是短进程,run 与 done 是独立进程,sessionId 靠 --change 跨进程传递
13
13
  - **完成每个 step 必须带 --change <quick-session-id>**(命令由 CLI 在下方注入);多会话并发时不带会命中他者会话状态(不带时 fallback 读 current-quick-run-id,单会话兼容;多会话不可靠)
14
14
 
15
15
  ### 操作
@@ -26,15 +26,15 @@ export const definition = {
26
26
  9. 根据任务描述初步判断可能涉及的模块
27
27
  10. 读取匹配到的 \`{SPEC_ROOT}/docs/<project>/modules/<module>.md\`
28
28
 
29
- ### QUICKLOG 记录(CLI 已接管,无需你手动创建)
30
- 本 quick 会话的 ql-ID 由 CLI 在启动时分配,已注入为 \`<quicklog-id>\`。CLI 同时已自动:
31
- - 在 \`{SPEC_ROOT}/quicklog/QUICKLOG-<git-user>.md\` 写入「进行中」条目(含关联变更与预估文件)
32
- - 对每个关联变更 \`<c>\`:在 \`{SPEC_ROOT}/changes/<c>/tasks.md\` 追加未勾选 task \`- [ ] <quicklog-id> <任务描述>\`
33
-
34
- **你不要创建或修改任何 QUICKLOG 的条目骨架 / tasks.md 记录**——ql-ID 分配、「进行中」条目、状态翻转、task 勾选、轮转全由 CLI 接管,你无需手写。<quicklog-id> 可用于 design.md / plan.md / archive / 模块变更索引引用。
35
-
36
- > ⚠️ 例外:step 3 --done 之后,CLI 落盘的 QUICKLOG 只是简版骨架,**你必须手动精修正文成丰富格式**(标题语义化 + 文件多行带括注 + 四段充实),详见 step 3「QUICKLOG 正文精修」。这是 quick 交付质量的一部分,不可跳过。
37
-
29
+ ### QUICKLOG 记录(CLI 已接管,无需你手动创建)
30
+ 本 quick 会话的 ql-ID 由 CLI 在启动时分配,已注入为 \`<quicklog-id>\`。CLI 同时已自动:
31
+ - 在 \`{SPEC_ROOT}/quicklog/QUICKLOG-<git-user>.md\` 写入「进行中」条目(含关联变更与预估文件)
32
+ - 对每个关联变更 \`<c>\`:在 \`{SPEC_ROOT}/changes/<c>/tasks.md\` 追加未勾选 task \`- [ ] <quicklog-id> <任务描述>\`
33
+
34
+ **你不要创建或修改任何 QUICKLOG 的条目骨架 / tasks.md 记录**——ql-ID 分配、「进行中」条目、状态翻转、task 勾选、轮转全由 CLI 接管,你无需手写。<quicklog-id> 可用于 design.md / plan.md / archive / 模块变更索引引用。
35
+
36
+ > ⚠️ 例外:step 3 --done 之后,CLI 落盘的 QUICKLOG 只是简版骨架,**你必须手动精修正文成丰富格式**(标题语义化 + 文件多行带括注 + 四段充实),详见 step 3「QUICKLOG 正文精修」。这是 quick 交付质量的一部分,不可跳过。
37
+
38
38
  ### 输出
39
39
  任务理解 + 上下文摘要`,
40
40
  outputHint: '任务理解',
@@ -42,16 +42,16 @@ export const definition = {
42
42
  },
43
43
  {
44
44
  name: '实现并验证',
45
- prompt: `直接在主工作区实现任务。
46
-
47
- ### 边界声明(quick 不校验 design.md)
48
- 关联变更的 design.md 仅供理解意图,**不作为验收基准**——quick 不 enforce design 一致性。需要 design 一致性保证 → 走完整流程(plan + verify)。发现 design 本身有错时,可 Reverse Sync 回写(见下方铁律)。
49
-
45
+ prompt: `直接在主工作区实现任务。
46
+
47
+ ### 边界声明(quick 不校验 design.md)
48
+ 关联变更的 design.md 仅供理解意图,**不作为验收基准**——quick 不 enforce design 一致性。需要 design 一致性保证 → 走完整流程(plan + verify)。发现 design 本身有错时,可 Reverse Sync 回写(见下方铁律)。
49
+
50
50
  ### 操作
51
51
  1. 先读后写:调用已有方法前 \`cat\` 源文件确认签名,\`grep\` 确认方法存在
52
52
  2. 写代码完成任务
53
53
  3. 如涉及逻辑变更,建议写单元测试验证(不强制,纯配置/文档/小改动可跳过)
54
- 4. **不要编译!** 除非用户明确要求或改动量很大
54
+ 4. **不要做无谓的全量编译/构建。** 但——跑针对性的单元测试始终允许且鼓励(见 step3);若本任务就是修测试/构建/基础设施本身,跑测试/构建即验收,应当跑,不算"编译"
55
55
 
56
56
  ### 输出
57
57
  实现摘要 + 修改文件列表
@@ -67,7 +67,7 @@ export const definition = {
67
67
  name: '暂存和更新记录',
68
68
  prompt: `Git 暂存并更新任务记录。
69
69
 
70
- ### 📌 收尾确认
70
+ ### 📌 收尾确认
71
71
  - 本步骤是最后一步,完成后 quick 会话即结束(--change 传递见首步 sessionId 说明)
72
72
 
73
73
  ### ⚠️ 结果摘要模板(必填,CLI 会校验结构)
@@ -102,19 +102,19 @@ export const definition = {
102
102
  - 使用 \`git add -- <module-doc>\` 暂存更新的模块文件
103
103
  9. 未命中任何模块 → 跳过,不做额外操作
104
104
 
105
- ### 输出
106
- 暂存确认 + 记录路径 + 模块文档同步结果(如有)
107
-
108
- ### 📝 QUICKLOG 正文精修(--done 之后必做,不可跳过)
109
- sillyspec run quick --done 成功后,CLI 已在 {SPEC_ROOT}/quicklog/QUICKLOG-<git-user>.md 落盘本次 <quicklog-id> 条目的简版骨架:状态翻「已完成」、「文件:」是单行逗号路径、正文是 --output 的四字段。骨架是机械产物不达标,你必须立即编辑该条目手动精修成丰富格式,三项必改:
110
-
111
- 1. 标题:把 ## <quicklog-id> | <时间> | <标题> 中的 <标题> 改为真实需求摘要(如「登录加 IP 限流(5 次/分)+ 失败 3 次滑块验证」)。禁止保留 CLI 兜底的 (quick 任务) 或一句话笼统描述。
112
- 2. 文件:把 CLI 写的单行「文件:a.js, b.js」改写为多行 bullet,每个文件带关键改动括注;禁止「(见实际改动)」或无括注纯路径单行。示例:
113
- 文件:
114
- - backend/app/modules/auth/router.py(登录端点串 check_rate_limit→assert_captcha_if_needed)
115
- - backend/app/modules/auth/captcha_service.py(新建:限流 INCR + 失败计数 + Pillow 滑块生成/校验)
116
- 3. 正文:核对 需求: 根因: 方案: 结果: 四段逐项充实(--output 已写、CLI 原样插入,按需扩写),不允许只留一段「结果:」。
117
-
105
+ ### 输出
106
+ 暂存确认 + 记录路径 + 模块文档同步结果(如有)
107
+
108
+ ### 📝 QUICKLOG 正文精修(--done 之后必做,不可跳过)
109
+ sillyspec run quick --done 成功后,CLI 已在 {SPEC_ROOT}/quicklog/QUICKLOG-<git-user>.md 落盘本次 <quicklog-id> 条目的简版骨架:状态翻「已完成」、「文件:」是单行逗号路径、正文是 --output 的四字段。骨架是机械产物不达标,你必须立即编辑该条目手动精修成丰富格式,三项必改:
110
+
111
+ 1. 标题:把 ## <quicklog-id> | <时间> | <标题> 中的 <标题> 改为真实需求摘要(如「登录加 IP 限流(5 次/分)+ 失败 3 次滑块验证」)。禁止保留 CLI 兜底的 (quick 任务) 或一句话笼统描述。
112
+ 2. 文件:把 CLI 写的单行「文件:a.js, b.js」改写为多行 bullet,每个文件带关键改动括注;禁止「(见实际改动)」或无括注纯路径单行。示例:
113
+ 文件:
114
+ - backend/app/modules/auth/router.py(登录端点串 check_rate_limit→assert_captcha_if_needed)
115
+ - backend/app/modules/auth/captcha_service.py(新建:限流 INCR + 失败计数 + Pillow 滑块生成/校验)
116
+ 3. 正文:核对 需求: 根因: 方案: 结果: 四段逐项充实(--output 已写、CLI 原样插入,按需扩写),不允许只留一段「结果:」。
117
+
118
118
  一条 quick = 一条独立 ql 条目;精修只动本次 <quicklog-id> 条目,不追加到旧条目。QUICKLOG 在 .sillyspec/(gitignore),精修不影响 --done 已通过的边界审计。`,
119
119
  outputHint: '暂存和记录确认',
120
120
  optional: false
@@ -139,7 +139,8 @@ export const definition = {
139
139
  对每个项目分别执行(将 \`<project>\` 替换为实际项目名)
140
140
  4. 如果检查报告有失败项,按报告中的角色和文件重试失败的部分
141
141
 
142
- ### 覆盖保护
142
+ ### 覆盖保护
143
+ - **scan_depth: quick 的浅层文档允许覆盖升级**:读取旧 frontmatter 时,若旧文档含 scan_depth: quick(由 --quick 快速接入生成的浅层版本),即使其 source_commit 与当前 HEAD 一致、updated_at 未变,也允许覆盖重写——深度扫描的目的就是把它升级为完整文档。
143
144
  - 生成每份 scan 文档时,frontmatter 必须包含:
144
145
  \`\`\`yaml
145
146
  ---
@@ -1,7 +1,7 @@
1
1
  export const definition = {
2
2
  name: 'status',
3
- title: '状态查看',
4
- description: '查看项目进度和状态',
3
+ title: '项目快照',
4
+ description: '项目级只读快照(非流程推进)。查「下一步该做什么/当前阶段进度」请用 sillyspec progress show,勿混淆。',
5
5
  auxiliary: true,
6
6
  steps: [
7
7
  {
@@ -36,16 +36,21 @@ export const definition = {
36
36
 
37
37
  steps: [
38
38
  {
39
- name: '状态检查',
40
- prompt: `检查当前状态,确认可以执行 verify。
39
+ name: '进度确认',
40
+ migratedFrom: ['状态检查'],
41
+ prompt: `> 💡 先说清楚:\`sillyspec run verify\`(不带 --done)**只下发执行指令,不会替你跑测试/构建**——真正的测试由你(Agent)在「运行测试和质量扫描」那步自己执行;CLI 只在最后 --done 时亲自跑 local.yaml 的 test 对账。别以为敲了 run verify 就自动验证了。
42
+
43
+ 检查当前进度,确认可以执行 verify。
44
+
45
+ > ⚠️ 本步用 \`sillyspec progress show\` 查**流程进度**(推进工作流用),不要用 \`sillyspec status\`(那是**项目级快照**,只读、不推进流程,是另一条命令)。
41
46
 
42
47
  ### 操作
43
48
  1. 运行 \`sillyspec progress show\`
44
49
  2. 确认 currentStage 为 "verify"
45
50
 
46
51
  ### 输出
47
- 当前状态摘要`,
48
- outputHint: '状态摘要',
52
+ 当前进度摘要`,
53
+ outputHint: '进度摘要',
49
54
  optional: false
50
55
  },
51
56
  {
@@ -113,8 +118,8 @@ export const definition = {
113
118
  {{include: verify-probes}}
114
119
 
115
120
  ### 探针结果处理
116
- - 将四个探针的结果汇总为「探针报告」
117
- - 如果探针发现问题(未实现标记、关键词缺失、测试缺失、决策未闭环),在最终验证报告中明确标注
121
+ - 将六个探针的结果汇总为「探针报告」
122
+ - 如果探针发现问题(未实现标记、关键词缺失、测试缺失、决策未闭环、API 契约缺口、代码删除对账),在最终验证报告中明确标注
118
123
  - 探针发现的问题不等同于验证失败,但必须在报告中列出
119
124
 
120
125
  ### 设计一致性检查
@@ -182,7 +187,11 @@ export const definition = {
182
187
  ### 操作
183
188
  1. 汇总以上所有检查结果
184
189
  2. **变更风险等级(change_risk_profile)由 CLI 自动判定与门控**:你无需自己扫描关键词。本步骤 --done 时,CLI 会用 detectChangeRisk 扫描 design.md / plan.md 自动判定等级(doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical)并强制门控:integration-critical / deployment-critical 变更若结论为 PASS / PASS WITH NOTES 但缺少真实集成证据,CLI 直接阻断 verify 完成——谎报结论无效。
190
+ - **判级是机械字面匹配、不认否定语境**:design 里写「本次不改动 daemon/session」仍命中关键词被误判高危级。
191
+ - **误判时的诚实出路(豁免级)**:在 design.md 顶部 frontmatter 加一行 \`risk_level: <真实等级>\`(doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical),CLI 会以声明为准覆盖关键词判级。声明后若是 unit-sufficient 等豁免级,PASS WITH NOTES 不再被强制拦;但结论为 PASS 仍需对应证据。
192
+ - **留痕要求(防逃逸)**:用了显式声明,必须在本报告「变更风险等级」section 写明「risk_level 由 design frontmatter 显式声明 = <等级>(覆盖关键词判级)」+ 一句话理由,让豁免可审计。
185
193
  你只需:在 verify-result.md 的「变更风险等级」section 如实记录变更性质;若变更涉及 daemon/backend 跨进程、session/lease/lifecycle 状态机、或部署启动路径,在「Runtime Evidence」section 提供真实集成证据(启动命令、daemon↔backend 调用与日志关键片段、终态断言)。
194
+ - **集成证据是自报告、CLI 不独立运行时核验**:「Runtime Evidence」由你如实填写,CLI 只校验其**字面存在**(是否含关键词),**不会替你启动 daemon、打真实请求或跑迁移**——它是否名副其实取决于你是否实跑过。务必实跑后据实填写,不得凭堆关键词通过门控。(测试套件对账另算:commands.test 由 CLI 真实执行,那块谎报无效。)
186
195
 
187
196
  3. 生成 verify-result.md 文件,保存到 \`.sillyspec/changes/<change-name>/verify-result.md\`
188
197
  4. 给出结论:PASS / PASS WITH NOTES / FAIL(受风险门控约束)
@@ -205,6 +214,8 @@ PASS / PASS WITH NOTES / FAIL
205
214
  - 关键词覆盖:...
206
215
  - 测试覆盖:...
207
216
  - 决策追踪覆盖:...
217
+ - API 契约对账:...
218
+ - 代码删除对账:声明修改/新增却被整文件删除 = 高风险;清单未列出的删除 = 未声明(CLI 会用 git diff --name-status HEAD 独立复核)
208
219
 
209
220
  ## 决策追踪矩阵(如存在 decisions.md)
210
221
  | 决策 ID | FR | Task | Evidence | 状态 |
@@ -218,9 +229,9 @@ PASS / PASS WITH NOTES / FAIL
218
229
  (TODO/FIXME/HACK 统计)
219
230
 
220
231
  ## 变更风险等级
221
- (自动检测的 change_risk_profile: doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical)
232
+ (自动检测的 change_risk_profile: doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical;若 design.md frontmatter 有 risk_level 显式声明,以声明为准并在此注明「显式声明 = <等级>」+ 理由)
222
233
 
223
- ## Runtime Evidence(integration-critical / deployment-critical 必填)
234
+ ## Runtime Evidence(integration-critical / deployment-critical 必填;自报告,CLI 仅校验字面存在、不独立核验,须真实执行过)
224
235
  - daemon 启动命令:
225
236
  - backend 地址:
226
237
  - 创建 session / 调用核心 API 的请求:
@@ -8,9 +8,13 @@
8
8
  * .sillyspec/.runtime/execute-runs/<runId>/tasks/<taskId>/review.json
9
9
  */
10
10
 
11
- import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync } from 'fs'
11
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync, statSync } from 'fs'
12
12
  import { join, resolve } from 'path'
13
13
  import { execFileSync } from 'child_process'
14
+ import { pathMatches } from './change-list.js'
15
+ import { parseAllowedPaths } from './stages/plan-postcheck.js'
16
+ import { resolveVerifyChangedFiles } from './verify-postcheck.js'
17
+ import { WorktreeManager } from './worktree.js'
14
18
 
15
19
  // ── review.json schema version ──
16
20
  export const REVIEW_SCHEMA_VERSION = 1
@@ -33,6 +37,122 @@ export function parseTaskIdsFromPlan(planContent) {
33
37
  return [...ids].sort()
34
38
  }
35
39
 
40
+ /**
41
+ * 统计 plan.md/tasks.md 中 task-NN checkbox 的勾选情况(降级口径)。
42
+ * 与 progress.js readPlanCheckboxStatus 同语义,但纯函数、无类依赖,供 summarizeTaskCompletion 降级复用。
43
+ */
44
+ function countPlanCheckboxes(planContent) {
45
+ const re = /^\s*[-*]\s+\[([ xX])\]\s+task-\d+/gm
46
+ let total = 0
47
+ let checked = 0
48
+ let m
49
+ while ((m = re.exec(planContent)) !== null) {
50
+ total++
51
+ if (m[1] === 'x' || m[1] === 'X') checked++
52
+ }
53
+ return { total, checked }
54
+ }
55
+
56
+ /**
57
+ * 汇总变更的任务完成度 —— archive Step1「任务完成度检查」的客观真相源。
58
+ *
59
+ * 以 execute 阶段产出的 review.json verdict 为准(task-NN 的 specVerdict + qualityVerdict 均 ≠ fail
60
+ * 视为完成),而非 plan.md checkbox。原因:plan.md checkbox 依赖 autoCheckPlanFromReviews 回填,
61
+ * 而 runId marker / review.json 缺失时回填静默 no-op,checkbox 会停在未勾态,导致完成度失真
62
+ * (archive 误判「全未完成」)。review.json 是 task 级客观 verdict,不受回填断裂影响。
63
+ *
64
+ * runId 解析:优先 current-execute-run-id-<changeName> marker(最新一次 execute run);
65
+ * marker 缺失则扫描 execute-runs/ 下子目录、取 mtime 最新的,尽力定位。
66
+ *
67
+ * fail-safe:无 plan / 无 runId / review 全缺等任何异常,降级到 plan.md checkbox 统计 + 标注 source,
68
+ * 绝不抛错、绝不阻断 archive(最坏退回「数 checkbox」现状,不比修复前差)。
69
+ *
70
+ * @param {{ changeDir: string, runtimeRoot: string, changeName: string }} opts
71
+ * @returns {{ source: string, total: number, completed: number, pending: Array<{id:string,reason:string}>, report: string }}
72
+ * source: 'review.json'(客观源可用)/ 'plan-checkbox-fallback'(降级)/ 'no-plan' / 'no-tasks'
73
+ * report: 人类可读报告,供 prompt {TASK_COMPLETION_REPORT} 注入
74
+ */
75
+ export function summarizeTaskCompletion({ changeDir, runtimeRoot, changeName }) {
76
+ const noPlan = {
77
+ source: 'no-plan', total: 0, completed: 0, pending: [],
78
+ report: '⚠️ 未找到 plan.md/tasks.md,无法计算任务完成度。请确认变更目录完整。'
79
+ }
80
+ if (!changeDir) return noPlan
81
+
82
+ const planPath = join(changeDir, 'plan.md')
83
+ const tasksPath = join(changeDir, 'tasks.md')
84
+ let planContent = null
85
+ if (existsSync(planPath)) planContent = readFileSync(planPath, 'utf8')
86
+ else if (existsSync(tasksPath)) planContent = readFileSync(tasksPath, 'utf8')
87
+ else return noPlan
88
+
89
+ const taskIds = parseTaskIdsFromPlan(planContent)
90
+ if (taskIds.length === 0) {
91
+ return { source: 'no-tasks', total: 0, completed: 0, pending: [],
92
+ report: '⚠️ plan.md 未解析出任何 task-NN 条目(plan 可能未按规范写 checkbox 列表)。' }
93
+ }
94
+
95
+ // runId 解析:marker → 扫描最新目录
96
+ let runId = null
97
+ const marker = join(runtimeRoot, `current-execute-run-id-${changeName}`)
98
+ try { if (existsSync(marker)) runId = readFileSync(marker, 'utf8').trim() } catch {}
99
+ if (!runId) {
100
+ try {
101
+ const runsDir = join(runtimeRoot, 'execute-runs')
102
+ if (existsSync(runsDir)) {
103
+ const entries = readdirSync(runsDir)
104
+ .map(e => ({ e, p: join(runsDir, e) }))
105
+ .filter(x => { try { return statSync(x.p).isDirectory() } catch { return false } })
106
+ .map(x => ({ e: x.e, mtime: statSync(x.p).mtimeMs }))
107
+ .sort((a, b) => b.mtime - a.mtime)
108
+ if (entries.length > 0) runId = entries[0].e
109
+ }
110
+ } catch {}
111
+ }
112
+
113
+ // 无 runId → 降级 plan.md checkbox 统计
114
+ if (!runId) {
115
+ const { total, checked } = countPlanCheckboxes(planContent)
116
+ return {
117
+ source: 'plan-checkbox-fallback', total, completed: checked,
118
+ pending: taskIds.slice(0, Math.max(0, total - checked)).map(id => ({ id, reason: 'checkbox 未勾(降级口径)' })),
119
+ report:
120
+ '⚠️ 客观源不可用(无 execute runId marker,review.json 无法定位)→ 降级用 plan.md checkbox。\n' +
121
+ `- 总任务:${total}\n- plan.md 已勾选:${checked}\n` +
122
+ '⚠️ checkbox 可能因 autoCheckPlanFromReviews 回填断裂而失真,务必交叉核对 .runtime/execute-runs/*/tasks/task-NN/review.json 的 verdict 再下结论。'
123
+ }
124
+ }
125
+
126
+ // 有 runId → 读 review.json verdict
127
+ const completed = []
128
+ const pending = []
129
+ for (const id of taskIds) {
130
+ const reviewPath = join(runtimeRoot, 'execute-runs', runId, 'tasks', id, 'review.json')
131
+ const r = readReview(reviewPath)
132
+ if (r.ok && r.review && r.review.specVerdict !== 'fail' && r.review.qualityVerdict !== 'fail') {
133
+ completed.push(id)
134
+ } else if (r.ok && r.review) {
135
+ pending.push({ id, reason: `verdict 未通过(spec=${r.review.specVerdict}, quality=${r.review.qualityVerdict})` })
136
+ } else {
137
+ pending.push({ id, reason: 'review.json 缺失(task 未走完 execute 评审)' })
138
+ }
139
+ }
140
+
141
+ const pendingLines = pending.length > 0
142
+ ? pending.map(p => ` - ${p.id}: ${p.reason}`).join('\n')
143
+ : ' (无)'
144
+ return {
145
+ source: 'review.json', total: taskIds.length, completed: completed.length, pending,
146
+ report:
147
+ `客观任务完成度(真相源 = review.json verdict,runId=${runId}):\n` +
148
+ `- 总任务:${taskIds.length}\n` +
149
+ `- 已通过(spec + quality verdict 均非 fail):${completed.length}\n` +
150
+ `- 未通过 / 缺失:${pending.length}\n` +
151
+ `- 未完成列表:\n${pendingLines}\n` +
152
+ '注:以 review.json verdict 为准;plan.md checkbox 仅作显示态(回填断裂时会与客观 verdict 不一致,以下方客观点为准)。'
153
+ }
154
+ }
155
+
36
156
  /**
37
157
  * 校验单个 review.json 文件
38
158
  * @param {object} review - 解析后的 JSON 对象
@@ -79,6 +199,36 @@ export function validateReviewSchema(review) {
79
199
  return { ok: errors.length === 0, errors }
80
200
  }
81
201
 
202
+ /**
203
+ * 增强 JSON.parse 失败的定位:从 V8 的 "at position N" 算出 line:col + 出错行上下文,
204
+ * 并对常见非法转义(正则的反斜杠序列误入 JSON 字符串)给针对性修复指引。
205
+ * @param {string} raw 原始文件内容
206
+ * @param {string} message V8 抛出的错误消息
207
+ * @returns {string} 增强后的可读消息
208
+ */
209
+ export function enrichJsonParseError(raw, message) {
210
+ const m = /at position (\d+)/.exec(message)
211
+ if (!m) return message
212
+ const pos = Math.min(Number(m[1]), raw.length)
213
+ let line = 1, col = 1
214
+ for (let i = 0; i < pos; i++) {
215
+ if (raw[i] === '\n') { line++; col = 1 } else { col++ }
216
+ }
217
+ const lineStart = raw.lastIndexOf('\n', pos - 1) + 1
218
+ const nlAfter = raw.indexOf('\n', pos)
219
+ const lineEnd = nlAfter === -1 ? raw.length : nlAfter
220
+ const snippet = raw.slice(lineStart, lineEnd)
221
+ const caret = ' '.repeat(Math.max(0, col - 1)) + '^'
222
+ // 单行注释内的反斜杠序列是纯文本,不触发块注释终止;正则字面量在函数体内正常解析。
223
+ let out = message + '\n → 第 ' + line + ' 行第 ' + col + ' 列:\n ' + snippet + '\n ' + caret
224
+ // 先移除合法的成对反斜杠(JSON 中 \\ 表字面 \),否则 "\\z" 会被逐字符正则误判为 "\z" 非法转义
225
+ const hasBadEscape = message.includes('Bad escaped character') || /\\[^"\\/bfnrtu]/.test(snippet.replace(/\\\\/g, ''))
226
+ if (hasBadEscape) {
227
+ out += '\n → 疑似正则转义(如 \\s \\d \\w)混入 JSON 字符串。修复:双写反斜杠(\\\\s),或用 JSON.stringify(obj) 重写 review.json。'
228
+ }
229
+ return out
230
+ }
231
+
82
232
  /**
83
233
  * 读取单个 task 的 review.json
84
234
  * @param {string} reviewPath - review.json 文件路径
@@ -100,8 +250,8 @@ export function readReview(reviewPath) {
100
250
  try {
101
251
  parsed = JSON.parse(raw)
102
252
  } catch (e) {
103
- // 文件存在但 JSON 非法:review 设为 null 但标记 parseError=true
104
- return { ok: false, review: null, parseError: true, errors: [`review.json 解析失败: ${e.message}`] }
253
+ // 文件存在但 JSON 非法:review 设为 null 但标记 parseError=true,错误消息带行列定位 + 转义修复指引
254
+ return { ok: false, review: null, parseError: true, errors: [`review.json 解析失败: ${enrichJsonParseError(raw, e.message)}`] }
105
255
  }
106
256
 
107
257
  const schemaResult = validateReviewSchema(parsed)
@@ -112,6 +262,34 @@ export function readReview(reviewPath) {
112
262
  return { ok: true, review: parsed, errors: [] }
113
263
  }
114
264
 
265
+ /**
266
+ * 校验 plan.md 里所有「已勾选 [x]」task 的 review.json 是否 schema 完整(坑 review-json-field-gap)。
267
+ *
268
+ * Task Review Gate(validateTaskReviews)只在 execute 整阶段完成时跑,单 task --done 不校验 →
269
+ * 子代理勾了 checkbox 却漏写/漏字段 review.json,要到收尾才暴露,用户被迫事后批量补。本函数供
270
+ * enforceReviewJsonGate 在每次 execute --done 调用:已勾 [x] = 声称完成 = 必有完整 review.json;
271
+ * 未勾 task 不校验(还没做)。readReview 已区分 missing/parseError/schemaError,这里只聚合。
272
+ *
273
+ * @param {{ planContent: string, runtimeRoot: string, executeRunId: string }} opts
274
+ * @returns {{ ok: boolean, failures: Array<{ taskId:string, reviewPath:string, kind:string, errors:string[] }> }}
275
+ * kind ∈ missing(review.json 不存在)/ parseError(JSON 坏)/ schemaError(字段缺)
276
+ */
277
+ export function validateCheckedTaskReviews({ planContent, runtimeRoot, executeRunId }) {
278
+ const failures = []
279
+ const re = /^\s*[-*]\s*\[x\]\s*task-(\d+)/gim
280
+ let m
281
+ while ((m = re.exec(planContent)) !== null) {
282
+ const taskId = `task-${m[1].padStart(2, '0')}`
283
+ const reviewPath = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', taskId, 'review.json')
284
+ const r = readReview(reviewPath)
285
+ if (!r.ok) {
286
+ const kind = r.parseError ? 'parseError' : (r.schemaError ? 'schemaError' : 'missing')
287
+ failures.push({ taskId, reviewPath, kind, errors: r.errors })
288
+ }
289
+ }
290
+ return { ok: failures.length === 0, failures }
291
+ }
292
+
115
293
  /**
116
294
  * execute --done 时的 task review 总校验
117
295
  *
@@ -402,6 +580,133 @@ export function generateExecuteRunId() {
402
580
  return `exec-${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
403
581
  }
404
582
 
583
+ /**
584
+ * worktree execute「主 agent 直接实现」模式收尾兜底:per-task review.json 缺失时,
585
+ * 据 git diff base..head 按 task allowed_paths 归属,自动落盘 cannot_verify 草稿。
586
+ *
587
+ * 根因(坑 worktree-execute-apply-friction 坑2):per-task review.json 无程序化 writer,
588
+ * 全靠子代理手写;主 agent 统一实现强依赖链/机械 task 时不走子代理 review 流程 →
589
+ * review.json 全缺,execute --done 的 Task Review Gate 报「task-XX 缺少 review.json」阻断。
590
+ *
591
+ * 复用(与 Task Review Gate / verify 同源,单一真相):
592
+ * - resolveVerifyChangedFiles(worktree-aware,meta.json 为权威)→ base..head diff 文件集
593
+ * - WorktreeManager.getMeta → baseHash(base)+ worktreePath(决定 head/gitDir,同 gates.js reviewGitDir)
594
+ * - parseAllowedPaths + pathMatches(同 worktree-apply Gate2 口径)→ task 文件归属
595
+ * - exec-id 解析同 gates.js:269 / autoCheckPlanFromReviews:current-execute-run-id-<change> marker,
596
+ * 缺失则 generateExecuteRunId + 落盘(保证草稿与后续 gate 读同一 run 目录)
597
+ *
598
+ * 行为:
599
+ * - 仅 review.json 不存在时写(幂等,已有人工/子代理 verdict 一律跳过,绝不覆盖)
600
+ * - changedFiles 为空的 task 不生成(verifyReviewGitEvidence 判空 diff 伪造,留给 agent)
601
+ * - 草稿 verdict=cannot_verify + 非空 requiredEvidence(过 schema,流转 verify 阶段兑现)
602
+ * - 不属任何 task 的文件累计 unattributed(顺带修复/非源码),不强塞某 task
603
+ *
604
+ * fail-open:任何异常 → 返回统计,仅 console.warn,不阻断 execute 完成(草稿是兜底,
605
+ * 缺了也只是退回原状——gate 报缺 review.json,agent 手补,不比修复前差)。
606
+ *
607
+ * @param {{ changeName: string, cwd: string, platformOpts?: object }} opts
608
+ * @returns {Promise<{ generated: number, skipped: number, unattributed: string[], reason?: string, executeRunId?: string }>}
609
+ */
610
+ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts = {} }) {
611
+ const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
612
+ const runtimeRoot = platformOpts.runtimeRoot || join(specBase, '.runtime')
613
+ const changeDir = join(specBase, 'changes', changeName)
614
+ const tasksDir = join(changeDir, 'tasks')
615
+
616
+ if (!changeName) {
617
+ return { generated: 0, skipped: 0, unattributed: [], reason: '无 changeName' }
618
+ }
619
+ // 无 task 卡片目录 → 非多 task 变更,无需补草稿
620
+ if (!existsSync(tasksDir)) {
621
+ return { generated: 0, skipped: 0, unattributed: [], reason: '无 tasks/ 目录' }
622
+ }
623
+
624
+ // exec-id:与 Task Review Gate(gates.js:269)/ autoCheckPlanFromReviews 同源
625
+ const runIdFile = join(runtimeRoot, 'current-execute-run-id-' + changeName)
626
+ let executeRunId = ''
627
+ try { if (existsSync(runIdFile)) executeRunId = readFileSync(runIdFile, 'utf8').trim() } catch {}
628
+ if (!executeRunId) {
629
+ executeRunId = generateExecuteRunId()
630
+ try { mkdirSync(runtimeRoot, { recursive: true }); writeFileSync(runIdFile, executeRunId + '\n') } catch {}
631
+ }
632
+
633
+ // base..head diff 文件集(worktree-aware;null=git 不可用,[]=无 commit diff)
634
+ const diffFiles = resolveVerifyChangedFiles(cwd, changeName)
635
+ if (!diffFiles || diffFiles.length === 0) {
636
+ return { generated: 0, skipped: 0, unattributed: [], executeRunId, reason: 'base..head 无代码 diff(改动未 commit?)' }
637
+ }
638
+
639
+ // base/head + gitDir:与 gates.js reviewGitDir 同源(worktree 优先,in-place 回退 cwd)
640
+ let base = null
641
+ let head = null
642
+ let reviewGitDir = cwd
643
+ try {
644
+ const wm = new WorktreeManager({ cwd })
645
+ const meta = wm.getMeta(changeName)
646
+ base = (meta && (meta.baselineCommit || meta.baseHash)) || null
647
+ if (meta && meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
648
+ reviewGitDir = meta.worktreePath
649
+ }
650
+ } catch {}
651
+ if (!base) {
652
+ return { generated: 0, skipped: 0, unattributed: diffFiles, executeRunId, reason: '无 worktree meta.baseHash(无法定 base/head)' }
653
+ }
654
+ try {
655
+ head = runGit(reviewGitDir, ['rev-parse', 'HEAD'])
656
+ } catch {
657
+ return { generated: 0, skipped: 0, unattributed: diffFiles, executeRunId, reason: 'git rev-parse HEAD 失败(' + reviewGitDir + ')' }
658
+ }
659
+
660
+ const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f)).sort()
661
+ let generated = 0
662
+ let skipped = 0
663
+ const attributed = new Set()
664
+
665
+ for (const tf of taskFiles) {
666
+ const taskId = tf.replace(/\.md$/, '')
667
+ const reviewDir = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', taskId)
668
+ const reviewPath = join(reviewDir, 'review.json')
669
+
670
+ // 幂等:review.json 已存在(无论合法/解析错/schema 错)一律跳过,绝不覆盖人工/子代理 verdict
671
+ const existing = readReview(reviewPath)
672
+ if (existing.ok || existing.parseError || existing.schemaError) {
673
+ skipped++
674
+ continue
675
+ }
676
+
677
+ const content = readFileSync(join(tasksDir, tf), 'utf8')
678
+ const allowedPaths = parseAllowedPaths(content)
679
+ const changedFiles = allowedPaths.length > 0
680
+ ? diffFiles.filter(f => allowedPaths.some(p => pathMatches(f, p)))
681
+ : []
682
+
683
+ // 空 changedFiles 的 task 不生成(verifyReviewGitEvidence 判空 diff 伪造,留给 agent 手补)
684
+ if (changedFiles.length === 0) {
685
+ skipped++
686
+ continue
687
+ }
688
+ changedFiles.forEach(f => attributed.add(f))
689
+
690
+ const draft = {
691
+ schemaVersion: REVIEW_SCHEMA_VERSION,
692
+ task: taskId,
693
+ base,
694
+ head,
695
+ changedFiles,
696
+ specVerdict: 'cannot_verify',
697
+ qualityVerdict: 'cannot_verify',
698
+ requiredEvidence: ['auto-generated draft: 待 agent 对照 ' + taskId + ' brief + diff 复核后升级为 pass/fail'],
699
+ reviewerNotes: 'auto-generated draft from git diff ' + base.slice(0, 8) + '..' + head.slice(0, 8) + ';verdict=未评审(worktree execute 主 agent 实现模式兜底,坑2)',
700
+ }
701
+ mkdirSync(reviewDir, { recursive: true })
702
+ writeFileSync(reviewPath, JSON.stringify(draft, null, 2) + '\n')
703
+ generated++
704
+ }
705
+
706
+ const unattributed = diffFiles.filter(f => !attributed.has(f))
707
+ return { generated, skipped, unattributed, executeRunId }
708
+ }
709
+
405
710
  /**
406
711
  * 获取当前(或最新)execute run id
407
712
  * 从 runtime 目录下查找 execute-runs/ 子目录