sillyspec 3.28.2 → 3.28.3

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-auto/SKILL.md +54 -93
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  3. package/.claude/skills/sillyspec-continue/SKILL.md +17 -47
  4. package/.claude/skills/sillyspec-execute/SKILL.md +222 -214
  5. package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -270
  6. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  7. package/.claude/skills/sillyspec-propose/SKILL.md +16 -87
  8. package/.claude/skills/sillyspec-resume/SKILL.md +21 -70
  9. package/.claude/skills/sillyspec-state/SKILL.md +25 -64
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +33 -175
  11. package/package.json +1 -1
  12. package/src/archive-delta.js +45 -2
  13. package/src/constants.js +6 -0
  14. package/src/design-facts.js +64 -0
  15. package/src/dispatch/backends/sillyhub-mcp.js +17 -5
  16. package/src/docs-check.js +119 -27
  17. package/src/docs-migrate.js +124 -0
  18. package/src/index.js +172 -22
  19. package/src/progress/change-registry.js +19 -0
  20. package/src/progress.js +2 -0
  21. package/src/run/command.js +129 -4
  22. package/src/run/complete-handlers.js +10 -3
  23. package/src/run/complete.js +59 -5
  24. package/src/run/gates.js +22 -2
  25. package/src/run/progress-confirm.js +66 -0
  26. package/src/run/prompt.js +139 -31
  27. package/src/run/quick-audit.js +12 -5
  28. package/src/run/scan-profile.js +20 -2
  29. package/src/run/shared.js +10 -5
  30. package/src/run/stage.js +7 -0
  31. package/src/scan-postcheck.js +5 -1
  32. package/src/stages/brainstorm-auto.js +2 -1
  33. package/src/stages/brainstorm.js +9 -17
  34. package/src/stages/execute.js +10 -14
  35. package/src/stages/plan.js +2 -1
  36. package/src/stages/quick.js +4 -2
  37. package/src/stages/verify.js +13 -15
  38. package/src/task-review.js +200 -0
  39. package/src/verify-postcheck.js +164 -30
  40. package/templates/agents-instruction.md +1 -1
  41. package/.claude/skills/verify-per-user/SKILL.md +0 -6
package/src/run/stage.js CHANGED
@@ -21,6 +21,7 @@ import { existsSync, readdirSync, readFileSync, mkdirSync } from 'node:fs'
21
21
  import { writeAtomicSync } from '../fs-atomic.js'
22
22
  import { resolveSpecDir, resolveChangeDir, resolveRuntimeRoot, resolveQuickSessionsDir, triggerSync, safeGit, parsePorcelainPath, formatWaitOptions, checkApproval, getStageSteps, warnApprovalUnknown, predictProtectedQuickFiles, mergeQuickBoundaryFiles, detectEmptyShellQuickSessions } from './shared.js'
23
23
  import { computeScanProfile, applyScanProfileSteps, executeScanPreflight, executeScanPostcheck, executeScanDetectProjects, executeScanResumeCheck, executeScanFinalize } from './scan-profile.js'
24
+ import { executeProgressConfirm } from './progress-confirm.js'
24
25
  import { outputStep, collectStageWaitHistory } from './prompt.js'
25
26
  import { allocateQuicklogEntry, deriveTitleFromLinkedChange, sanitizeDesc } from '../quicklog.js'
26
27
  import { createHash } from 'node:crypto'
@@ -537,6 +538,10 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
537
538
  await executeScanFinalize(cwd, platformOpts)
538
539
  } else if (cliAction === 'planPostcheck') {
539
540
  await executePlanPostcheck(cwd, platformOpts, progress)
541
+ } else if (cliAction === 'progressConfirm') {
542
+ // 主流程 step1「进度确认」noAI 化(2026-09-07):brainstorm/execute/verify——
543
+ // 快照本就 CLI 注入、阶段路由已由 run 落定,省一轮「复述摘要→--done」往返
544
+ executeProgressConfirm({ stageName, cwd, stageData, changeName, pm })
540
545
  } else {
541
546
  throw new Error(`noAI 步骤 ${stepName} 的未知 _cliAction: ${cliAction}——请在 stage.js 注册对应分支`)
542
547
  }
@@ -552,6 +557,8 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
552
557
  // 所有步骤完成
553
558
  stageData.status = 'completed'
554
559
  stageData.completedAt = new Date().toLocaleString('zh-CN', { hour12: false })
