sillyspec 3.26.10 → 3.26.11

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 (97) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +85 -85
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  3. package/.claude/skills/sillyspec-commit/SKILL.md +109 -109
  4. package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
  5. package/.claude/skills/sillyspec-doctor/SKILL.md +76 -76
  6. package/.claude/skills/sillyspec-execute/SKILL.md +200 -200
  7. package/.claude/skills/sillyspec-explore/SKILL.md +95 -95
  8. package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
  9. package/.claude/skills/sillyspec-plan/SKILL.md +148 -148
  10. package/.claude/skills/sillyspec-quick/SKILL.md +111 -111
  11. package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
  12. package/.claude/skills/sillyspec-scan/SKILL.md +79 -79
  13. package/.claude/skills/sillyspec-state/SKILL.md +64 -64
  14. package/.claude/skills/sillyspec-status/SKILL.md +54 -54
  15. package/.claude/skills/sillyspec-verify/SKILL.md +83 -83
  16. package/.claude/skills/sillyspec-workspace/SKILL.md +169 -169
  17. package/.gitattributes +2 -0
  18. package/.husky/pre-push +20 -20
  19. package/.npmrc-publish +2 -0
  20. package/README.md +201 -201
  21. package/SKILL.md +90 -90
  22. package/package.json +39 -39
  23. package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
  24. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
  25. package/packages/dashboard/dist/index.html +16 -16
  26. package/packages/dashboard/package.json +25 -25
  27. package/packages/dashboard/server/executor.js +86 -86
  28. package/packages/dashboard/server/index.js +601 -601
  29. package/packages/dashboard/server/parser.js +526 -526
  30. package/src/change-risk-profile.js +254 -254
  31. package/src/classify-change.js +109 -109
  32. package/src/config-schema.js +310 -310
  33. package/src/contract-matrix.js +379 -379
  34. package/src/db.js +350 -350
  35. package/src/dispatch/backends/local-agent.js +155 -155
  36. package/src/dispatch/backends/sillyhub-mcp.js +245 -245
  37. package/src/dispatch/probe.js +210 -210
  38. package/src/dispatch/strategy.js +121 -121
  39. package/src/docs-check.js +131 -4
  40. package/src/doctor-diagnostics.js +711 -711
  41. package/src/endpoint-extractor.js +401 -401
  42. package/src/fs-atomic.js +69 -69
  43. package/src/git-helper.js +79 -79
  44. package/src/hooks/claude-pre-tool-use.cjs +125 -125
  45. package/src/hooks/worktree-guard.js +848 -848
  46. package/src/index.js +90 -8
  47. package/src/init.js +623 -623
  48. package/src/knowledge-match.js +130 -130
  49. package/src/local-detect.js +120 -120
  50. package/src/machine-interface.js +543 -543
  51. package/src/migrate.js +127 -127
  52. package/src/progress/change-registry.js +297 -260
  53. package/src/progress/consistency-doctor.js +355 -355
  54. package/src/progress/shared.js +36 -36
  55. package/src/progress/stage-machine.js +29 -1
  56. package/src/progress/step-store.js +261 -261
  57. package/src/progress.js +1131 -1127
  58. package/src/quicklog.js +693 -693
  59. package/src/review-tier.js +77 -77
  60. package/src/run/command.js +1385 -1364
  61. package/src/run/complete-handlers.js +1443 -1399
  62. package/src/run/complete.js +225 -24
  63. package/src/run/gates.js +755 -755
  64. package/src/run/prompt.js +761 -761
  65. package/src/run/quick-audit.js +155 -146
  66. package/src/run/shared.js +1268 -1215
  67. package/src/run/stage.js +487 -479
  68. package/src/scan-postcheck.js +371 -371
  69. package/src/setup.js +416 -416
  70. package/src/sillyhub-mcp/client.js +529 -529
  71. package/src/spec-sync.js +323 -319
  72. package/src/stage-contract.js +897 -897
  73. package/src/stage-review.js +549 -549
  74. package/src/stages/archive.js +155 -155
  75. package/src/stages/brainstorm-auto.js +236 -236
  76. package/src/stages/brainstorm.js +596 -597
  77. package/src/stages/cmd-existence.js +145 -145
  78. package/src/stages/doctor.js +371 -371
  79. package/src/stages/execute.js +1003 -1003
  80. package/src/stages/explore.js +34 -34
  81. package/src/stages/index.js +29 -29
  82. package/src/stages/knowledge.js +498 -498
  83. package/src/stages/plan-postcheck.js +1302 -1302
  84. package/src/stages/plan.js +625 -623
  85. package/src/stages/quick.js +151 -151
  86. package/src/stages/scan.js +607 -607
  87. package/src/stages/status.js +64 -64
  88. package/src/stages/verify.js +264 -264
  89. package/src/sync.js +1223 -1135
  90. package/src/task-review.js +1019 -1019
  91. package/src/verify-postcheck.js +1186 -1186
  92. package/src/workflow.js +600 -600
  93. package/src/worktree-apply.js +1213 -1136
  94. package/src/worktree-deps.js +353 -353
  95. package/src/worktree.js +1481 -1481
  96. package/templates/workflows/archive-impact.yaml +79 -79
  97. package/templates/workflows/scan-docs.yaml +132 -132