560
+ // 主阶段完成钉 currentStage(troubleshooting #56 根因修复,2026-09-08;与 complete.js 两处同语义)
561
+ if (!AUXILIARY_STAGES.includes(stageName)) progress.currentStage = stageName
555
562
  // persist _write 移到 completeStageGates 成功之后(task-02 / review-2026-08-09 #2):gate 异常/失败 → rollback 回 in-progress 落盘,此处未到 _write,DB 不留假 completed。
556
563
  // 阶段完成收尾共享管线(noAI 末步核心修复 S1:plan postcheck independent-tier review verdict=fail /
557
564
  // 平台 scan manifest 此前被绕过)。gate 失败已 rollback 为 in-progress,early-return(不 fall through 到末尾 return)。
@@ -42,7 +42,11 @@ function checkScanDocRefs(cwd, scanDir, docs) {
42
42
  severity: CHECK_SEVERITY.WARNING,
43
43
  detail: `${invalid.length} 条 file:line 引用未通过核验(${totalRefs} 条中): ${sample}${invalid.length > 3 ? ` 等 ${invalid.length} 条` : ''}`,
44
44
  evidence: { invalidCount: invalid.length, totalRefs, skippedCrossRepo, checkedDocs: docs.length, sample: invalid.slice(0, 3) },
45
- supportedFixes: ['运行 sillyspec docs check --suggest 查看候选锚点后修正引用', '删除无法核验的引用(引用了不存在的文件/行号)'],
45
+ supportedFixes: [
46
+ 'sillyspec docs check --paths <该文档> --dry-run 查看候选锚点(fixable 条目会给出 newLine),确认后去 --dry-run 落盘重锚',
47
+ 'sillyspec docs check --paths <该文档> --fix 对 fixable 引用自动重锚(回执附修复前后失效数对比)',
48
+ '先跑 sillyspec docs check --paths <该文档> 确认零候选锚点后,再删除确认无法核验的引用行',
49
+ ],
46
50
  }
47
51
  }
48
52
 