@@ -1,549 +1,549 @@
1
- /**
2
- * SillySpec Stage Review Gate — 阶段级审查校验(文档型)
3
- *
4
- * 与 task-review.js 的关系:
5
- * task-review 校验 execute 每个 task 的 review.json(git 代码 diff 证据:base/head)。
6
- * stage-review 校验 brainstorm/plan/execute-acceptance 的阶段级 review.json
7
- * (文档证据:reviewedFiles + docHash)。两者 verdict 语义一致(pass/fail/cannot_verify
8
- * 三态 + cannot_verify 必须带 requiredEvidence 的反逃逸规则),复用 task-review 导出的
9
- * VALID_VERDICTS / REVIEW_SCHEMA_VERSION 常量;task-review.js 本身不改、execute 路径零风险。
10
- *
11
- * 目录结构:
12
- * .sillyspec/.runtime/stage-reviews/<stage>-<reviewRunId>/review.json
13
- * (平台模式落 <runtimeRoot>/stage-reviews/...,与 execute-runs 同构)
14
- */
15
-
16
- import { existsSync, readFileSync, mkdirSync, readdirSync, writeFileSync } from 'fs'
17
- import { join } from 'path'
18
- import { createHash } from 'crypto'
19
- import { VALID_VERDICTS, REVIEW_SCHEMA_VERSION } from './task-review.js'
20
- import { detectSpecDirTypo } from './spec-dir-typo.js'
21
- import { resolveRuntimeRoot } from './run/shared.js'
22
-
23
- // 文档型 stage review 的合法 reviewType
24
- const STAGE_REVIEW_TYPES = ['design', 'plan', 'proposal', 'code', 'acceptance']
25
-
26
- // checklist 每项 result 的合法值
27
- const CHECKLIST_RESULTS = ['pass', 'gap', 'fail']
28
-
29
- // stage → 主审查文档(brainstorm/execute 审 design.md;plan 审 plan.md)
30
- const STAGE_MAIN_DOC = { brainstorm: 'design.md', plan: 'plan.md', execute: 'design.md' }
31
- // stage → reviewType(对齐 gates.js Stage Review Gate 的映射)
32
- const STAGE_REVIEW_TYPE = { brainstorm: 'design', plan: 'plan', execute: 'acceptance' }
33
-
34
- /**
35
- * 渲染 review.json 产物契约(markdown)给 review 子代理事前看 —— schema 表 + 完整 JSON 示例 +
36
- * docHash 算法 + 重算提示 + 位置。复用 validateStageReviewSchema 的同源常量,事前给的 == 事后查的。
37
- *
38
- * 历史教训:review 子代理靠读 stage-review.js 源码 + 翻模板才搞清 schema,常错(漏 schemaVersion /
39
- * checklist 按层嵌套对象 / docHash 用主文档改版前的旧 sha256)。本函数把契约前置进 prompt。
40
- *
41
- * @param {{ stage?: string, changeDir?: string, reviewRunId?: string, tier?: string }} opts
42
- * @returns {string} markdown 段;tier=self 返回简短提示(无需 review.json)
43
- */
44
- export function renderReviewJsonContract({ stage, changeDir, reviewRunId, tier } = {}) {
45
- if (tier === 'self') {
46
- return '> review.json 契约:tier=self(此刻 design.md 变更文件数 ≤3,当前 agent 自审即可)。⚠️ 注意:本判定基于此刻 design.md 快照——Stage Review Gate 会以 --done 时刻的 design.md 重新判定,若 design 后续扩大到 >3 文件,tier 将升级为 independent 并硬要求独立审查子代理产出 review.json。以 gate 实际校验结果为准(FAILED 即 independent 需补 review.json),勿据本提示提前断言无需 review.json。'
47
- }
48
- const reviewType = STAGE_REVIEW_TYPE[stage] || 'design'
49
- const mainDoc = STAGE_MAIN_DOC[stage] || 'design.md'
50
- const mainDocRel = changeDir ? 'changes/<change>/' + mainDoc : mainDoc
51
- const stageLbl = stage || '<stage>'
52
- const runIdLbl = reviewRunId || '<reviewRunId>'
53
- const L = [
54
- '## review.json 产物契约(CLI Stage Review Gate 将硬校验,以下为精确 schema —— 事前给的 == 事后查的)',
55
- '',
56
- '> 路径:`{SPEC_ROOT}/.runtime/stage-reviews/' + stageLbl + '-' + runIdLbl + '/review.json`(本次 reviewType=' + reviewType + ',主审查文档=' + mainDocRel + ')',
57
- '',
58
- '### 必填字段(validateStageReviewSchema 硬校验,缺任一即 schema 失败阻断)',
59
- '- `schemaVersion`: ' + REVIEW_SCHEMA_VERSION,
60
- '- `reviewType`: "' + reviewType + '"(必须与本 stage 一致;合法值 ' + STAGE_REVIEW_TYPES.join(' / ') + ')',
61
- '- `specVerdict` / `qualityVerdict`: ∈ { ' + VALID_VERDICTS.join(' / ') + ' }',
62
- '- `reviewedFiles`: 非空数组,[0] = 主审查文档 = `' + mainDocRel + '`',
63
- '- `docHash`: reviewedFiles[0] 文件内容的 sha256(hex,见下方算法)',
64
- '- `requiredEvidence`: 非空数组(specVerdict 或 qualityVerdict = cannot_verify 时必填,反逃逸)',
65
- '- `checklist`(可选): 扁平数组 —— 每项 { item: string, result: ∈ { ' + CHECKLIST_RESULTS.join(' / ') + ' }, note?: string }。注意是**扁平数组**,不是按层(定义/一致/可行性)嵌套对象',
66
- '- `reviewerNotes`: 说明(verdict=fail 时写明阻断项)',
67
- '',
68
- '### docHash 算法(必须等于 reviewedFiles[0] 的 sha256)',
69
- "- node: `crypto.createHash('sha256').update(require('fs').readFileSync(reviewedFiles[0])).digest('hex')`(对原始字节)",
70
- '- shell: `sha256sum <reviewedFiles[0]>` 取首列(hex)',
71
- '- ⚠️ **review.json 写入后若 ' + mainDoc + ' 再被改,必须重算 docHash** —— gate 会重算 reviewedFiles[0] 的 sha256 比对,不符判伪造(历史翻车根因:design 改了 rev 后 docHash 仍是旧值)',
72
- '',
73
- '### 完整 JSON 示例(照抄改值)',
74
- '```json',
75
- '{',
76
- ' "schemaVersion": ' + REVIEW_SCHEMA_VERSION + ',',
77
- ' "reviewType": "' + reviewType + '",',
78
- ' "specVerdict": "pass",',
79
- ' "qualityVerdict": "pass",',
80
- ' "reviewedFiles": ["' + mainDocRel + '"],',
81
- ' "docHash": "<上面算出的 sha256 hex>",',
82
- ' "checklist": [',
83
- ' { "item": "<审查项>", "result": "pass", "note": "<证据/说明>" }',
84
- ' ],',
85
- ' "reviewerNotes": "<总结>"',
86
- '}',
87
- '```',
88
- ]
89
- return L.join('\n')
90
- }
91
-
92
- /**
93
- * 计算文件内容的 sha256(用于 docHash 真实性校验,防 agent 凭空伪造 review.json)
94
- * @param {string} filePath - 绝对路径
95
- * @returns {string|null} sha256 hex;文件不存在返回 null
96
- */
97
- export function computeDocHash(filePath) {
98
- if (!filePath || !existsSync(filePath)) return null
99
- const content = readFileSync(filePath)
100
- return createHash('sha256').update(content).digest('hex')
101
- }
102
-
103
- /**
104
- * 校验文档型 stage review.json 的 schema
105
- *
106
- * 与 task-review.validateReviewSchema 的区别:
107
- * - 不要求 git base/head(证据形态不同)
108
- * - 改要求 reviewedFiles(被审查文档路径数组)+ docHash(主文档内容 sha256)
109
- * - 新增 reviewType(design/plan/proposal/code/acceptance)+ checklist
110
- * - 复用 VALID_VERDICTS 三态 + cannot_verify 联动规则(task-review.js:65-69)
111
- *
112
- * @param {object} review - 解析后的 JSON 对象
113
- * @returns {{ ok: boolean, errors: string[] }}
114
- */
115
- export function validateStageReviewSchema(review) {
116
- const errors = []
117
- if (!review || typeof review !== 'object') {
118
- return { ok: false, errors: ['stage review.json 不是有效 JSON 对象'] }
119
- }
120
-
121
- if (review.schemaVersion !== REVIEW_SCHEMA_VERSION) {
122
- errors.push(`schemaVersion 应为 ${REVIEW_SCHEMA_VERSION},实际为 ${review.schemaVersion}`)
123
- }
124
-
125
- if (!STAGE_REVIEW_TYPES.includes(review.reviewType)) {
126
- errors.push(`reviewType 无效:${review.reviewType}(应为 ${STAGE_REVIEW_TYPES.join('/')})`)
127
- }
128
-
129
- if (!VALID_VERDICTS.includes(review.specVerdict)) {
130
- errors.push(`specVerdict 无效:${review.specVerdict}(应为 ${VALID_VERDICTS.join('/')})`)
131
- }
132
- if (!VALID_VERDICTS.includes(review.qualityVerdict)) {
133
- errors.push(`qualityVerdict 无效:${review.qualityVerdict}(应为 ${VALID_VERDICTS.join('/')})`)
134
- }
135
-
136
- // cannot_verify 必须提供非空 requiredEvidence(与 task-review:65-69 一致,反逃逸)
137
- if (review.specVerdict === 'cannot_verify' || review.qualityVerdict === 'cannot_verify') {
138
- if (!Array.isArray(review.requiredEvidence) || review.requiredEvidence.length === 0) {
139
- errors.push('cannot_verify 的 verdict 必须提供非空的 requiredEvidence 数组')
140
- }
141
- }
142
-
143
- // reviewedFiles 必须是非空数组
144
- if (!Array.isArray(review.reviewedFiles) || review.reviewedFiles.length === 0) {
145
- errors.push('缺少 reviewedFiles 字段(应为被审查文档路径数组,[0] 为主审查对象,docHash 对应它)')
146
- }
147
-
148
- // docHash 非空
149
- if (!review.docHash || typeof review.docHash !== 'string') {
150
- errors.push('缺少 docHash 字段(被审查主文档内容的 sha256)')
151
- }
152
-
153
- // checklist 若提供,校验每项结构
154
- if (review.checklist !== undefined) {
155
- if (!Array.isArray(review.checklist)) {
156
- errors.push('checklist 若提供必须是数组')
157
- } else {
158
- for (let i = 0; i < review.checklist.length; i++) {
159
- const item = review.checklist[i]
160
- if (!item || typeof item !== 'object' || !item.item) {
161
- errors.push(`checklist[${i}] 缺少 item 描述`)
162
- continue
163
- }
164
- if (item.result !== undefined && !CHECKLIST_RESULTS.includes(item.result)) {
165
- errors.push(`checklist[${i}].result 无效:${item.result}(应为 ${CHECKLIST_RESULTS.join('/')})`)
166
- }
167
- }
168
- }
169
- }
170
-
171
- return { ok: errors.length === 0, errors }
172
- }
173
-
174
- /**
175
- * docHash 真实性交叉校验:review 声称审查了某文档,重算该文档 sha256 对比。
176
- *
177
- * 对应 task-review.verifyReviewGitEvidence 的"防伪造"职责——agent 不能凭空编一份
178
- * review.json 贴个假 hash。约定 docHash 对应 reviewedFiles[0](主审查对象)。
179
- *
180
- * searchDirs 依次尝试解析 reviewedFiles[0] 的相对路径(effectiveSpecBase / changeDir / cwd)。
181
- * 主文档在所有基准下都找不到时 fail-closed 阻断——连主审查文档都定位不到,要么 reviewedFiles[0]
182
- * 是伪造路径(防 agent 填假路径跳过校验),要么路径基准严重错位,两者都不该静默放行。
183
- *
184
- * @param {object} review - 已通过 schema 校验
185
- * @param {string[]} searchDirs - 解析相对路径的候选基准目录(绝对路径)
186
- * @returns {{ ok: boolean, errors: string[], warnings: string[] }}
187
- */
188
- export function verifyStageReviewDocHash(review, searchDirs) {
189
- const errors = []
190
- const warnings = []
191
- const files = Array.isArray(review.reviewedFiles) ? review.reviewedFiles : []
192
- if (files.length === 0) {
193
- return { ok: false, errors: ['reviewedFiles 为空,无法做 docHash 校验'], warnings }
194
- }
195
-
196
- const primaryRel = files[0]
197
-
198
- for (const base of searchDirs || []) {
199
- const abs = join(base, primaryRel)
200
- if (!existsSync(abs)) continue
201
- // 找到主文档,比对 hash。容忍「原始字节」与「LF 规范化」两种口径:Windows 下 CRLF/LF 在
202
- // git add / eol 规范化前后字节会漂移,同一文件 sha256 偶发不一致(坑 worktree-execute-apply-friction
203
- // 坑3)——agent 写的 docHash(原始 sha256sum)与 gate 重算只要任一口径匹配即认可,消除 CRLF
204
- // 漂移导致的反复「疑似伪造」误报。两口径都对不上仍判不匹配(防伪造能力不降级)。
205
- const content = readFileSync(abs, 'utf8')
206
- const actualRaw = createHash('sha256').update(content).digest('hex')
207
- const actualLf = createHash('sha256').update(content.replace(/\r\n/g, '\n')).digest('hex')
208
- const claimed = String(review.docHash).toLowerCase()
209
- if (claimed !== actualRaw.toLowerCase() && claimed !== actualLf.toLowerCase()) {
210
- errors.push(
211
- `docHash 与主审查文档 ${primaryRel} 的实际内容不匹配。` +
212
- `若刚改过该文档(如 design 修订后忘重算),重算并更新 review.json 的 docHash 字段:` +
213
- `在基准目录 ${base} 下运行 sha256sum "${primaryRel}" 取首列 hex 填入;` +
214
- `若未改文档,则 review.json 疑似伪造(未真正读取文档)。`
215
- )
216
- return { ok: false, errors, warnings }
217
- }
218
- return { ok: true, errors, warnings }
219
- }
220
-
221
- // 所有基准都找不到主文档 → fail-closed。连主审查文档都定位不到,说明 reviewedFiles[0] 要么
222
- // 是伪造路径(恶意 agent 填假路径跳过 docHash 校验),要么路径基准严重错位——两者都该阻断
223
- // 而非静默放行(历史降级 warning 是可被利用的伪造通道)。searchDirs =
224
- // [effectiveSpecBase(.sillyspec), reviewChangeDir(changes/<change>), cwd],契约规定的
225
- // reviewedFiles[0](changes/<change>/<mainDoc> 或 <mainDoc>)在这三个基准下必命中其一。
226
- errors.push('主审查文档 ' + primaryRel + ' 在 ' + (searchDirs || []).length + ' 个候选基准目录下均不存在 — 无法做 docHash 校验(reviewedFiles[0] 路径伪造或基准错位)')
227
- return { ok: false, errors, warnings }
228
- }
229
-
230
- /**
231
- * 生成 stage review run id
232
- * @returns {string} 如 'review-2026-07-16-143000'
233
- */
234
- export function generateStageReviewRunId() {
235
- const now = new Date()
236
- const pad = (n) => String(n).padStart(2, '0')
237
- return `review-${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
238
- }
239
-
240
- /**
241
- * stage review marker 文件路径(prompt 渲染时写 / getLatestStageReviewRunId 读,同源)。
242
- *
243
- * 对齐 execute 的 current-execute-run-id-<change> 语义:含 change 防多 change 串台
244
- * (同一 stage 不同变更的 review 各自独立 marker);changeName 缺失时退化为按 stage 分。
245
- *
246
- * @param {string} runtimeRoot - .runtime 绝对路径
247
- * @param {string} stage - brainstorm|plan|execute
248
- * @param {string} [changeName] - 变更名(防多 change 串台)
249
- * @returns {string} marker 文件绝对路径
250
- */
251
- export function stageReviewMarkerPath(runtimeRoot, stage, changeName) {
252
- const suffix = changeName ? `${stage}-${changeName}` : stage
253
- return join(runtimeRoot, `current-stage-review-run-id-${suffix}`)
254
- }
255
-
256
- /**
257
- * 获取最新的 stage review run id
258
- *
259
- * 优先读 marker 文件(prompt 渲染 {REVIEW_TIER} 时写入,保证 gate 取的 ID == 注入给 agent
260
- * 的 ID —— 修复「prompt 多次渲染 / 多次 review 时 gate 取错 ID 读错 review.json」),
261
- * 对齐 getLatestExecuteRunId(task-review.js:420-428)。marker 缺失时 fallback 扫描
262
- * stage-reviews/<stage>-review-* 目录取字典序最新(向后兼容无 marker 旧数据)。
263
- *
264
- * @param {string} runtimeRoot - .runtime 绝对路径
265
- * @param {string} stage - brainstorm|plan|execute
266
- * @param {string} [changeName] - 变更名(读对应 marker,防多 change 串台)
267
- * @returns {string|null} run id(如 'review-2026-07-16-143000'),无则 null
268
- */
269
- export function getLatestStageReviewRunId(runtimeRoot, stage, changeName) {
270
- // 优先读 marker(prompt 写 / gate 读同源,对齐 execute current-execute-run-id 机制)
271
- try {
272
- const markerPath = stageReviewMarkerPath(runtimeRoot, stage, changeName)
273
- if (existsSync(markerPath)) {
274
- const content = readFileSync(markerPath, 'utf8').trim()
275
- // marker 内容格式校验(execute 复盘 b2):必须 review- 前缀(generateStageReviewRunId 格式)。
276
- // agent 手写 marker 可能误填 execute 的 exec- 前缀 → 按 stage-reviews/<stage>-<runId> 拼目录必找不到,
277
- // 与其读坏 ID 报误导错误,不如显式 warn 并退回目录扫描。
278
- if (content) {
279
- if (/^review-/.test(content)) return content
280
- console.warn(`⚠️ stage-review marker ${markerPath} 内容 "${content.slice(0, 40)}" 不是 review- 前缀(应为 generateStageReviewRunId 格式,勿填 execute 的 exec- ID),忽略并退回目录扫描`)
281
- }
282
- }
283
- } catch {}
284
-
285
- // fallback:扫描 stage-reviews/<stage>-review-* 目录取字典序最新(向后兼容无 marker 旧数据)
286
- const dir = join(runtimeRoot, 'stage-reviews')
287
- if (!existsSync(dir)) return null
288
- try {
289
- const entries = readdirSync(dir, { withFileTypes: true })
290
- .filter(e => e.isDirectory() && e.name.startsWith(`${stage}-review-`))
291
- .map(e => e.name)
292
- .sort()
293
- .reverse()
294
- if (!entries[0]) return null
295
- // cross-change 防护(execute 复盘 b1):changeName 提供时按 review.json 的 reviewedFiles[0] 归属过滤。
296
- // 契约 reviewedFiles[0] = `changes/<change>/<mainDoc>`(renderReviewJsonContract),据此排除他变更的 review,
297
- // 否则批量完成 marker 缺失时可能读到 proxy 等其他变更的 acceptance review 报错误导。
298
- if (changeName) {
299
- const matching = entries.find(e => {
300
- try {
301
- const rj = JSON.parse(readFileSync(join(dir, e, 'review.json'), 'utf8'))
302
- const r0 = Array.isArray(rj.reviewedFiles) ? String(rj.reviewedFiles[0]) : ''
303
- return r0.includes(`changes/${changeName}/`)
304
- } catch { return false }
305
- })
306
- if (matching) return matching.slice(stage.length + 1)
307
- console.warn(`⚠️ 无 ${stage} 的 review marker,且 stage-reviews/ 下无归属变更 ${changeName} 的 review(reviewedFiles 应含 changes/${changeName}/);为避免跨变更串台不再取最新,返回 null(fail-closed,由调用方报缺 review.json)`)
308
- return null
309
- }
310
- // entries[0] = "plan-review-2026-...",剥掉 `${stage}-` 前缀得到 runId "review-2026-..."
311
- return entries[0].slice(stage.length + 1)
312
- } catch {
313
- return null
314
- }
315
- }
316
-
317
- /**
318
- * stage-level review 总校验(brainstorm/plan/execute-acceptance 的 done gate)
319
- *
320
- * 规则(与 task-review.validateTaskReviews 对称):
321
- * - review.json 缺失 → error 阻断
322
- * - 解析失败 / schema 失败 → error 阻断
323
- * - reviewType 与期望不符 → error(agent 复制模板未改)
324
- * - specVerdict 或 qualityVerdict 为 fail → error 阻断
325
- * - cannot_verify + 非空 requiredEvidence → warning
326
- * - cannot_verify + 空 requiredEvidence → error(schema 已拦,双保险)
327
- * - docHash 与主文档实际内容不符 → error(伪造)
328
- *
329
- * 调用方约定:先用 classifyReviewTier 判定 tier——tier=self 不调用本函数(直接放行),
330
- * tier=independent 才调用。
331
- *
332
- * @param {object} opts
333
- * @param {string} opts.stage - brainstorm|plan|execute
334
- * @param {string} opts.reviewType - design|plan|proposal|code|acceptance
335
- * @param {string} opts.runtimeRoot - .runtime 绝对路径(平台模式为 runtimeRoot)
336
- * @param {string} opts.reviewRunId - stage review run id(review-<ts>)
337
- * @param {string[]} [opts.searchDirs] - docHash 校验的候选基准目录
338
- * @param {boolean} [opts.verifyDocHash=true] - 是否做 docHash 真实性校验
339
- * @returns {{ ok: boolean, errors: string[], warnings: string[], review: object|null }}
340
- */
341
- export function validateStageReview(opts) {
342
- const { stage, reviewType, runtimeRoot, reviewRunId, searchDirs = [], verifyDocHash = true } = opts
343
- const errors = []
344
- const warnings = []
345
-
346
- const reviewDir = join(runtimeRoot, 'stage-reviews', `${stage}-${reviewRunId}`)
347
- const reviewPath = join(reviewDir, 'review.json')
348
-
349
- if (!reviewRunId || !existsSync(reviewPath)) {
350
- const errs = [`缺少 ${stage} 阶段的 stage review.json — tier=independent 要求独立审查子代理产出(期望路径:${reviewPath})`]
351
- const typo = detectSpecDirTypo(runtimeRoot)
352
- if (typo) errs.push(`💡 路径疑似拼错:发现 ${typo.typoDir} 目录(应为 ${typo.canonical}),review.json 可能误存于此——检查是否把 .sillyspec 拼成了变体`)
353
- return {
354
- ok: false,
355
- errors: errs,
356
- warnings,
357
- review: null,
358
- }
359
- }
360
-
361
- let raw
362
- try {
363
- raw = readFileSync(reviewPath, 'utf8')
364
- } catch (e) {
365
- return { ok: false, errors: [`${stage} review.json 读取失败: ${e.message}`], warnings, review: null }
366
- }
367
-
368
- let parsed
369
- try {
370
- parsed = JSON.parse(raw)
371
- } catch (e) {
372
- return { ok: false, errors: [`${stage} review.json 解析失败: ${e.message}`], warnings, review: null }
373
- }
374
-
375
- const schemaResult = validateStageReviewSchema(parsed)
376
- if (!schemaResult.ok) {
377
- return { ok: false, errors: [`${stage} review.json schema 校验失败 — ${schemaResult.errors.join('; ')}`], warnings, review: parsed }
378
- }
379
-
380
- // reviewType 与期望一致性
381
- if (parsed.reviewType && parsed.reviewType !== reviewType) {
382
- errors.push(`${stage} review.json 的 reviewType 为 "${parsed.reviewType}",与期望 "${reviewType}" 不符 — agent 可能复制模板未修改`)
383
- }
384
-
385
- // fail verdict
386
- if (parsed.specVerdict === 'fail' || parsed.qualityVerdict === 'fail') {
387
- errors.push(`${stage} review 未通过 — spec: ${parsed.specVerdict}, quality: ${parsed.qualityVerdict}`)
388
- if (parsed.reviewerNotes) errors.push(`${stage}: ${parsed.reviewerNotes}`)
389
- }
390
-
391
- // docHash 真实性(fail verdict 已错时跳过,避免噪声)
392
- if (errors.length === 0 && verifyDocHash && searchDirs.length > 0) {
393
- const dh = verifyStageReviewDocHash(parsed, searchDirs)
394
- for (const w of dh.warnings) warnings.push(`${stage}: ${w}`)
395
- if (!dh.ok) {
396
- for (const err of dh.errors) errors.push(`${stage}: ${err}`)
397
- }
398
- }
399
-
400
- // cannot_verify → warning(requiredEvidence 非空已由 schema 保证)
401
- if (errors.length === 0 && (parsed.specVerdict === 'cannot_verify' || parsed.qualityVerdict === 'cannot_verify')) {
402
- const verdicts = []
403
- if (parsed.specVerdict === 'cannot_verify') verdicts.push('spec')
404
- if (parsed.qualityVerdict === 'cannot_verify') verdicts.push('quality')
405
- warnings.push(`${stage}: ${verdicts.join('+')}=cannot_verify,requiredEvidence 需在后续阶段满足`)
406
- }
407
-
408
- return { ok: errors.length === 0, errors, warnings, review: parsed }
409
- }
410
-
411
- /**
412
- * 打印 stage review 校验结果
413
- * @param {{ ok: boolean, errors: string[], warnings: string[] }} result
414
- * @param {object} context - { stage, reviewRunId } 用于错误提示路径
415
- */
416
- export function printStageReviewResult(result, context = {}) {
417
- const { stage, reviewRunId, runtimeRoot } = context
418
- if (result.ok && result.warnings.length === 0) {
419
- console.log(`\n✅ Stage Review Gate — ${stage} 阶段独立审查通过`)
420
- return
421
- }
422
-
423
- if (result.errors.length > 0) {
424
- console.error(`\n🚫 Stage Review Gate — ${stage} FAILED`)
425
- for (const err of result.errors) {
426
- console.error(` - ${err}`)
427
- }
428
- // echo 完整 review.json 路径:runId + marker 已由 CLI 自动生成写入(prompt.js review step
429
- // 渲染 / gate 触发),agent 直接派独立子代理把 review.json 写到此路径,勿手算 runId、勿手写
430
- // marker(历史坑:agent 撞 gate 后自己算 runId 猜错 review- 前缀,填 exec-/裸 ts 被 CLI 忽略)。
431
- if (reviewRunId) {
432
- const reviewDir = runtimeRoot ? `${runtimeRoot}/stage-reviews/${stage}-${reviewRunId}` : `.sillyspec/.runtime/stage-reviews/${stage}-${reviewRunId}`
433
- console.error(`\n 📁 review.json 直接写此路径(runId/marker CLI 已生成写入,勿手算 runId、勿手写 marker):`)
434
- console.error(` ${reviewDir}/review.json`)
435
- }
436
- console.error(`\n 提示:tier=independent 要求独立审查子代理产出 review.json,补全后重新 --done`)
437
- console.error(` 可用 sillyspec register-stage-review --change <名> --stage ${stage} [--from <已有review.json>] 一步生成 run 目录 + review.json 骨架(docHash 自动算)+ 写 marker + 自检,省掉手动建目录/写 marker`)
438
- }
439
-
440
- if (result.warnings.length > 0) {
441
- console.warn(`\n⚠️ Stage Review Gate — ${stage} WARNING`)
442
- for (const w of result.warnings) {
443
- console.warn(` - ${w}`)
444
- }
445
- }
446
- }
447
-
448
- /**
449
- * 注册一个 stage 级 review(确定性 writer)—— 生成 run 目录 + review.json 骨架(或 adopt
450
- * agent 草稿)+ 写 marker,治 tier=independent 时「调度者手动派独立子代理 → marker 不写 →
451
- * gate 取错 run」的死锁。与 task 级 generateTaskReviewDrafts(task-review.js)对称。
452
- *
453
- * docHash 由 CLI 直接 computeDocHash 算(命令有文件访问权),消除 agent 手算 hash 易错
454
- * (部分实现 P6.1b defer:仅本 scaffold 路径确定性,不改 agent 自写 review.json 的 hash 链路)。
455
- *
456
- * 纯新增,不改 validateStageReview / getLatestStageReviewRunId / renderReviewJsonContract 等
457
- * 现有函数;self-check 只验 mechanics(schema + docHash),verdict 留给 Stage Review Gate 判。
458
- *
459
- * @param {object} opts
460
- * @param {string} opts.changeName - 变更名
461
- * @param {string} opts.stage - brainstorm|plan|execute
462
- * @param {string} [opts.fromFile] - adopt 模式:agent 已写的 review.json(保留 verdict/checklist,重算 docHash + 规范化 reviewedFiles[0])
463
- * @param {string} opts.cwd - 主仓 cwd(解析 specBase)
464
- * @param {object} [opts.platformOpts] - 平台选项(specRoot 等)
465
- * @returns {{ ok: boolean, reviewRunId: string, reviewPath: string, markerPath: string, mode: 'skeleton'|'adopted', mainDoc: string, review: object }}
466
- * @throws {Error} changeName 空 / stage 非法 / 主文档缺失 / --from 不存在或 schema 不过
467
- */
468
- export function registerStageReview({ changeName, stage, fromFile, cwd, platformOpts = {} }) {
469
- if (!changeName) throw new Error('register-stage-review: changeName 不能为空')
470
- if (!stage || !STAGE_MAIN_DOC[stage]) {
471
- throw new Error(`register-stage-review: stage 无效 "${stage}"(应为 brainstorm|plan|execute)`)
472
- }
473
-
474
- const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
475
- const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
476
- const changeDir = join(specBase, 'changes', changeName)
477
- const reviewType = STAGE_REVIEW_TYPE[stage]
478
- const mainDoc = STAGE_MAIN_DOC[stage]
479
- const mainDocPath = join(changeDir, mainDoc)
480
- if (!existsSync(mainDocPath)) {
481
- throw new Error(`register-stage-review: 主审查文档不存在 ${mainDocPath}(${stage} 审 ${mainDoc}),无法算 docHash`)
482
- }
483
- const docHash = computeDocHash(mainDocPath)
484
- if (!docHash) {
485
- throw new Error(`register-stage-review: 计算 docHash 失败(computeDocHash 返回 null):${mainDocPath}`)
486
- }
487
- const reviewedFiles = [`changes/${changeName}/${mainDoc}`]
488
-
489
- let review
490
- let mode
491
- if (fromFile) {
492
- // adopt 模式:保留 agent 的 verdict/checklist/reviewerNotes/requiredEvidence,仅修 mechanics
493
- const fromAbs = existsSync(fromFile) ? fromFile : join(cwd, fromFile)
494
- if (!existsSync(fromAbs)) {
495
- throw new Error(`register-stage-review: --from 文件不存在 ${fromFile}(相对 cwd ${fromAbs} 也未命中)`)
496
- }
497
- let parsed
498
- try {
499
- parsed = JSON.parse(readFileSync(fromAbs, 'utf8'))
500
- } catch (e) {
501
- throw new Error(`register-stage-review: --from 文件 JSON 解析失败 ${fromAbs}: ${e.message}`)
502
- }
503
- const schemaResult = validateStageReviewSchema(parsed)
504
- if (!schemaResult.ok) {
505
- throw new Error(`register-stage-review: --from 文件 schema 校验失败 — ${schemaResult.errors.join('; ')}`)
506
- }
507
- review = { ...parsed, docHash, reviewedFiles }
508
- mode = 'adopted'
509
- } else {
510
- // 骨架模式:cannot_verify 待审占位(schema 反逃逸:cannot_verify 必须带非空 requiredEvidence)
511
- review = {
512
- schemaVersion: REVIEW_SCHEMA_VERSION,
513
- reviewType,
514
- specVerdict: 'cannot_verify',
515
- qualityVerdict: 'cannot_verify',
516
- reviewedFiles,
517
- docHash,
518
- requiredEvidence: [`待独立审查子代理对照 ${mainDoc} 逐节核验(骨架由 register-stage-review 生成)`],
519
- reviewerNotes: '骨架由 register-stage-review 生成,verdict 待独立审查子代理填写',
520
- }
521
- mode = 'skeleton'
522
- }
523
-
524
- const reviewRunId = generateStageReviewRunId()
525
- const reviewDir = join(runtimeRoot, 'stage-reviews', `${stage}-${reviewRunId}`)
526
- const reviewPath = join(reviewDir, 'review.json')
527
- mkdirSync(reviewDir, { recursive: true })
528
- writeFileSync(reviewPath, JSON.stringify(review, null, 2) + '\n')
529
-
530
- const markerPath = stageReviewMarkerPath(runtimeRoot, stage, changeName)
531
- if (existsSync(markerPath)) {
532
- console.warn(`⚠️ register-stage-review: marker 已存在 ${markerPath},将被覆盖为 ${reviewRunId}`)
533
- }
534
- mkdirSync(runtimeRoot, { recursive: true })
535
- writeFileSync(markerPath, reviewRunId + '\n')
536
-
537
- // self-check(fail-closed):刚写的 review 必过 schema + docHash 真实性。只验 mechanics,
538
- // 不判 verdict(verdict 是 agent/子代理的审查结论,even fail 也如实落盘,由 Stage Review Gate 裁决)。
539
- const schemaRecheck = validateStageReviewSchema(review)
540
- if (!schemaRecheck.ok) {
541
- throw new Error(`register-stage-review: 写入后 schema 自检失败(不应发生)— ${schemaRecheck.errors.join('; ')}`)
542
- }
543
- const hashRecheck = verifyStageReviewDocHash(review, [specBase, changeDir, cwd])
544
- if (!hashRecheck.ok) {
545
- throw new Error(`register-stage-review: 写入后 docHash 自检失败(不应发生)— ${hashRecheck.errors.join('; ')}`)
546
- }
547
-
548
- return { ok: true, reviewRunId, reviewPath, markerPath, mode, mainDoc, review }
549
- }
1
+ /**
2
+ * SillySpec Stage Review Gate — 阶段级审查校验(文档型)
3
+ *
4
+ * 与 task-review.js 的关系:
5
+ * task-review 校验 execute 每个 task 的 review.json(git 代码 diff 证据:base/head)。
6
+ * stage-review 校验 brainstorm/plan/execute-acceptance 的阶段级 review.json
7
+ * (文档证据:reviewedFiles + docHash)。两者 verdict 语义一致(pass/fail/cannot_verify
8
+ * 三态 + cannot_verify 必须带 requiredEvidence 的反逃逸规则),复用 task-review 导出的
9
+ * VALID_VERDICTS / REVIEW_SCHEMA_VERSION 常量;task-review.js 本身不改、execute 路径零风险。
10
+ *
11
+ * 目录结构:
12
+ * .sillyspec/.runtime/stage-reviews/<stage>-<reviewRunId>/review.json
13
+ * (平台模式落 <runtimeRoot>/stage-reviews/...,与 execute-runs 同构)
14
+ */
15
+
16
+ import { existsSync, readFileSync, mkdirSync, readdirSync, writeFileSync } from 'fs'
17
+ import { join } from 'path'
18
+ import { createHash } from 'crypto'
19
+ import { VALID_VERDICTS, REVIEW_SCHEMA_VERSION } from './task-review.js'
20
+ import { detectSpecDirTypo } from './spec-dir-typo.js'
21
+ import { resolveRuntimeRoot } from './run/shared.js'
22
+
23
+ // 文档型 stage review 的合法 reviewType
24
+ const STAGE_REVIEW_TYPES = ['design', 'plan', 'proposal', 'code', 'acceptance']
25
+
26
+ // checklist 每项 result 的合法值
27
+ const CHECKLIST_RESULTS = ['pass', 'gap', 'fail']
28
+
29
+ // stage → 主审查文档(brainstorm/execute 审 design.md;plan 审 plan.md)
30
+ const STAGE_MAIN_DOC = { brainstorm: 'design.md', plan: 'plan.md', execute: 'design.md' }
31
+ // stage → reviewType(对齐 gates.js Stage Review Gate 的映射)
32
+ const STAGE_REVIEW_TYPE = { brainstorm: 'design', plan: 'plan', execute: 'acceptance' }
33
+
34
+ /**
35
+ * 渲染 review.json 产物契约(markdown)给 review 子代理事前看 —— schema 表 + 完整 JSON 示例 +
36
+ * docHash 算法 + 重算提示 + 位置。复用 validateStageReviewSchema 的同源常量,事前给的 == 事后查的。
37
+ *
38
+ * 历史教训:review 子代理靠读 stage-review.js 源码 + 翻模板才搞清 schema,常错(漏 schemaVersion /
39
+ * checklist 按层嵌套对象 / docHash 用主文档改版前的旧 sha256)。本函数把契约前置进 prompt。
40
+ *
41
+ * @param {{ stage?: string, changeDir?: string, reviewRunId?: string, tier?: string }} opts
42
+ * @returns {string} markdown 段;tier=self 返回简短提示(无需 review.json)
43
+ */
44
+ export function renderReviewJsonContract({ stage, changeDir, reviewRunId, tier } = {}) {
45
+ if (tier === 'self') {
46
+ return '> review.json 契约:tier=self(此刻 design.md 变更文件数 ≤3,当前 agent 自审即可)。⚠️ 注意:本判定基于此刻 design.md 快照——Stage Review Gate 会以 --done 时刻的 design.md 重新判定,若 design 后续扩大到 >3 文件,tier 将升级为 independent 并硬要求独立审查子代理产出 review.json。以 gate 实际校验结果为准(FAILED 即 independent 需补 review.json),勿据本提示提前断言无需 review.json。'
47
+ }
48
+ const reviewType = STAGE_REVIEW_TYPE[stage] || 'design'
49
+ const mainDoc = STAGE_MAIN_DOC[stage] || 'design.md'
50
+ const mainDocRel = changeDir ? 'changes/<change>/' + mainDoc : mainDoc
51
+ const stageLbl = stage || '<stage>'
52
+ const runIdLbl = reviewRunId || '<reviewRunId>'
53
+ const L = [
54
+ '## review.json 产物契约(CLI Stage Review Gate 将硬校验,以下为精确 schema —— 事前给的 == 事后查的)',
55
+ '',
56
+ '> 路径:`{SPEC_ROOT}/.runtime/stage-reviews/' + stageLbl + '-' + runIdLbl + '/review.json`(本次 reviewType=' + reviewType + ',主审查文档=' + mainDocRel + ')',
57
+ '',
58
+ '### 必填字段(validateStageReviewSchema 硬校验,缺任一即 schema 失败阻断)',
59
+ '- `schemaVersion`: ' + REVIEW_SCHEMA_VERSION,
60
+ '- `reviewType`: "' + reviewType + '"(必须与本 stage 一致;合法值 ' + STAGE_REVIEW_TYPES.join(' / ') + ')',
61
+ '- `specVerdict` / `qualityVerdict`: ∈ { ' + VALID_VERDICTS.join(' / ') + ' }',
62
+ '- `reviewedFiles`: 非空数组,[0] = 主审查文档 = `' + mainDocRel + '`',
63
+ '- `docHash`: reviewedFiles[0] 文件内容的 sha256(hex,见下方算法)',
64
+ '- `requiredEvidence`: 非空数组(specVerdict 或 qualityVerdict = cannot_verify 时必填,反逃逸)',
65
+ '- `checklist`(可选): 扁平数组 —— 每项 { item: string, result: ∈ { ' + CHECKLIST_RESULTS.join(' / ') + ' }, note?: string }。注意是**扁平数组**,不是按层(定义/一致/可行性)嵌套对象',
66
+ '- `reviewerNotes`: 说明(verdict=fail 时写明阻断项)',
67
+ '',
68
+ '### docHash 算法(必须等于 reviewedFiles[0] 的 sha256)',
69
+ "- node: `crypto.createHash('sha256').update(require('fs').readFileSync(reviewedFiles[0])).digest('hex')`(对原始字节)",
70
+ '- shell: `sha256sum <reviewedFiles[0]>` 取首列(hex)',
71
+ '- ⚠️ **review.json 写入后若 ' + mainDoc + ' 再被改,必须重算 docHash** —— gate 会重算 reviewedFiles[0] 的 sha256 比对,不符判伪造(历史翻车根因:design 改了 rev 后 docHash 仍是旧值)',
72
+ '',
73
+ '### 完整 JSON 示例(照抄改值)',
74
+ '```json',
75
+ '{',
76
+ ' "schemaVersion": ' + REVIEW_SCHEMA_VERSION + ',',
77
+ ' "reviewType": "' + reviewType + '",',
78
+ ' "specVerdict": "pass",',
79
+ ' "qualityVerdict": "pass",',
80
+ ' "reviewedFiles": ["' + mainDocRel + '"],',
81
+ ' "docHash": "<上面算出的 sha256 hex>",',
82
+ ' "checklist": [',
83
+ ' { "item": "<审查项>", "result": "pass", "note": "<证据/说明>" }',
84
+ ' ],',
85
+ ' "reviewerNotes": "<总结>"',
86
+ '}',
87
+ '```',
88
+ ]
89
+ return L.join('\n')
90
+ }
91
+
92
+ /**
93
+ * 计算文件内容的 sha256(用于 docHash 真实性校验,防 agent 凭空伪造 review.json)
94
+ * @param {string} filePath - 绝对路径
95
+ * @returns {string|null} sha256 hex;文件不存在返回 null
96
+ */
97
+ export function computeDocHash(filePath) {
98
+ if (!filePath || !existsSync(filePath)) return null
99
+ const content = readFileSync(filePath)
100
+ return createHash('sha256').update(content).digest('hex')
101
+ }
102
+
103
+ /**
104
+ * 校验文档型 stage review.json 的 schema
105
+ *
106
+ * 与 task-review.validateReviewSchema 的区别:
107
+ * - 不要求 git base/head(证据形态不同)
108
+ * - 改要求 reviewedFiles(被审查文档路径数组)+ docHash(主文档内容 sha256)
109
+ * - 新增 reviewType(design/plan/proposal/code/acceptance)+ checklist
110
+ * - 复用 VALID_VERDICTS 三态 + cannot_verify 联动规则(task-review.js:65-69)
111
+ *
112
+ * @param {object} review - 解析后的 JSON 对象
113
+ * @returns {{ ok: boolean, errors: string[] }}
114
+ */
115
+ export function validateStageReviewSchema(review) {
116
+ const errors = []
117
+ if (!review || typeof review !== 'object') {
118
+ return { ok: false, errors: ['stage review.json 不是有效 JSON 对象'] }
119
+ }
120
+
121
+ if (review.schemaVersion !== REVIEW_SCHEMA_VERSION) {
122
+ errors.push(`schemaVersion 应为 ${REVIEW_SCHEMA_VERSION},实际为 ${review.schemaVersion}`)
123
+ }
124
+
125
+ if (!STAGE_REVIEW_TYPES.includes(review.reviewType)) {
126
+ errors.push(`reviewType 无效:${review.reviewType}(应为 ${STAGE_REVIEW_TYPES.join('/')})`)
127
+ }
128
+
129
+ if (!VALID_VERDICTS.includes(review.specVerdict)) {
130
+ errors.push(`specVerdict 无效:${review.specVerdict}(应为 ${VALID_VERDICTS.join('/')})`)
131
+ }
132
+ if (!VALID_VERDICTS.includes(review.qualityVerdict)) {
133
+ errors.push(`qualityVerdict 无效:${review.qualityVerdict}(应为 ${VALID_VERDICTS.join('/')})`)
134
+ }
135
+
136
+ // cannot_verify 必须提供非空 requiredEvidence(与 task-review:65-69 一致,反逃逸)
137
+ if (review.specVerdict === 'cannot_verify' || review.qualityVerdict === 'cannot_verify') {
138
+ if (!Array.isArray(review.requiredEvidence) || review.requiredEvidence.length === 0) {
139
+ errors.push('cannot_verify 的 verdict 必须提供非空的 requiredEvidence 数组')
140
+ }
141
+ }
142
+
143
+ // reviewedFiles 必须是非空数组
144
+ if (!Array.isArray(review.reviewedFiles) || review.reviewedFiles.length === 0) {
145
+ errors.push('缺少 reviewedFiles 字段(应为被审查文档路径数组,[0] 为主审查对象,docHash 对应它)')
146
+ }
147
+
148
+ // docHash 非空
149
+ if (!review.docHash || typeof review.docHash !== 'string') {
150
+ errors.push('缺少 docHash 字段(被审查主文档内容的 sha256)')
151
+ }
152
+
153
+ // checklist 若提供,校验每项结构
154
+ if (review.checklist !== undefined) {
155
+ if (!Array.isArray(review.checklist)) {
156
+ errors.push('checklist 若提供必须是数组')
157
+ } else {
158
+ for (let i = 0; i < review.checklist.length; i++) {
159
+ const item = review.checklist[i]
160
+ if (!item || typeof item !== 'object' || !item.item) {
161
+ errors.push(`checklist[${i}] 缺少 item 描述`)
162
+ continue
163
+ }
164
+ if (item.result !== undefined && !CHECKLIST_RESULTS.includes(item.result)) {
165
+ errors.push(`checklist[${i}].result 无效:${item.result}(应为 ${CHECKLIST_RESULTS.join('/')})`)
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+ return { ok: errors.length === 0, errors }
172
+ }
173
+
174
+ /**
175
+ * docHash 真实性交叉校验:review 声称审查了某文档,重算该文档 sha256 对比。
176
+ *
177
+ * 对应 task-review.verifyReviewGitEvidence 的"防伪造"职责——agent 不能凭空编一份
178
+ * review.json 贴个假 hash。约定 docHash 对应 reviewedFiles[0](主审查对象)。
179
+ *
180
+ * searchDirs 依次尝试解析 reviewedFiles[0] 的相对路径(effectiveSpecBase / changeDir / cwd)。
181
+ * 主文档在所有基准下都找不到时 fail-closed 阻断——连主审查文档都定位不到,要么 reviewedFiles[0]
182
+ * 是伪造路径(防 agent 填假路径跳过校验),要么路径基准严重错位,两者都不该静默放行。
183
+ *
184
+ * @param {object} review - 已通过 schema 校验
185
+ * @param {string[]} searchDirs - 解析相对路径的候选基准目录(绝对路径)
186
+ * @returns {{ ok: boolean, errors: string[], warnings: string[] }}
187
+ */
188
+ export function verifyStageReviewDocHash(review, searchDirs) {
189
+ const errors = []
190
+ const warnings = []
191
+ const files = Array.isArray(review.reviewedFiles) ? review.reviewedFiles : []
192
+ if (files.length === 0) {
193
+ return { ok: false, errors: ['reviewedFiles 为空,无法做 docHash 校验'], warnings }
194
+ }
195
+
196
+ const primaryRel = files[0]
197
+
198
+ for (const base of searchDirs || []) {
199
+ const abs = join(base, primaryRel)
200
+ if (!existsSync(abs)) continue
201
+ // 找到主文档,比对 hash。容忍「原始字节」与「LF 规范化」两种口径:Windows 下 CRLF/LF 在
202
+ // git add / eol 规范化前后字节会漂移,同一文件 sha256 偶发不一致(坑 worktree-execute-apply-friction
203
+ // 坑3)——agent 写的 docHash(原始 sha256sum)与 gate 重算只要任一口径匹配即认可,消除 CRLF
204
+ // 漂移导致的反复「疑似伪造」误报。两口径都对不上仍判不匹配(防伪造能力不降级)。
205
+ const content = readFileSync(abs, 'utf8')
206
+ const actualRaw = createHash('sha256').update(content).digest('hex')
207
+ const actualLf = createHash('sha256').update(content.replace(/\r\n/g, '\n')).digest('hex')
208
+ const claimed = String(review.docHash).toLowerCase()
209
+ if (claimed !== actualRaw.toLowerCase() && claimed !== actualLf.toLowerCase()) {
210
+ errors.push(
211
+ `docHash 与主审查文档 ${primaryRel} 的实际内容不匹配。` +
212
+ `若刚改过该文档(如 design 修订后忘重算),重算并更新 review.json 的 docHash 字段:` +
213
+ `在基准目录 ${base} 下运行 sha256sum "${primaryRel}" 取首列 hex 填入;` +
214
+ `若未改文档,则 review.json 疑似伪造(未真正读取文档)。`
215
+ )
216
+ return { ok: false, errors, warnings }
217
+ }
218
+ return { ok: true, errors, warnings }
219
+ }
220
+
221
+ // 所有基准都找不到主文档 → fail-closed。连主审查文档都定位不到,说明 reviewedFiles[0] 要么
222
+ // 是伪造路径(恶意 agent 填假路径跳过 docHash 校验),要么路径基准严重错位——两者都该阻断
223
+ // 而非静默放行(历史降级 warning 是可被利用的伪造通道)。searchDirs =
224
+ // [effectiveSpecBase(.sillyspec), reviewChangeDir(changes/<change>), cwd],契约规定的
225
+ // reviewedFiles[0](changes/<change>/<mainDoc> 或 <mainDoc>)在这三个基准下必命中其一。
226
+ errors.push('主审查文档 ' + primaryRel + ' 在 ' + (searchDirs || []).length + ' 个候选基准目录下均不存在 — 无法做 docHash 校验(reviewedFiles[0] 路径伪造或基准错位)')
227
+ return { ok: false, errors, warnings }
228
+ }
229
+
230
+ /**
231
+ * 生成 stage review run id
232
+ * @returns {string} 如 'review-2026-07-16-143000'
233
+ */
234
+ export function generateStageReviewRunId() {
235
+ const now = new Date()
236
+ const pad = (n) => String(n).padStart(2, '0')
237
+ return `review-${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
238
+ }
239
+
240
+ /**
241
+ * stage review marker 文件路径(prompt 渲染时写 / getLatestStageReviewRunId 读,同源)。
242
+ *
243
+ * 对齐 execute 的 current-execute-run-id-<change> 语义:含 change 防多 change 串台
244
+ * (同一 stage 不同变更的 review 各自独立 marker);changeName 缺失时退化为按 stage 分。
245
+ *
246
+ * @param {string} runtimeRoot - .runtime 绝对路径
247
+ * @param {string} stage - brainstorm|plan|execute
248
+ * @param {string} [changeName] - 变更名(防多 change 串台)
249
+ * @returns {string} marker 文件绝对路径
250
+ */
251
+ export function stageReviewMarkerPath(runtimeRoot, stage, changeName) {
252
+ const suffix = changeName ? `${stage}-${changeName}` : stage
253
+ return join(runtimeRoot, `current-stage-review-run-id-${suffix}`)
254
+ }
255
+
256
+ /**
257
+ * 获取最新的 stage review run id
258
+ *
259
+ * 优先读 marker 文件(prompt 渲染 {REVIEW_TIER} 时写入,保证 gate 取的 ID == 注入给 agent
260
+ * 的 ID —— 修复「prompt 多次渲染 / 多次 review 时 gate 取错 ID 读错 review.json」),
261
+ * 对齐 getLatestExecuteRunId(task-review.js:420-428)。marker 缺失时 fallback 扫描
262
+ * stage-reviews/<stage>-review-* 目录取字典序最新(向后兼容无 marker 旧数据)。
263
+ *
264
+ * @param {string} runtimeRoot - .runtime 绝对路径
265
+ * @param {string} stage - brainstorm|plan|execute
266
+ * @param {string} [changeName] - 变更名(读对应 marker,防多 change 串台)
267
+ * @returns {string|null} run id(如 'review-2026-07-16-143000'),无则 null
268
+ */
269
+ export function getLatestStageReviewRunId(runtimeRoot, stage, changeName) {
270
+ // 优先读 marker(prompt 写 / gate 读同源,对齐 execute current-execute-run-id 机制)
271
+ try {
272
+ const markerPath = stageReviewMarkerPath(runtimeRoot, stage, changeName)
273
+ if (existsSync(markerPath)) {
274
+ const content = readFileSync(markerPath, 'utf8').trim()
275
+ // marker 内容格式校验(execute 复盘 b2):必须 review- 前缀(generateStageReviewRunId 格式)。
276
+ // agent 手写 marker 可能误填 execute 的 exec- 前缀 → 按 stage-reviews/<stage>-<runId> 拼目录必找不到,
277
+ // 与其读坏 ID 报误导错误,不如显式 warn 并退回目录扫描。
278
+ if (content) {
279
+ if (/^review-/.test(content)) return content
280
+ console.warn(`⚠️ stage-review marker ${markerPath} 内容 "${content.slice(0, 40)}" 不是 review- 前缀(应为 generateStageReviewRunId 格式,勿填 execute 的 exec- ID),忽略并退回目录扫描`)
281
+ }
282
+ }
283
+ } catch {}
284
+
285
+ // fallback:扫描 stage-reviews/<stage>-review-* 目录取字典序最新(向后兼容无 marker 旧数据)
286
+ const dir = join(runtimeRoot, 'stage-reviews')
287
+ if (!existsSync(dir)) return null
288
+ try {
289
+ const entries = readdirSync(dir, { withFileTypes: true })
290
+ .filter(e => e.isDirectory() && e.name.startsWith(`${stage}-review-`))
291
+ .map(e => e.name)
292
+ .sort()
293
+ .reverse()
294
+ if (!entries[0]) return null
295
+ // cross-change 防护(execute 复盘 b1):changeName 提供时按 review.json 的 reviewedFiles[0] 归属过滤。
296
+ // 契约 reviewedFiles[0] = `changes/<change>/<mainDoc>`(renderReviewJsonContract),据此排除他变更的 review,
297
+ // 否则批量完成 marker 缺失时可能读到 proxy 等其他变更的 acceptance review 报错误导。
298
+ if (changeName) {
299
+ const matching = entries.find(e => {
300
+ try {
301
+ const rj = JSON.parse(readFileSync(join(dir, e, 'review.json'), 'utf8'))
302
+ const r0 = Array.isArray(rj.reviewedFiles) ? String(rj.reviewedFiles[0]) : ''
303
+ return r0.includes(`changes/${changeName}/`)
304
+ } catch { return false }
305
+ })
306
+ if (matching) return matching.slice(stage.length + 1)
307
+ console.warn(`⚠️ 无 ${stage} 的 review marker,且 stage-reviews/ 下无归属变更 ${changeName} 的 review(reviewedFiles 应含 changes/${changeName}/);为避免跨变更串台不再取最新,返回 null(fail-closed,由调用方报缺 review.json)`)
308
+ return null
309
+ }
310
+ // entries[0] = "plan-review-2026-...",剥掉 `${stage}-` 前缀得到 runId "review-2026-..."
311
+ return entries[0].slice(stage.length + 1)
312
+ } catch {
313
+ return null
314
+ }
315
+ }
316
+
317
+ /**
318
+ * stage-level review 总校验(brainstorm/plan/execute-acceptance 的 done gate)
319
+ *
320
+ * 规则(与 task-review.validateTaskReviews 对称):
321
+ * - review.json 缺失 → error 阻断
322
+ * - 解析失败 / schema 失败 → error 阻断
323
+ * - reviewType 与期望不符 → error(agent 复制模板未改)
324
+ * - specVerdict 或 qualityVerdict 为 fail → error 阻断
325
+ * - cannot_verify + 非空 requiredEvidence → warning
326
+ * - cannot_verify + 空 requiredEvidence → error(schema 已拦,双保险)
327
+ * - docHash 与主文档实际内容不符 → error(伪造)
328
+ *
329
+ * 调用方约定:先用 classifyReviewTier 判定 tier——tier=self 不调用本函数(直接放行),
330
+ * tier=independent 才调用。
331
+ *
332
+ * @param {object} opts
333
+ * @param {string} opts.stage - brainstorm|plan|execute
334
+ * @param {string} opts.reviewType - design|plan|proposal|code|acceptance
335
+ * @param {string} opts.runtimeRoot - .runtime 绝对路径(平台模式为 runtimeRoot)
336
+ * @param {string} opts.reviewRunId - stage review run id(review-<ts>)
337
+ * @param {string[]} [opts.searchDirs] - docHash 校验的候选基准目录
338
+ * @param {boolean} [opts.verifyDocHash=true] - 是否做 docHash 真实性校验
339
+ * @returns {{ ok: boolean, errors: string[], warnings: string[], review: object|null }}
340
+ */
341
+ export function validateStageReview(opts) {
342
+ const { stage, reviewType, runtimeRoot, reviewRunId, searchDirs = [], verifyDocHash = true } = opts
343
+ const errors = []
344
+ const warnings = []
345
+
346
+ const reviewDir = join(runtimeRoot, 'stage-reviews', `${stage}-${reviewRunId}`)
347
+ const reviewPath = join(reviewDir, 'review.json')
348
+
349
+ if (!reviewRunId || !existsSync(reviewPath)) {
350
+ const errs = [`缺少 ${stage} 阶段的 stage review.json — tier=independent 要求独立审查子代理产出(期望路径:${reviewPath})`]
351
+ const typo = detectSpecDirTypo(runtimeRoot)
352
+ if (typo) errs.push(`💡 路径疑似拼错:发现 ${typo.typoDir} 目录(应为 ${typo.canonical}),review.json 可能误存于此——检查是否把 .sillyspec 拼成了变体`)
353
+ return {
354
+ ok: false,
355
+ errors: errs,
356
+ warnings,
357
+ review: null,
358
+ }
359
+ }
360
+
361
+ let raw
362
+ try {
363
+ raw = readFileSync(reviewPath, 'utf8')
364
+ } catch (e) {
365
+ return { ok: false, errors: [`${stage} review.json 读取失败: ${e.message}`], warnings, review: null }
366
+ }
367
+
368
+ let parsed
369
+ try {
370
+ parsed = JSON.parse(raw)
371
+ } catch (e) {
372
+ return { ok: false, errors: [`${stage} review.json 解析失败: ${e.message}`], warnings, review: null }
373
+ }
374
+
375
+ const schemaResult = validateStageReviewSchema(parsed)
376
+ if (!schemaResult.ok) {
377
+ return { ok: false, errors: [`${stage} review.json schema 校验失败 — ${schemaResult.errors.join('; ')}`], warnings, review: parsed }
378
+ }
379
+
380
+ // reviewType 与期望一致性
381
+ if (parsed.reviewType && parsed.reviewType !== reviewType) {
382
+ errors.push(`${stage} review.json 的 reviewType 为 "${parsed.reviewType}",与期望 "${reviewType}" 不符 — agent 可能复制模板未修改`)
383
+ }
384
+
385
+ // fail verdict
386
+ if (parsed.specVerdict === 'fail' || parsed.qualityVerdict === 'fail') {
387
+ errors.push(`${stage} review 未通过 — spec: ${parsed.specVerdict}, quality: ${parsed.qualityVerdict}`)
388
+ if (parsed.reviewerNotes) errors.push(`${stage}: ${parsed.reviewerNotes}`)
389
+ }
390
+
391
+ // docHash 真实性(fail verdict 已错时跳过,避免噪声)
392
+ if (errors.length === 0 && verifyDocHash && searchDirs.length > 0) {
393
+ const dh = verifyStageReviewDocHash(parsed, searchDirs)
394
+ for (const w of dh.warnings) warnings.push(`${stage}: ${w}`)
395
+ if (!dh.ok) {
396
+ for (const err of dh.errors) errors.push(`${stage}: ${err}`)
397
+ }
398
+ }
399
+
400
+ // cannot_verify → warning(requiredEvidence 非空已由 schema 保证)
401
+ if (errors.length === 0 && (parsed.specVerdict === 'cannot_verify' || parsed.qualityVerdict === 'cannot_verify')) {
402
+ const verdicts = []
403
+ if (parsed.specVerdict === 'cannot_verify') verdicts.push('spec')
404
+ if (parsed.qualityVerdict === 'cannot_verify') verdicts.push('quality')
405
+ warnings.push(`${stage}: ${verdicts.join('+')}=cannot_verify,requiredEvidence 需在后续阶段满足`)
406
+ }
407
+
408
+ return { ok: errors.length === 0, errors, warnings, review: parsed }
409
+ }
410
+
411
+ /**
412
+ * 打印 stage review 校验结果
413
+ * @param {{ ok: boolean, errors: string[], warnings: string[] }} result
414
+ * @param {object} context - { stage, reviewRunId } 用于错误提示路径
415
+ */
416
+ export function printStageReviewResult(result, context = {}) {
417
+ const { stage, reviewRunId, runtimeRoot } = context
418
+ if (result.ok && result.warnings.length === 0) {
419
+ console.log(`\n✅ Stage Review Gate — ${stage} 阶段独立审查通过`)
420
+ return
421
+ }
422
+
423
+ if (result.errors.length > 0) {
424
+ console.error(`\n🚫 Stage Review Gate — ${stage} FAILED`)
425
+ for (const err of result.errors) {
426
+ console.error(` - ${err}`)
427
+ }
428
+ // echo 完整 review.json 路径:runId + marker 已由 CLI 自动生成写入(prompt.js review step
429
+ // 渲染 / gate 触发),agent 直接派独立子代理把 review.json 写到此路径,勿手算 runId、勿手写
430
+ // marker(历史坑:agent 撞 gate 后自己算 runId 猜错 review- 前缀,填 exec-/裸 ts 被 CLI 忽略)。
431
+ if (reviewRunId) {
432
+ const reviewDir = runtimeRoot ? `${runtimeRoot}/stage-reviews/${stage}-${reviewRunId}` : `.sillyspec/.runtime/stage-reviews/${stage}-${reviewRunId}`
433
+ console.error(`\n 📁 review.json 直接写此路径(runId/marker CLI 已生成写入,勿手算 runId、勿手写 marker):`)
434
+ console.error(` ${reviewDir}/review.json`)
435
+ }
436
+ console.error(`\n 提示:tier=independent 要求独立审查子代理产出 review.json,补全后重新 --done`)
437
+ console.error(` 可用 sillyspec register-stage-review --change <名> --stage ${stage} [--from <已有review.json>] 一步生成 run 目录 + review.json 骨架(docHash 自动算)+ 写 marker + 自检,省掉手动建目录/写 marker`)
438
+ }
439
+
440
+ if (result.warnings.length > 0) {
441
+ console.warn(`\n⚠️ Stage Review Gate — ${stage} WARNING`)
442
+ for (const w of result.warnings) {
443
+ console.warn(` - ${w}`)
444
+ }
445
+ }
446
+ }
447
+
448
+ /**
449
+ * 注册一个 stage 级 review(确定性 writer)—— 生成 run 目录 + review.json 骨架(或 adopt
450
+ * agent 草稿)+ 写 marker,治 tier=independent 时「调度者手动派独立子代理 → marker 不写 →
451
+ * gate 取错 run」的死锁。与 task 级 generateTaskReviewDrafts(task-review.js)对称。
452
+ *
453
+ * docHash 由 CLI 直接 computeDocHash 算(命令有文件访问权),消除 agent 手算 hash 易错
454
+ * (部分实现 P6.1b defer:仅本 scaffold 路径确定性,不改 agent 自写 review.json 的 hash 链路)。
455
+ *
456
+ * 纯新增,不改 validateStageReview / getLatestStageReviewRunId / renderReviewJsonContract 等
457
+ * 现有函数;self-check 只验 mechanics(schema + docHash),verdict 留给 Stage Review Gate 判。
458
+ *
459
+ * @param {object} opts
460
+ * @param {string} opts.changeName - 变更名
461
+ * @param {string} opts.stage - brainstorm|plan|execute
462
+ * @param {string} [opts.fromFile] - adopt 模式:agent 已写的 review.json(保留 verdict/checklist,重算 docHash + 规范化 reviewedFiles[0])
463
+ * @param {string} opts.cwd - 主仓 cwd(解析 specBase)
464
+ * @param {object} [opts.platformOpts] - 平台选项(specRoot 等)
465
+ * @returns {{ ok: boolean, reviewRunId: string, reviewPath: string, markerPath: string, mode: 'skeleton'|'adopted', mainDoc: string, review: object }}
466
+ * @throws {Error} changeName 空 / stage 非法 / 主文档缺失 / --from 不存在或 schema 不过
467
+ */
468
+ export function registerStageReview({ changeName, stage, fromFile, cwd, platformOpts = {} }) {
469
+ if (!changeName) throw new Error('register-stage-review: changeName 不能为空')
470
+ if (!stage || !STAGE_MAIN_DOC[stage]) {
471
+ throw new Error(`register-stage-review: stage 无效 "${stage}"(应为 brainstorm|plan|execute)`)
472
+ }
473
+
474
+ const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
475
+ const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
476
+ const changeDir = join(specBase, 'changes', changeName)
477
+ const reviewType = STAGE_REVIEW_TYPE[stage]
478
+ const mainDoc = STAGE_MAIN_DOC[stage]
479
+ const mainDocPath = join(changeDir, mainDoc)
480
+ if (!existsSync(mainDocPath)) {
481
+ throw new Error(`register-stage-review: 主审查文档不存在 ${mainDocPath}(${stage} 审 ${mainDoc}),无法算 docHash`)
482
+ }
483
+ const docHash = computeDocHash(mainDocPath)
484
+ if (!docHash) {
485
+ throw new Error(`register-stage-review: 计算 docHash 失败(computeDocHash 返回 null):${mainDocPath}`)
486
+ }
487
+ const reviewedFiles = [`changes/${changeName}/${mainDoc}`]
488
+
489
+ let review
490
+ let mode
491
+ if (fromFile) {
492
+ // adopt 模式:保留 agent 的 verdict/checklist/reviewerNotes/requiredEvidence,仅修 mechanics
493
+ const fromAbs = existsSync(fromFile) ? fromFile : join(cwd, fromFile)
494
+ if (!existsSync(fromAbs)) {
495
+ throw new Error(`register-stage-review: --from 文件不存在 ${fromFile}(相对 cwd ${fromAbs} 也未命中)`)
496
+ }
497
+ let parsed
498
+ try {
499
+ parsed = JSON.parse(readFileSync(fromAbs, 'utf8'))
500
+ } catch (e) {
501
+ throw new Error(`register-stage-review: --from 文件 JSON 解析失败 ${fromAbs}: ${e.message}`)
502
+ }
503
+ const schemaResult = validateStageReviewSchema(parsed)
504
+ if (!schemaResult.ok) {
505
+ throw new Error(`register-stage-review: --from 文件 schema 校验失败 — ${schemaResult.errors.join('; ')}`)
506
+ }
507
+ review = { ...parsed, docHash, reviewedFiles }
508
+ mode = 'adopted'
509
+ } else {
510
+ // 骨架模式:cannot_verify 待审占位(schema 反逃逸:cannot_verify 必须带非空 requiredEvidence)
511
+ review = {
512
+ schemaVersion: REVIEW_SCHEMA_VERSION,
513
+ reviewType,
514
+ specVerdict: 'cannot_verify',
515
+ qualityVerdict: 'cannot_verify',
516
+ reviewedFiles,
517
+ docHash,
518
+ requiredEvidence: [`待独立审查子代理对照 ${mainDoc} 逐节核验(骨架由 register-stage-review 生成)`],
519
+ reviewerNotes: '骨架由 register-stage-review 生成,verdict 待独立审查子代理填写',
520
+ }
521
+ mode = 'skeleton'
522
+ }
523
+
524
+ const reviewRunId = generateStageReviewRunId()
525
+ const reviewDir = join(runtimeRoot, 'stage-reviews', `${stage}-${reviewRunId}`)
526
+ const reviewPath = join(reviewDir, 'review.json')
527
+ mkdirSync(reviewDir, { recursive: true })
528
+ writeFileSync(reviewPath, JSON.stringify(review, null, 2) + '\n')
529
+
530
+ const markerPath = stageReviewMarkerPath(runtimeRoot, stage, changeName)
531
+ if (existsSync(markerPath)) {
532
+ console.warn(`⚠️ register-stage-review: marker 已存在 ${markerPath},将被覆盖为 ${reviewRunId}`)
533
+ }
534
+ mkdirSync(runtimeRoot, { recursive: true })
535
+ writeFileSync(markerPath, reviewRunId + '\n')
536
+
537
+ // self-check(fail-closed):刚写的 review 必过 schema + docHash 真实性。只验 mechanics,
538
+ // 不判 verdict(verdict 是 agent/子代理的审查结论,even fail 也如实落盘,由 Stage Review Gate 裁决)。
539
+ const schemaRecheck = validateStageReviewSchema(review)
540
+ if (!schemaRecheck.ok) {
541
+ throw new Error(`register-stage-review: 写入后 schema 自检失败(不应发生)— ${schemaRecheck.errors.join('; ')}`)
542
+ }
543
+ const hashRecheck = verifyStageReviewDocHash(review, [specBase, changeDir, cwd])
544
+ if (!hashRecheck.ok) {
545
+ throw new Error(`register-stage-review: 写入后 docHash 自检失败(不应发生)— ${hashRecheck.errors.join('; ')}`)
546
+ }
547
+
548
+ return { ok: true, reviewRunId, reviewPath, markerPath, mode, mainDoc, review }
549
+ }