@@ -28,7 +28,8 @@ export const definition = {
28
28
  ### 加载上下文
29
29
  4. 读取项目总览 \`.sillyspec/docs/<project>/scan/PROJECT.md\`(如存在)+ 共享规范 + 子项目上下文
30
30
  5. 加载项目信息:\`cat .sillyspec/projects/*.yaml 2>/dev/null\`
31
- 6. 加载本地配置:\`cat .sillyspec/local.yaml 2>/dev/null\`
31
+ 6. 构建命令(CLI 自 local.yaml 注入,勿再读文件):
32
+ {LOCAL_COMMANDS}
32
33
  7. 棕地项目:读取 scan 下的 STRUCTURE.md、CONVENTIONS.md、ARCHITECTURE.md
33
34
  8. 加载模块索引:读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(如存在)
34
35
  9. 查看进行中的变更:\`ls .sillyspec/changes/ | grep -v archive\`
@@ -7,22 +7,13 @@ export const definition = {
7
7
  name: '进度确认',
8
8
  // 重命名自「状态检查」(③ status 消歧):老进度里的「状态检查」completed 由迁移逻辑承接,防 currentIdx 回跳
9
9
  migratedFrom: ['状态检查'],
10
- prompt: `检查当前变更的进度状态(快照已由 CLI 注入,勿再跑 progress show——那是全量输出,浪费上下文;\`sillyspec status\` 是项目级快照,也不推进流程)。
11
-
12
- ### 进度快照(CLI 注入)
13
- {PROGRESS_SNAPSHOT}
14
-
15
- ### 操作
16
- 1. **检查变更名称是否有意义**:如果当前变更名是自动生成的(如 \`2026-06-02-new-change-a3f2b7c1\`),询问用户确认实际变更名,然后运行 \`sillyspec change-rename <旧名> <新名>\` 重命名
17
- 2. 如果快照显示阶段不符或未初始化,输出正确提示并停止
18
-
19
- ### 输出
20
- 当前状态摘要(1-2 句话)
21
-
22
- ### 注意
23
- - 以 CLI 注入快照为准,不要自行推断阶段
24
- - **不要用 mv 命令重命名变更目录**,必须使用 \`sillyspec change-rename\`,否则 DB 和目录会脱节`,
25
- outputHint: '状态摘要',
10
+ // noAI 化(2026-09-07):快照本就 CLI 注入、阶段路由由 run 落定、变更名自动生成检测为机械
11
+ // 正则(命中只打印 rename 建议,agent 在工具结果可见、可在后续步骤交互改名)——省一轮
12
+ // 复述往返。步骤名保持不变(存量进度库按名匹配迁移)。
13
+ noAI: true,
14
+ _cliAction: 'progressConfirm',
15
+ prompt: '',
16
+ outputHint: '状态摘要(CLI 快照输出)',
26
17
  optional: false
27
18
  },
28
19
  {
@@ -34,7 +25,8 @@ export const definition = {
34
25
  - 漂移事实(CLI 算):{SCAN_STALENESS}
35
26
  - 机械事实底稿(CLI 注入,无 scan 底稿则为空):{SCAN_FACTS}
36
27
  2. 加载项目信息:\`cat {SPEC_ROOT}/projects/*.yaml 2>/dev/null\`
37
- 3. 加载本地配置:\`cat {SPEC_ROOT}/local.yaml 2>/dev/null\`
28
+ 3. 构建命令(CLI 自 local.yaml 注入,勿再读文件):
29
+ {LOCAL_COMMANDS}
38
30
  4. 棕地项目:读取 {SPEC_ROOT}/docs/<project>/scan/ 下的 STRUCTURE.md、CONVENTIONS.md、ARCHITECTURE.md
39
31
  5. **加载模块索引**:读取 \`{SPEC_ROOT}/docs/<project>/modules/_module-map.yaml\`(如存在)
40
32
  - 这一步是高频操作,_module-map.yaml 回答"哪个文件属于哪个模块、模块之间怎么依赖"
@@ -220,18 +220,12 @@ const fixedPrefix = [
220
220
  {
221
221
  name: '进度确认',
222
222
  migratedFrom: ['状态检查'],
223
- prompt: `检查当前进度,确认可以执行 execute(快照已由 CLI 注入,勿再跑 progress show;\`sillyspec status\` 是项目级快照,也不推进流程)。
224
-
225
- ### 进度快照(CLI 注入)
226
- {PROGRESS_SNAPSHOT}
227
-
228
- ### 操作
229
- 1. 如果快照显示不是 execute 阶段 → 检查是否有未完成的 tasks.md
230
- 2. 确认执行范围($ARGUMENTS 指定 wave/task 或全部)
231
-
232
- ### 输出
233
- 当前状态 + 执行范围确认`,
234
- outputHint: '当前状态 + 执行范围',
223
+ // noAI 化(2026-09-07):快照复述/阶段检查机械化(progressConfirm 补 tasks 勾选计数);
224
+ // 执行范围是会话层信息($ARGUMENTS 不经 CLI),以用户会话指定为准。步骤名不变。
225
+ noAI: true,
226
+ _cliAction: 'progressConfirm',
227
+ prompt: '',
228
+ outputHint: '当前状态 + 执行范围(CLI 快照输出)',
235
229
  optional: false
236
230
  },
237
231
  {
@@ -242,7 +236,8 @@ const fixedPrefix = [
242
236
  1. 读取 tasks.md(任务注册表与勾选唯一真相;plan.md 只提供 Wave 分组/依赖结构——Wave 段下为纯 ID 引用行)
243
237
  2. 读取 design.md(技术方案)
244
238
  3. 读取 CONVENTIONS.md、ARCHITECTURE.md
245
- 4. 读取 local.yaml(构建命令);若 local.yaml 不存在,先 \`sillyspec local detect\` 生成骨架再读取
239
+ 4. 构建命令(CLI 自 local.yaml 注入,勿再读文件;未配置时按注入说明跑 local detect):
240
+ {LOCAL_COMMANDS}
246
241
  5. 加载项目总览 \`.sillyspec/docs/<project>/scan/PROJECT.md\`(如存在)
247
242
 
248
243
  ### 模块文档加载(细粒度卡优先——CLI 已按 tasks 卡 allowed_paths 级联匹配)
@@ -378,7 +373,8 @@ review.json 已落盘(checklist=逐项核验表)+ checklist 摘要与偏差
378
373
  本步骤由当前 agent 执行,不需要启动独立子代理。
379
374
 
380
375
  ### 操作
381
- 1. 读取 local.yaml 获取构建和测试命令;若 local.yaml 不存在,先 \`sillyspec local detect\` 生成骨架再读取
376
+ 1. 构建命令(CLI 自 local.yaml 注入,勿再读文件;未配置时按注入说明跑 local detect):
377
+ {LOCAL_COMMANDS}
382
378
  2. 运行测试套件(单元测试、集成测试)
383
379
  3. 运行 lint 检查 **+ 格式化**:凡变更涉及的源码,既跑 lint check 也跑 formatter(如 \`ruff format\` / \`prettier --write\` / \`black\`),不要只跑 check——只 check 不 format 会把格式问题留到 commit 时被 pre-commit hook 拦截
384
380
  4. 如果有测试失败 → 分析原因,标注是代码问题还是测试本身的问题
@@ -63,7 +63,8 @@ const stepClassify = {
63
63
  - 如果发现 priority=P0/P1 且 status=unresolved/blocking 的决策,停止生成计划,要求先回到 brainstorm 的 Design Grill 修正
64
64
  - 如果发现 superseded 决策,只引用最新版本,不引用旧版本
65
65
  5. 读取 CONVENTIONS.md、ARCHITECTURE.md(技术栈含在 ARCHITECTURE.md)
66
- 6. 读取 local.yaml 获取构建/测试命令
66
+ 6. 构建命令(CLI 自 local.yaml 注入,勿再读文件):
67
+ {LOCAL_COMMANDS}
67
68
  7. 读取 \`{SPEC_ROOT}/docs/<project>/modules/_module-map.yaml\`(不存在则跳过)
68
69
  - 根据 design.md 的文件变更清单匹配模块
69
70
  - 读取匹配到的模块文档
@@ -23,7 +23,8 @@ step 3 --done 审计会把 src 核心文件(CLI / 状态机 / 注入框架等
23
23
  2. 理解任务:模糊则问一个问题确认
24
24
  3. 加载项目信息:\`cat {SPEC_ROOT}/projects/*.yaml 2>/dev/null\`(了解项目结构和技术栈)
25
25
  4. 加载上下文:\`cat {SPEC_ROOT}/docs/<project>/scan/CONVENTIONS.md 2>/dev/null\`
26
- 5. 加载本地配置:\`cat {SPEC_ROOT}/local.yaml 2>/dev/null\`(构建命令、测试命令、环境变量等)
26
+ 5. 构建命令(CLI 自 local.yaml 注入,勿再读文件):
27
+ {LOCAL_COMMANDS}
27
28
  6. 若有关联变更,加载每个变更的设计文档:\`cat {SPEC_ROOT}/changes/<c>/design.md 2>/dev/null\`(理解设计意图)
28
29
  7. 如有需要,查询知识库:\`cat {SPEC_ROOT}/knowledge/INDEX.md 2>/dev/null\`
29
30
 
@@ -121,7 +122,8 @@ sillyspec run quick --done --change <id> --req "…" --cause "…" --solution "
121
122
  3. 【--done 后】核对 QUICKLOG 标题(CLI 已从「需求:」自动提取,仅弱标题才手改)→ 若改了再 \`git add\`
122
123
 
123
124
  ### 操作
124
- 1. 查看 \`git status --porcelain\`,确认只包含本次 quick 相关文件
125
+ 1. 工作区脏文件(CLI 注入,勿重跑 \`git status\`)——确认只包含本次 quick 相关文件,无关改动不暂存:
126
+ {GIT_DIRTY}
125
127
  2. 使用 \`git add -- <file...>\` 暂存本次 quick 实际修改的文件(不要 commit,由用户通过统一提交工具处理)
126
128
  - 禁止使用 \`git add -A\`
127
129
  - 不要暂存 quick 开始前就已存在的无关改动
@@ -45,19 +45,12 @@ export const definition = {
45
45
  {
46
46
  name: '进度确认',
47
47
  migratedFrom: ['状态检查'],
48
- prompt: `> 💡 先说清楚:\`sillyspec run verify\`(不带 --done)**只下发执行指令,不会替你跑测试/构建**——真正的测试由 CLI 在最后 --done 时统一执行(local.yaml 的 commands.test,同步对账可能耗时较长);「运行测试和质量扫描」那步**不需要你重复手动跑全量**(避免与 CLI 对账重复耗时),只做 lint/静态检查 + 可选快速冒烟。别以为敲了 run verify 就自动验证了。
49
-
50
- 检查当前进度,确认可以执行 verify(快照已由 CLI 注入,勿再跑 progress show;\`sillyspec status\` 是项目级快照,也不推进流程)。
51
-
52
- ### 进度快照(CLI 注入)
53
- {PROGRESS_SNAPSHOT}
54
-
55
- ### 操作
56
- 1. 如果快照显示阶段不符,输出正确提示并停止
57
-
58
- ### 输出
59
- 当前进度摘要`,
60
- outputHint: '进度摘要',
48
+ // noAI 化(2026-09-07):唯一非常量信息(「CLI 在 --done 统一跑测试」)改由 progressConfirm
49
+ // console 提示;快照复述与阶段检查已机械化。步骤名不变(存量进度库按名匹配迁移)。
50
+ noAI: true,
51
+ _cliAction: 'progressConfirm',
52
+ prompt: '',
53
+ outputHint: '进度摘要(CLI 快照输出)',
61
54
  optional: false
62
55
  },
63
56
  {
@@ -70,7 +63,8 @@ export const definition = {
70
63
  - 如果存在 P0/P1 unresolved/blocking 决策,验证结论不能为 PASS
71
64
  - 如果发现 superseded 决策被下游引用,标记为 ⚠️ stale decision reference
72
65
  3. 加载项目信息:\`cat {SPEC_ROOT}/projects/*.yaml 2>/dev/null\`
73
- 4. 加载本地配置:\`cat {SPEC_ROOT}/local.yaml 2>/dev/null\`(构建命令、测试命令、lint 命令等)若 local.yaml 不存在,先 \`sillyspec local detect\` 生成骨架再读取
66
+ 4. 构建命令(CLI 自 local.yaml 注入,勿再读文件;未配置时按注入说明跑 local detect):
67
+ {LOCAL_COMMANDS}
74
68
  5. 加载代码规范:\`cat {SPEC_ROOT}/docs/<project>/scan/CONVENTIONS.md 2>/dev/null\`
75
69
  - 测试现状:\`cat {SPEC_ROOT}/docs/<project>/scan/TESTING.md 2>/dev/null\`(了解既有测试约定与覆盖范围,验收时对照)
76
70
  - 技术债清单:\`cat {SPEC_ROOT}/docs/<project>/scan/CONCERNS.md 2>/dev/null\`(🔴/🟡 区域;本次变更若触碰须在 verify-result.md 标注)
@@ -104,6 +98,9 @@ export const definition = {
104
98
  name: '逐项检查任务',
105
99
  prompt: `对照 tasks.md(任务注册表唯一真相)检查每个任务完成状态。勾选由 execute 双路写入(agent 按 review gate 手动勾 + CLI autoCheckPlanFromReviews 机器勾选器按 review.json 自动勾),本阶段只读对照、不改勾。
106
100
 
101
+ ### 勾选状态(CLI 注入,勿手数)
102
+ {TASKS_CHECKBOX}
103
+
107
104
  ### 操作
108
105
  对每个 checkbox:
109
106
  1. 检查相关文件是否存在
@@ -173,7 +170,8 @@ export const definition = {
173
170
  prompt: `运行代码质量扫描(测试实测统一由 CLI 对账执行,本步不重复手动跑全量)。
174
171
 
175
172
  ### 操作
176
- 1. 读取 \`{SPEC_ROOT}/local.yaml\` 获取构建、测试和 lint 命令若 local.yaml 不存在,先 \`sillyspec local detect\` 生成骨架再读取
173
+ 1. 构建命令(CLI 自 local.yaml 注入,勿再读文件;未配置时按注入说明跑 local detect):
174
+ {LOCAL_COMMANDS}
177
175
  2. **不要手动重复跑 commands.test**——CLI 会在最终 --done 时统一执行一次(按变更命中模块子集),本步再跑 = 与 CLI 对账重复耗时(实测 198s×2)。如为提前发现实现问题,可对变更模块做**针对性快速冒烟**(可选,非必需):
178
176
  - Maven:\`mvn test -pl <变更模块> -am\`(仅编译变更模块及其依赖)
179
177
  - Gradle:\`./gradlew :<模块>:test\`
@@ -1468,6 +1468,206 @@ export async function adoptTaskReviewMechanics({ changeName, cwd, platformOpts =
1468
1468
  return { adopted, unchanged, skipped, missing, downgraded, reasons, executeRunId }
1469
1469
  }
1470
1470
 
1471
+ /**
1472
+ * writeTaskReview —— 单 task review.json 的命令式写入(`sillyspec review write`,2026-09-07)。
1473
+ *
1474
+ * 背景(noAI 主流程批次 T2-4):task 级 review.json 此前由 agent/子代理手拼 JSON + 手算
1475
+ * base/head + 手抄 changedFiles——schemaVersion/目录创建/executeRunId 解析/git 锡点全部是
1476
+ * 纯机械活(generateTaskReviewDrafts/adoptTaskReviewMechanics 已证明 CLI 可代算),agent 只
1477
+ * 应贡献判断字段(verdict/notes/evidence)。命令式写入消灭「手拼 JSON→gate 拒→重写」循环。
1478
+ *
1479
+ * 职责分工:
1480
+ * - CLI 代算(mechanics):executeRunId(marker 优先)、base/head(worktree meta 优先,
1481
+ * --base/--head 显式覆盖)、changedFiles(diff × allowed_paths 归属切片,--changed-files
1482
+ * 显式覆盖)、diffPaths(allowed_paths 非空时)、repo 锡点(task 卡)。
1483
+ * - agent 供给(判断):--spec/--quality verdict、--notes、--evidence。
1484
+ *
1485
+ * 防伪/防误写:
1486
+ * - review.json 已存在 → 拒绝覆盖(--force 显式越过,控制台留痕)——保护独立 reviewer 产物。
1487
+ * - verdict=cannot_verify 且未给 --evidence → 报错(schema 硬要求非空 requiredEvidence),
1488
+ * 不静默填占位(与草稿语义不同:草稿是兜底待复核,这里是 agent 明确结论)。
1489
+ * - 归属切片为空(allowed_paths 未命中 diff)→ 报错并列出 diff 候选与 task allowed_paths
1490
+ * (帮 agent 当场发现「改了声明外文件 / 路径写错」),--changed-files 显式越过。
1491
+ *
1492
+ * @param {object} opts
1493
+ * @param {string} opts.changeName
1494
+ * @param {string} opts.cwd
1495
+ * @param {string} opts.taskId - 形如 task-NN
1496
+ * @param {string} opts.specVerdict - pass | fail | cannot_verify
1497
+ * @param {string} opts.qualityVerdict - 同上
1498
+ * @param {string} [opts.reviewerNotes]
1499
+ * @param {string[]} [opts.requiredEvidence]
1500
+ * @param {string|null} [opts.baseOverride] - 显式 base 锡点(无 worktree meta 时必填)
1501
+ * @param {string|null} [opts.headOverride]
1502
+ * @param {string[]|null} [opts.changedFilesOverride]
1503
+ * @param {boolean} [opts.force] - 覆盖已存在 review.json
1504
+ * @param {object} [opts.platformOpts]
1505
+ * @returns {Promise<{ok: boolean, reviewPath?: string, executeRunId?: string, errors: string[], warnings: string[]}>}
1506
+ */
1507
+ export async function writeTaskReview({
1508
+ changeName, cwd, taskId, specVerdict, qualityVerdict,
1509
+ reviewerNotes = '', requiredEvidence = [],
1510
+ baseOverride = null, headOverride = null, changedFilesOverride = null,
1511
+ force = false, platformOpts = {},
1512
+ }) {
1513
+ const errors = []
1514
+ const warnings = []
1515
+ const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
1516
+ const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
1517
+ const changeDir = join(specBase, 'changes', changeName)
1518
+
1519
+ // ── 入参校验 ──
1520
+ if (!changeName || typeof changeName !== 'string') errors.push('缺少 --change <变更名>')
1521
+ if (!taskId || !/^task-\d+$/.test(taskId)) errors.push(`--task 格式应为 task-NN(实际 ${taskId})`)
1522
+ if (!VALID_VERDICTS.includes(specVerdict)) errors.push(`--spec verdict 无效:${specVerdict}(应为 ${VALID_VERDICTS.join('/')})`)
1523
+ if (!VALID_VERDICTS.includes(qualityVerdict)) errors.push(`--quality verdict 无效:${qualityVerdict}(应为 ${VALID_VERDICTS.join('/')})`)
1524
+ if ((specVerdict === 'cannot_verify' || qualityVerdict === 'cannot_verify')
1525
+ && (!Array.isArray(requiredEvidence) || requiredEvidence.filter(Boolean).length === 0)) {
1526
+ errors.push('verdict=cannot_verify 必须给 --evidence "<非空说明>"(schema 硬要求 requiredEvidence 非空)')
1527
+ }
1528
+ if (errors.length > 0) return { ok: false, errors, warnings }
1529
+
1530
+ // ── task 卡与 executeRunId 解析(与草稿生成同源)──
1531
+ const taskCardPath = join(changeDir, 'tasks', `${taskId}.md`)
1532
+ if (!existsSync(taskCardPath)) {
1533
+ return { ok: false, errors: [`task 卡不存在:${taskCardPath}(先由 plan 阶段生成或手建)`], warnings }
1534
+ }
1535
+ const markerFile = join(runtimeRoot, 'current-execute-run-id-' + changeName)
1536
+ let executeRunId = ''
1537
+ try {
1538
+ if (existsSync(markerFile)) {
1539
+ const c = readFileSync(markerFile, 'utf8').trim()
1540
+ if (isValidExecuteRunId(c)) executeRunId = c
1541
+ }
1542
+ } catch {}
1543
+ if (!executeRunId) {
1544
+ executeRunId = resolveLatestExecuteRunId({ runtimeRoot, changeName }) || ''
1545
+ }
1546
+ if (!executeRunId) {
1547
+ executeRunId = generateExecuteRunId()
1548
+ warnings.push(`无 execute run marker——已生成新 runId ${executeRunId} 并建目录(正常应先跑过 execute Wave 步)`)
1549
+ try {
1550
+ mkdirSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'), { recursive: true })
1551
+ writeFileSync(markerFile, executeRunId + '\n')
1552
+ stampExecuteRunChange(runtimeRoot, executeRunId, changeName)
1553
+ } catch (e) {
1554
+ return { ok: false, errors: [`execute run 目录/marker 写入失败:${e.message}`], warnings }
1555
+ }
1556
+ }
1557
+
1558
+ const reviewDir = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', taskId)
1559
+ const reviewPath = join(reviewDir, 'review.json')
1560
+
1561
+ // ── 已存在保护(--force 越过)──
1562
+ if (existsSync(reviewPath) && !force) {
1563
+ return { ok: false, errors: [`review.json 已存在:${reviewPath}——拒绝覆盖(保护既有 reviewer 结论);确认重写加 --force`], warnings }
1564
+ }
1565
+ if (existsSync(reviewPath)) warnings.push(`--force 覆盖已存在 review:${reviewPath}`)
1566
+
1567
+ // ── mechanics 代算:base/head(worktree meta 优先,显式覆盖次之)──
1568
+ let base = baseOverride
1569
+ let head = headOverride
1570
+ let reviewGitDir = cwd
1571
+ if (!base || !head) {
1572
+ try {
1573
+ const wm = new WorktreeManager({ cwd })
1574
+ const meta = wm.getMeta(changeName)
1575
+ if (!base) base = (meta && (meta.baselineCommit || meta.baseHash)) || null
1576
+ if (meta && meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
1577
+ reviewGitDir = meta.worktreePath
1578
+ }
1579
+ } catch { /* meta 读取失败走显式覆盖路径 */ }
1580
+ }
1581
+ if (!head) {
1582
+ try { head = runGit(reviewGitDir, ['rev-parse', 'HEAD']) } catch { head = null }
1583
+ }
1584
+ if (!base || !head) {
1585
+ return {
1586
+ ok: false,
1587
+ errors: [`无法确定 base/head(无 worktree meta 且未显式覆盖)——用 --base <commit> --head <commit> 显式给出(git rev-parse 取值)`],
1588
+ warnings,
1589
+ }
1590
+ }
1591
+
1592
+ // ── mechanics 代算:changedFiles(diff × allowed_paths 归属切片;显式覆盖优先)──
1593
+ const content = readFileSync(taskCardPath, 'utf8')
1594
+ // 跨仓 task 守卫:base/head/changedFiles 的跨仓口径(task 卡锡点 + 跨仓仓 diff)本函数未实现
1595
+ //(草稿生成有专门分支)——显式拒绝并引导既有路径,防主仓锡点被误配到跨仓改动上
1596
+ const guardRepo = normalizeRepoKey(parseRepo(content))
1597
+ if (guardRepo && guardRepo !== 'main') {
1598
+ return {
1599
+ ok: false,
1600
+ errors: [`task 卡声明 repo=${guardRepo}(跨仓 task)——review write 暂不支持跨仓口径,请手写 review.json 后跑 sillyspec backfill-reviews --change ${changeName} --adopt 代填 mechanics(base/head 从 task 卡锡点取)`],
1601
+ warnings,
1602
+ }
1603
+ }
1604
+ const allowedPaths = parseAllowedPaths(content)
1605
+ let changedFiles = null
1606
+ if (Array.isArray(changedFilesOverride)) {
1607
+ // 显式覆盖(含空数组=纯验证任务的合法零 diff 声明)
1608
+ changedFiles = changedFilesOverride.map(p => String(p).replace(/\\/g, '/'))
1609
+ warnings.push(`changedFiles 采用显式覆盖(${changedFiles.length} 个)——evidence 校验仍会与 git diff 相交比对`)
1610
+ } else {
1611
+ let diffFiles = resolveVerifyChangedFiles(cwd, changeName, null, { specBase })
1612
+ // 并入 worktree 未提交改动(与草稿生成同源口径:子代理默认不 commit)
1613
+ try {
1614
+ const wm = new WorktreeManager({ cwd })
1615
+ const meta = wm.getMeta(changeName)
1616
+ if (meta) {
1617
+ const wtGitDir = (meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath))
1618
+ ? meta.worktreePath
1619
+ : cwd
1620
+ const wtStatus = runGit(wtGitDir, ['status', '--porcelain', '--untracked-files=all'], { trim: false })
1621
+ const wtFiles = parsePorcelainFiles(wtStatus)
1622
+ .map(p => String(p).replace(/\\/g, '/'))
1623
+ .filter(p => p !== '.sillyspec' && !p.startsWith('.sillyspec/'))
1624
+ if (wtFiles.length > 0) diffFiles = [...new Set([...(Array.isArray(diffFiles) ? diffFiles : []), ...wtFiles])]
1625
+ }
1626
+ } catch { /* 退回 commit diff 口径 */ }
1627
+ const mainDiff = Array.isArray(diffFiles) ? diffFiles : []
1628
+ changedFiles = allowedPaths.length > 0
1629
+ ? mainDiff.filter(f => allowedPaths.some(p => pathMatches(f, p)))
1630
+ : []
1631
+ if (changedFiles.length === 0) {
1632
+ return {
1633
+ ok: false,
1634
+ errors: [
1635
+ `归属切片为空:task 卡 allowed_paths ${JSON.stringify(allowedPaths)} 未命中本次 diff 任何文件。` +
1636
+ `本变更 diff 共 ${mainDiff.length} 个文件:${mainDiff.slice(0, 10).join('、')}${mainDiff.length > 10 ? '…' : ''}。` +
1637
+ `确认改动确属本 task → 用 --changed-files <a,b> 显式给出;纯验证任务(无代码 diff 是本质属性)用 --changed-files ""(空覆盖)+ --evidence 披露验证区间`,
1638
+ ],
1639
+ warnings,
1640
+ }
1641
+ }
1642
+ }
1643
+
1644
+ // ── 组装 + schema 自检 + 落盘 ──
1645
+ const review = {
1646
+ schemaVersion: REVIEW_SCHEMA_VERSION,
1647
+ task: taskId,
1648
+ base,
1649
+ head,
1650
+ changedFiles,
1651
+ ...(allowedPaths.length > 0 ? { diffPaths: allowedPaths } : {}),
1652
+ specVerdict,
1653
+ qualityVerdict,
1654
+ ...(requiredEvidence.filter(Boolean).length > 0 ? { requiredEvidence: requiredEvidence.filter(Boolean) } : {}),
1655
+ reviewerNotes: reviewerNotes || `written by sillyspec review write (${base.slice(0, 8)}..${head.slice(0, 8)})`,
1656
+ }
1657
+ const schemaResult = validateReviewSchema(review)
1658
+ if (!schemaResult.ok) {
1659
+ return { ok: false, errors: schemaResult.errors, warnings }
1660
+ }
1661
+ try {
1662
+ mkdirSync(reviewDir, { recursive: true })
1663
+ writeFileSync(reviewPath, JSON.stringify(review, null, 2) + '\n')
1664
+ } catch (e) {
1665
+ return { ok: false, errors: [`review.json 落盘失败:${e.message}`], warnings }
1666
+ }
1667
+ return { ok: true, reviewPath, executeRunId, errors: [], warnings }
1668
+ }
1669
+ // 注:跨仓 task(repo≠main)在上方 mechanics 段前置守卫拒绝——review.repo 恒为 main,不再单独赋值
1670
+
1471
1671
  /**
1472
1672
  * 获取当前(或最新)execute run id
1473
1673
  * 从 runtime 目录下查找 execute-runs/ 子目录