sillyspec 3.29.4 → 3.31.0

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 (148) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +1 -1
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
  3. package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
  4. package/.claude/skills/sillyspec-flow/SKILL.md +57 -0
  5. package/.claude/skills/sillyspec-quick/SKILL.md +7 -105
  6. package/AGENTS.md +38 -20
  7. package/SKILL.md +9 -3
  8. package/assets/command-cards/flow.md +32 -0
  9. package/assets/command-cards/run-archive.md +26 -0
  10. package/assets/command-cards/run-brainstorm.md +33 -0
  11. package/assets/command-cards/run-execute.md +29 -0
  12. package/assets/command-cards/run-plan.md +27 -0
  13. package/assets/command-cards/run-quick.md +17 -0
  14. package/assets/command-cards/run-verify.md +29 -0
  15. package/assets/command-cards/status.md +27 -0
  16. package/package.json +2 -1
  17. package/packages/dashboard/server/executor.js +2 -1
  18. package/packages/dashboard/server/parser.js +7 -7
  19. package/r7-test-env/README-R7-TEST.md +93 -0
  20. package/r7-test-env/demo-project/lib/calc.js +6 -0
  21. package/r7-test-env/demo-project/package.json +1 -0
  22. package/r7-test-env/demo-project/test.js +10 -0
  23. package/r7-test-env/prompt-R7-SMOKE.md +41 -0
  24. package/r7-test-env/sillyspec-r7.cmd +3 -0
  25. package/round4/account.mjs +98 -0
  26. package/round4/b1-analyze-task08.mjs +40 -0
  27. package/round4/b1-b1-target-validation.md +32 -0
  28. package/round4/b3a-prompt-audit-result.md +66 -0
  29. package/round4/b3a-prompt-audit.mjs +89 -0
  30. package/round4/brief-autocompact.md +34 -0
  31. package/round4/brief-session-replay.md +50 -0
  32. package/round4/deep-read-synthesis.md +38 -0
  33. package/round4/extract-full.mjs +38 -0
  34. package/round4/extract-timeline.mjs +43 -0
  35. package/round4/find-sessions.mjs +13 -0
  36. package/round4/full-r4-l.md +4707 -0
  37. package/round4/full-r4-o-l.md +4300 -0
  38. package/round4/full-r4-os.md +1681 -0
  39. package/round4/full-r4-sf.md +4255 -0
  40. package/round4/full-r4-sq.md +1734 -0
  41. package/round4/gate-audit-relay-receipt.md +30 -0
  42. package/round4/gate-value-audit.md +52 -0
  43. package/round4/optimization-plan.md +165 -0
  44. package/round4/prereg.md +67 -0
  45. package/round4/prompt-R4-L.md +63 -0
  46. package/round4/prompt-R4-O-L.md +59 -0
  47. package/round4/prompt-R4-O-S.md +56 -0
  48. package/round4/prompt-R4-S-F.md +60 -0
  49. package/round4/prompt-R4-S-Q.md +66 -0
  50. package/round4/r4-final-report.html +347 -0
  51. package/round4/r4-os-timeline.txt +297 -0
  52. package/round4/r4-sf-timeline.txt +738 -0
  53. package/round4/r4-sq-timeline.txt +322 -0
  54. package/round4/r4l-timeline.txt +793 -0
  55. package/round4/r4ol-timeline.txt +729 -0
  56. package/round4/r4sf-timeline.txt +852 -0
  57. package/round4/r5-scenario-matrix.md +46 -0
  58. package/round4/result-S.md +171 -0
  59. package/round4/result-final.md +46 -0
  60. package/round4/result-quality.md +63 -0
  61. package/src/change-list.js +9 -0
  62. package/src/classify-change.js +6 -0
  63. package/src/command-cards.js +74 -0
  64. package/src/commit-guard.js +2 -0
  65. package/src/config-schema.js +64 -12
  66. package/src/contract-matrix.js +26 -5
  67. package/src/db.js +35 -4
  68. package/src/decision-distill.js +9 -3
  69. package/src/diagnostic-codes.js +2 -1
  70. package/src/docs-check.js +1 -1
  71. package/src/doctor-diagnostics.js +44 -2
  72. package/src/flow-draft.js +748 -0
  73. package/src/flow-parity.js +236 -0
  74. package/src/flow-review.js +184 -0
  75. package/src/flow.js +1492 -0
  76. package/src/foreign-declared.js +17 -1
  77. package/src/fr-index.js +396 -22
  78. package/src/fs-atomic.js +5 -1
  79. package/src/git-helper.js +2 -2
  80. package/src/greenfield-bootstrap.js +98 -0
  81. package/src/handoff.js +120 -7
  82. package/src/hunk-attribution.js +191 -0
  83. package/src/index.js +602 -21
  84. package/src/init.js +25 -8
  85. package/src/knowledge-classify.js +47 -1
  86. package/src/knowledge-digest.js +162 -0
  87. package/src/knowledge-quicklog.js +246 -0
  88. package/src/machine-draft.js +128 -0
  89. package/src/machine-interface.js +194 -7
  90. package/src/mcp-server.js +1 -0
  91. package/src/module-impact.js +51 -22
  92. package/src/module-resolve.js +3 -1
  93. package/src/preview-progress.js +160 -0
  94. package/src/progress/change-registry.js +11 -3
  95. package/src/progress/shared.js +3 -2
  96. package/src/progress.js +56 -13
  97. package/src/quicklog.js +44 -6
  98. package/src/redomain.js +147 -0
  99. package/src/review-material-pack.js +204 -1
  100. package/src/run/archive-distill.js +4 -2
  101. package/src/run/bg-sync.js +67 -0
  102. package/src/run/command.js +127 -10
  103. package/src/run/complete-handlers.js +363 -73
  104. package/src/run/complete.js +356 -74
  105. package/src/run/gate-snapshot-ledger.js +213 -0
  106. package/src/run/gate-snapshot.js +260 -38
  107. package/src/run/gates.js +482 -268
  108. package/src/run/green-cache.js +123 -0
  109. package/src/run/junction-rm.js +70 -0
  110. package/src/run/next.js +14 -0
  111. package/src/run/prompt.js +185 -55
  112. package/src/run/quick-audit.js +213 -14
  113. package/src/run/shared.js +123 -4
  114. package/src/run/stage.js +182 -216
  115. package/src/run/test-ledger.js +264 -0
  116. package/src/run/verify-quality-scan.js +84 -7
  117. package/src/scope-audit.js +6 -1
  118. package/src/sentinel-assertions.js +114 -0
  119. package/src/setup.js +3 -3
  120. package/src/spec-sync.js +41 -1
  121. package/src/stage-contract.js +26 -8
  122. package/src/stage-review-checklist.js +2 -0
  123. package/src/stage-review.js +7 -2
  124. package/src/stage-templates.js +157 -0
  125. package/src/stages/archive.js +2 -0
  126. package/src/stages/brainstorm.js +24 -106
  127. package/src/stages/execute.js +222 -85
  128. package/src/stages/knowledge.js +74 -1
  129. package/src/stages/plan-postcheck.js +385 -20
  130. package/src/stages/plan.js +23 -1
  131. package/src/stages/quick.js +10 -2
  132. package/src/stages/verify.js +4 -2
  133. package/src/sync.js +81 -23
  134. package/src/task-done.js +181 -0
  135. package/src/task-review.js +8 -55
  136. package/src/taskcard-frontmatter.js +57 -0
  137. package/src/test-bindings.js +549 -0
  138. package/src/timeline.js +218 -0
  139. package/src/ui-visual.js +175 -0
  140. package/src/verify-draft.js +214 -0
  141. package/src/verify-postcheck.js +919 -159
  142. package/src/verify-probes.js +103 -25
  143. package/src/watcher.js +1031 -0
  144. package/src/worktree-apply.js +44 -4
  145. package/src/worktree-deps.js +22 -8
  146. package/src/worktree-policy.js +42 -0
  147. package/src/worktree.js +102 -15
  148. package/templates/agents-instruction.md +37 -18
package/src/run/gates.js CHANGED
@@ -3,7 +3,9 @@
3
3
  *
4
4
  * 阶段完成校验 gate 级联 + execute deps 硬门 + 完成回滚(自洽叶子模块,仅被 completeStep 调用):
5
5
  * - runStageCompletionGates:runValidators → verify-test 对账 → Plan→Execute contract →
6
- * Stage Review Gate → Execute Task Review Gate,任一失败走 rollbackCompletionAndReturn;
6
+ * Stage Review Gate,任一失败走 rollbackCompletionAndReturn(Execute Task Review Gate
7
+ * 已随 2026-09-26-task-review-retire 退役——R18 实证任务粒度评审在无独立评审者供给时
8
+ * 全降形式合规、实质拦截为零);
7
9
  * 级联末位追仪式档位摩擦升档检查点(2026-09-18-ceremony-risk-pricing task-03:读
8
10
  * friction-ledger 累计账 → escalateByFriction → .runtime/ceremony-tier-<change>.json
9
11
  * 迁移留痕,只升不降、fail-soft 不阻断完成);verify 检查族末位接 ceremony 双跑收口第一
@@ -33,6 +35,37 @@ import { writeAtomicSync } from '../fs-atomic.js'
33
35
  import { triggerSync, resolveChangeDir, resolveRuntimeRoot } from './shared.js'
34
36
  import { runValidators } from '../stage-contract.js'
35
37
  import { handleScanStageCompleted, handleExecuteWorktreeCleanup } from './complete-handlers.js'
38
+
39
+ /**
40
+ * 评审豁免凭据(2026-09-26-review-unsupervised-exit,R18-full 实证驱动):无嵌套派发能力的
41
+ * 环境(子代理不能再派子代理)里,阶段评审只能降级自审——三份自审 review.json 全 PASS 零信息
42
+ * 量纯表演,门仍硬拦形式合规(R18-full 15 次拦截里 5 次是 review 面形式)。诚实出口:agent 写
43
+ * 变更目录 review-unsupervised.md(一行声明:环境无嵌套派发能力+时间,须含 unsupervised 字样)
44
+ * → Stage Review Gate / doctor align 门两点共吃(Task Review 层与 --done review.json 硬门
45
+ * 已随 2026-09-26-task-review-retire 退役):有效
46
+ * review.json ‖ 豁免声明二选一放行(warn+遥测,豁免率可观测);两凭据皆无照旧 fail-closed。
47
+ * 真独立评审(有派发能力)不受影响——豁免是声明式自曝,不是绕门。
48
+ */
49
+ export function readReviewUnsupervisedWaiver(changeDir) {
50
+ try {
51
+ if (!changeDir) return null
52
+ const p = join(changeDir, 'review-unsupervised.md')
53
+ if (!existsSync(p)) return null
54
+ const t = readFileSync(p, 'utf8')
55
+ return /unsupervised/i.test(t) ? t.trim().slice(0, 200) : null
56
+ } catch {
57
+ return null
58
+ }
59
+ }
60
+
61
+ /** 豁免放行的统一回显+遥测(调用点持 runtimeRoot/specBase)。 */
62
+ async function waiveWithTelemetry({ changeName, gate, specBase }) {
63
+ console.warn(`⚠️ [评审豁免] review-unsupervised.md 在场(${gate})——环境无嵌套派发能力,阶段评审降级留痕放行(不产自审 review.json 不表演);声明随归档留痕,可经遥测观测豁免率`)
64
+ try {
65
+ const { appendKnowledgeHit } = await import('../knowledge-hits.js')
66
+ appendKnowledgeHit(join(specBase, '.runtime'), { type: 'review-unsupervised-escape', change: changeName, gate, source: 'gates' })
67
+ } catch { /* 遥测 fail-soft */ }
68
+ }
36
69
  import { detectConcurrentChanges, formatConcurrentWarning, detectCommittedDrift, formatCommittedDriftWarning } from './concurrent-detect.js'
37
70
  import { stageRegistry } from '../stages/index.js'
38
71
  import { normalizeTaskId } from '../taskcard.js'
@@ -235,8 +268,9 @@ function isCurrentWaveAllNoDepsVerify(stepName, changeDir) {
235
268
  * 只读校验(不自动生成 marker/runId——恢复读场景缺失就是缺失,如实报告):
236
269
  * 1. Stage Review:tier=independent 时须存在有效 execute stage review.json(verdict 非 fail);
237
270
  * tier=self 放行(与正常 gate 分级一致)。
238
- * 2. Task Review:所有 task 的 review.json 齐备且 verdict 非 fail(runId 解析同 Task Review Gate:
239
- * marker → 含 tasks/ 目录扫描;不 generate)。
271
+ * 2. Task Review 段已退役(2026-09-26-task-review-retire:R18 实证任务粒度评审在无嵌套
272
+ * 派发环境全降自审表演、实质拦截为零——豁免通道不足以止损,层整体退役;残存 review.json
273
+ * 由 task-review.js 保留导出作历史兼容读侧)。
240
274
  *
241
275
  * @param {{ cwd: string, changeName: string, specBase: string, platformOpts?: object }} opts
242
276
  * @returns {Promise<boolean>} true=被门阻断(CLI 应放弃 align 并 exit 1);false=校验通过可继续
@@ -245,10 +279,16 @@ export async function enforceAlignExecuteReviewGate({ cwd, changeName, specBase,
245
279
  const effectiveSpecBase = platformOpts?.specRoot || specBase
246
280
  const runtimeRoot = resolveRuntimeRoot(platformOpts, effectiveSpecBase)
247
281
  const reviewChangeDir = resolveChangeDir(cwd, { currentChange: changeName }, platformOpts?.specRoot || null)
282
+ // 豁免凭据(review-unsupervised-exit):无嵌套派发能力环境两段校验整体降级留痕放行
283
+ if (readReviewUnsupervisedWaiver(reviewChangeDir)) {
284
+ await waiveWithTelemetry({ changeName, gate: 'doctor-align', specBase: effectiveSpecBase })
285
+ console.warn(' ℹ️ align 前置 review 门整体豁免(unsupervised 声明在场)——继续对齐')
286
+ return false
287
+ }
248
288
  const blocked = (msgs) => {
249
289
  console.error('\n❌ doctor --align-execute-progress 前置 review 校验未过——execute 完成审计不能绕过:')
250
290
  for (const m of msgs) console.error(' - ' + m)
251
- console.error(' 解法:补齐上述 review 后重跑 align(stage review:sillyspec register-stage-review --change ' + changeName + ' --stage execute;task review:按报错路径补 review.json)')
291
+ console.error(' 解法:补齐上述 review 后重跑 align(stage review:sillyspec register-stage-review --change ' + changeName + ' --stage execute)')
252
292
  return true
253
293
  }
254
294
 
@@ -285,36 +325,10 @@ export async function enforceAlignExecuteReviewGate({ cwd, changeName, specBase,
285
325
  return true
286
326
  }
287
327
 
288
- // 2. Task Review(runId 只读解析:marker → 含 tasks/ 扫描;不 generate)
289
- try {
290
- const { validateTaskReviews, printReviewResult, resolveLatestExecuteRunIdWithTasks, isValidExecuteRunId } = await import('../task-review.js')
291
- const planPath = reviewChangeDir ? join(reviewChangeDir, 'plan.md') : null
292
- if (!planPath || !existsSync(planPath)) {
293
- return blocked(['plan.md 不存在,Task Review 无从校验(align 的 checkbox 判定依赖同目录,疑似路径解析异常)'])
294
- }
295
- const planContent = readFileSync(planPath, 'utf8')
296
- const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
297
- let executeRunId = ''
298
- try {
299
- if (existsSync(runIdFile)) {
300
- const c = readFileSync(runIdFile, 'utf8').trim()
301
- if (c && isValidExecuteRunId(c)) executeRunId = c
302
- }
303
- } catch {}
304
- if (!executeRunId) {
305
- executeRunId = resolveLatestExecuteRunIdWithTasks({ runtimeRoot, changeName }) || ''
306
- }
307
- if (!executeRunId) {
308
- return blocked(['execute runId 无法定位(marker 缺失且 execute-runs/ 无含 review 的 run 目录)——15/N 份 task review 所在 run 不可寻,无法对账'])
309
- }
310
- const reviewResult = validateTaskReviews({ planContent, runtimeRoot, executeRunId, changeDir: reviewChangeDir, gitDir: cwd })
311
- printReviewResult(reviewResult, { runtimeRoot, executeRunId })
312
- if (!reviewResult.ok) return true // 报错明细已在 printReviewResult 输出
313
- console.log('✅ align 前置 review 校验通过(stage review + task review 均齐备),继续对齐')
314
- } catch (e) {
315
- console.error('❌ align 前置 Task Review 校验异常,阻断对齐: ' + e.message)
316
- return true
317
- }
328
+ // 2. Task Review 段已退役(2026-09-26-task-review-retire)——原「所有 task review.json 齐备
329
+ // 且 verdict 非 fail」校验随层退役删除;task 粒度审计改由 execute 收口的代码证据核验
330
+ // (checkExecuteCodeEvidence)+ verify 测试对账承担。
331
+ console.log('✅ align 前置 review 校验通过(stage review;task review 段已退役),继续对齐')
318
332
  return false
319
333
  }
320
334
 
@@ -342,7 +356,7 @@ export async function enforceDepsGate(stageName, cwd, changeName, step, steps, c
342
356
 
343
357
  // main 侧就绪后的跨仓 worktree deps 校验(坑 cross-repo-no-worktree-isolation):
344
358
  // 跨仓仓供给失败同样无构建/测试能力(前端仓无 node_modules 建不了 build)。只校验已建
345
- // worktree 的跨仓仓(legacy 直写模式无 meta,维持原语义,下游 Task Review 兜底)。
359
+ // worktree 的跨仓仓(legacy 直写模式无 meta,维持原语义)。
346
360
  // 放行返回 true;跨仓未就绪置 blocked + exit(1)。
347
361
  const crossCheckOrExit = async () => {
348
362
  let crossFailed = []
@@ -446,85 +460,11 @@ export async function enforceDepsGate(stageName, cwd, changeName, step, steps, c
446
460
  process.exit(1)
447
461
  }
448
462
 
449
- /**
450
- * execute --done 硬门:已勾 [x] task 的 review.json 必须 schema 完整(坑 review-json-field-gap)。
451
- *
452
- * Task Review Gate(validateTaskReviews)只在 execute 整阶段完成时跑(complete.js 阶段完成分支的
453
- * actualCompleted===actualTotal 守卫),单 task --done 不校验 → 子代理勾 checkbox 却漏写/漏字段
454
- * review.json,要到收尾才暴露,用户被迫事后批量补。本门提前到每次 --done:校验 plan 里所有已勾
455
- * task 的 review.json,缺字段/不存在/JSON 坏 → 置 step=blocked + exit(1),与 enforceDepsGate 同范式
456
- * (阻断前经 persist 回调落盘 blocked)。
457
- * 未勾 task 不校验(还没做)。平台模式/无 marker/无 plan 时放行(下游 Task Review Gate 兜底)。
458
- */
459
- export async function enforceReviewJsonGate(stageName, cwd, changeName, step, steps, currentIdx, specBase, platformOpts, persist) {
460
- if (stageName !== 'execute' || !changeName) return true
461
- // head 锡点自动落盘(2026-08-21 审计项③,D-010 补对称):跨仓 task base_commit 已在派发时
462
- // CLI 落盘,head_commit 此前靠主 agent 按 prompt 手跑 rev-parse 手写(漏抄/抄错炸 review gate)。
463
- // 每次 --done 时机幂等补齐(已存在不覆盖)。best-effort:失败只 warn,不阻断 --done 主流程。
464
- try {
465
- const { stampCrossRepoHeadCommits } = await import('../stages/execute.js')
466
- const stamped = await stampCrossRepoHeadCommits({ changeName, cwd, specBase, platformOpts })
467
- if (stamped && stamped.stamped > 0) {
468
- console.log(`📌 已自动落盘跨仓 task head_commit 锡点:${stamped.stampedTasks.join(', ')}(幂等,此后 --done 不再重复写)`)
469
- }
470
- } catch (e) {
471
- console.warn(`⚠️ head_commit 锡点自动落盘失败(降级留给 agent 手写): ${e.message}`)
472
- }
473
- const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
474
- const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
475
- const planPath = join(specBase, 'changes', changeName, 'plan.md')
476
- if (!existsSync(runIdFile) || !existsSync(planPath)) return true
477
- const { validateCheckedTaskReviews, resolveLatestExecuteRunIdWithTasks, isValidExecuteRunId } = await import('../task-review.js')
478
- // existsSync 与 read 之间 marker 可能被并发 cleanup/归档删除(多 agent 场景):读失败按
479
- //「marker 缺失」处理,走漂移兜底重定位,不让 ENOENT 冒成顶层 stack(与 :426-435 口径对齐)
480
- let executeRunId = ''
481
- try {
482
- executeRunId = readFileSync(runIdFile, 'utf8').trim()
483
- } catch {
484
- console.warn('⚠️ execute run marker 读取失败(可能被并发清理),改扫真实 run 目录')
485
- executeRunId = resolveLatestExecuteRunIdWithTasks({ runtimeRoot, changeName }) || ''
486
- }
487
- // marker 是 agent 可写内容:格式校验防注入/穿越,非法视为缺失走漂移兜底重定位
488
- if (executeRunId && !isValidExecuteRunId(executeRunId)) {
489
- console.warn(`⚠️ execute run marker 内容非法(期望 exec-YYYY-MM-DD-HHMMSS,实得 ${JSON.stringify(executeRunId.slice(0, 60))}),改扫真实 run 目录`)
490
- executeRunId = ''
491
- }
492
- // 2026-08-20-task-truth-unify:已勾 task 清单源迁 tasks.md(勾选唯一落点);tasks.md 缺失
493
- // 回退 planPath 内容(旧变更兼容读侧)。planPath 存在性已在上方把关。
494
- const tasksRegistryPath = join(specBase, 'changes', changeName, 'tasks.md')
495
- const planContent = existsSync(tasksRegistryPath)
496
- ? readFileSync(tasksRegistryPath, 'utf8')
497
- : readFileSync(planPath, 'utf8')
498
- // marker 读失败且重定位无果(无任何含 tasks/ 的 run):无 run 可校验,放行(下游 Task Review Gate 兜底)
499
- if (!executeRunId) return true
500
- // marker 漂移兜底(gate-atom-a 正确修法):marker 指向的 run 缺 tasks/(generateExecuteRunId 只写
501
- // marker 不建目录,漂移后新 run 不继承旧 review)时,无视 marker 改扫 execute-runs/ 取 mtime 最新
502
- // 且真正含 tasks/ 的 run,用其齐备的 review.json 校验,避免误报「review.json 不存在」。注意不能用
503
- // resolveLatestExecuteRunId——它见 marker 非空即原样返回(不校验目录),恰是本场景要绕开的值。
504
- if (executeRunId && !existsSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'))) {
505
- const relocated = resolveLatestExecuteRunIdWithTasks({ runtimeRoot, changeName })
506
- if (relocated && relocated !== executeRunId) {
507
- console.warn(`⚠️ execute run marker 漂移:${executeRunId} 无 tasks/,改用真实含 review 的 run ${relocated}`)
508
- executeRunId = relocated
509
- }
510
- }
511
- const result = validateCheckedTaskReviews({ planContent, runtimeRoot, executeRunId })
512
- if (result.ok) return true
513
- if (steps && steps[currentIdx]) {
514
- steps[currentIdx].status = 'blocked'
515
- steps[currentIdx].blockReason = 'review.json 字段校验阻断:补齐后重试 --done(backfill-reviews --adopt 可代填 mechanics)'
516
- }
517
- console.error('❌ ── review.json 字段校验阻断(本次 --done 未完成,进度未推进)──')
518
- console.error(' 已勾选 [x] 的 task review.json 不完整(铁律:勾 checkbox 前必须先写完整 review.json):')
519
- for (const f of result.failures) {
520
- const kindLabel = f.kind === 'missing' ? 'review.json 不存在' : (f.kind === 'parseError' ? 'JSON 解析失败' : '字段缺失')
521
- console.error(` • ${f.taskId}(${kindLabel}): ${f.reviewPath}`)
522
- for (const e of f.errors) console.error(` - ${e}`)
523
- }
524
- console.error(' 修复:mechanics 字段(base/head/changedFiles/schemaVersion)缺错可跑 sillyspec backfill-reviews --change ' + changeName + ' --adopt 一键代填(verdict 保留);verdict/证据缺失需人工补全后重跑 execute --done。')
525
- if (persist) { try { await persist() } catch { /* 落盘失败不吞阻断语义 */ } }
526
- process.exit(1)
527
- }
463
+ // ── enforceReviewJsonGate 已退役(2026-09-26-task-review-retire)──
464
+ // 原「execute --done 时已勾 [x] task 的 review.json schema 硬门」(坑 review-json-field-gap)
465
+ // 随 Task Review 层整体退役删除:勾选语义回归 agent 手动(完成=实现+测试绿+wt-commit 即勾),
466
+ // review.json 不再是任何门的输入;validateCheckedTaskReviews 纯函数保留在 task-review.js
467
+ // (历史变更 doctor/回放兼容读侧,单元测试 review-json-field-gate.test.mjs 仍覆盖)。
528
468
 
529
469
  /**
530
470
  * 阶段完成校验失败时回滚状态。
@@ -564,7 +504,7 @@ export function rollbackStageCompletion(stageData, steps, currentIdx) {
564
504
  * 阶段完成校验失败后的统一收尾:回滚 stage/step 状态 + 落盘 + sync + 返回「未完成」。
565
505
  *
566
506
  * completeStep 的各 gate(runValidators / verify-test 对账 / plan→execute contract /
567
- * Stage Review / Execute Task Review)失败时都走这套动作;原先每个失败分支手写重复
507
+ * Stage Review)失败时都走这套动作;原先每个失败分支手写重复
568
508
  * ~7 次(含 lastActive 落盘 + triggerSync + return 结构),统一进来消坑,避免某分支
569
509
  * 漏写 triggerSync / 写错 return 结构导致行为分裂。返回 nextPendingIdx=currentIdx,
570
510
  * 让上层走「完成但不推进」分支,--done 被拒、agent 修复产物后重跑。
@@ -739,13 +679,21 @@ async function escalateCeremonyTierAtGate({ cwd, specBase, platformOpts, progres
739
679
  /**
740
680
  * 阶段完成校验 gate 级联(从 completeStep 抽出,行为保持)。仅当所有步骤确实标记为 completed 时
741
681
  * 由 completeStep 调用。顺序:runValidators → verify-test 对账 → Plan→Execute contract →
742
- * Stage Review Gate → Execute Task Review Gate。任一 gate 失败 → rollbackCompletionAndReturn
682
+ * Stage Review Gate。任一 gate 失败 → rollbackCompletionAndReturn
743
683
  * (统一回滚 + 返回 early-return 对象);全部通过 → 返回 null(completeStep 继续收尾)。
744
684
  *
745
685
  * @returns {{stageCompleted:false,currentIdx,nextPendingIdx:number}|null}
746
686
  */
747
687
  export async function runStageCompletionGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, ctx = null }) {
748
688
  const projectName = progress.project || basename(cwd)
689
+ // ── 纯文档门收集模式(R16 减负批次,2026-09-24)──
690
+ // 对撞 R15 实证「execute 收口连挡 4 次」:多门串行 fail-fast,每次 --done 只暴露一个门,
691
+ // 修复重跑才见下一个门。纯文档 blocking 门(validators / verify 死信与预填注 / Stage
692
+ // Review)改为收集:全部跑完 → 一次打印清单 → 统一 rollback 一次(detail 取首门,
693
+ // 进度库信号语义不变)。实测门(test/lint/parity/probe)保持文档面全清后才跑、内部 fail-fast
694
+ // 不变——贵的门只为真实代码问题付账。检查点 A=verify 实测门前;B=brainstorm/plan/execute
695
+ // Stage Review Gate 后(friction 升档前——升档语义=「所有阻断门通过」)。
696
+ const docGateFailures = []
749
697
  // decisions.md header 自动补齐(坑 decisions-header-late-warning,2026-08-24 用户反馈二期):
750
698
  // brainstorm step8 旧模板自带无 frontmatter 的 decisions 样例,存量变更照抄必缺 author/created_at,
751
699
  // 拖到平台同步/后续环节才提示。gate 前幂等补机械字段(先例:autoCheckPlanFromReviews 同层写
@@ -818,8 +766,9 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
818
766
  console.error(`\n 提示:修复缺失产物后重新完成此步骤(--skip-approval 只跳过阶段转换/审批检查,不能跳过产物校验)`)
819
767
  // 产物校验失败必须阻断完成 —— 否则 validator 形同虚设,
820
768
  // verify 会带着 FAIL/缺 verify-result.md 被 ✅ 标记完成(历史教训)。
821
- // plan/execute 的专项契约校验(下方)在产物齐全后才需要继续跑,故此处先 return。
822
- return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'validators' })
769
+ // 收集模式(R16):不再直接 return——继续跑完其余纯文档门(review 等)后由检查点统一
770
+ // rollback,一次 --done 暴露全部缺失面(R15 连挡 4 次的解)。
771
+ docGateFailures.push({ type: 'gate_rollback', detail: 'validators', label: `产物契约校验未过(${contractResult.errors.length} 条,明细见上)` })
823
772
  }
824
773
  if (contractResult.warnings.length > 0) {
825
774
  console.warn(`\n⚠️ 阶段 ${stageName} 校验警告:`)
@@ -861,6 +810,77 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
861
810
  } catch { /* advisory 失败不阻断完成(fail-soft,facade 预检先例同款) */ }
862
811
  }
863
812
 
813
+ // ── fail-fast 前置:纯文档 blocking 检查先于 test/lint 实测门(R4-S-F 实证,2026-09-21)──
814
+ // module-impact 死信与预填注清零 error 门只读文档零外部执行,原先排在 9 分钟级 test+lint
815
+ // 实测门之后——文档未清时每轮 --done 都先白烧一轮门禁再被拦(R4-S-F 166min 的主要形状)。
816
+ // 前置到实测门之前:秒级失败秒级返工,贵的门只在文档面已清后才跑。
817
+ if (stageName === 'verify' && changeName) {
818
+ // module-impact 死信探针(blocking,债单 D-1/D-5):更新结果表 pending/待办行 → 阻断 verify。
819
+ // 与 archive 移动前校验(extractPendingDocSyncRows)同一口径,把死信号从 archive 提前到 verify:
820
+ // agent 在 verify 阶段就须完成文档同步并回填 done/skipped,而非拖到归档被拦。
821
+ const verifyChangeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
822
+ if (verifyChangeDir) {
823
+ const impactPath = join(verifyChangeDir, 'module-impact.md')
824
+ if (existsSync(impactPath)) {
825
+ const { extractPendingDocSyncRows } = await import('./complete-handlers.js')
826
+ const pendingRows = extractPendingDocSyncRows(readFileSync(impactPath, 'utf8'))
827
+ if (pendingRows.length > 0) {
828
+ console.error(`\n❌ verify 阶段被阻断:module-impact.md「更新结果」表存在 ${pendingRows.length} 个未清 pending/待办项(死信)`)
829
+ for (const row of pendingRows) console.error(` - ${row}`)
830
+ console.error(' 文档同步是 verify 的收尾义务:请完成模块文档同步并回填状态为 done/skipped(说明原因),再重新完成 verify。')
831
+ docGateFailures.push({ type: 'gate_rollback', detail: 'verify-contract', label: `module-impact.md 存在 ${pendingRows.length} 个 pending/待办死信(明细见上)` })
832
+ }
833
+ }
834
+ }
835
+ // 预填注清零 error 门(归档前最后一道;2026-09-18-artifact-prefill task-03 / FR-03 / D-003@v1
836
+ // 门禁梯度·error 档):白名单槽宿主文件(design.md + tasks/task-*.md)仍含未删预填注 →
837
+ // 阻断 verify 完成。brainstorm/plan --done 的 advisory 在此收口为硬门,注在场=预填未确认。
838
+ // 检测单点 = runProbe10PrefillNoteClearance(不二算)。已知误报面:散文引用注字面量会命中
839
+ // ——修法是核对后删注/改写散文措辞,不是绕门。
840
+ try {
841
+ const { runProbe10PrefillNoteClearance } = await import('../verify-probes.js')
842
+ const noteClearance = runProbe10PrefillNoteClearance({ specBase, changeName })
843
+ if (noteClearance.unclearedFiles.length > 0) {
844
+ console.error(`\n❌ verify 阶段被阻断:预填注清零校验未过(${noteClearance.unclearedFiles.length} 个文件的白名单槽仍含未删预填注——预填≠结论,注在场=未确认):`)
845
+ for (const f of noteClearance.unclearedFiles) console.error(` - ${f}`)
846
+ console.error(' 修复:逐槽核对预填值后删除行尾「(预填:核对后删本注)」注(删注=确认动作);散文含注字面量的命中改写该处措辞。清零后重新完成 verify(进度不丢)。')
847
+ docGateFailures.push({ type: 'gate_rollback', detail: 'verify-contract', label: `预填注清零未过(${noteClearance.unclearedFiles.length} 文件,明细见上)` })
848
+ }
849
+ } catch (e) {
850
+ console.warn(`⚠️ 预填注清零校验异常(降级放行,fail-soft——异常不是未清零,ceremony 双跑同款处置): ${(e && e.message) || e}`)
851
+ }
852
+
853
+ // UI 视觉证据门·error 档(2026-09-27-ui-visual-guidance / 探针 12 分级门的 verify 收尾执法):
854
+ // 视觉降级无用户裁决留痕恒拦;ui_visual_gate=error 时缺 visual-evidence.md 亦拦(默认 warn
855
+ // 只警告不拦)。检测单点 = runUiVisualProbe(与 flow done probes 子步、verify-probes --init
856
+ // 同源,不二算)。fail-soft 同预填注门。
857
+ try {
858
+ const { runUiVisualProbe, readUiVisualGate, UI_EVIDENCE_FILENAME } = await import('../ui-visual.js')
859
+ const uiProbe = runUiVisualProbe({ changeDir: join(specBase, 'changes', changeName), gate: readUiVisualGate(specBase) })
860
+ if (uiProbe.level === 'error') {
861
+ console.error(`
862
+ ❌ verify 阶段被阻断:UI 视觉证据门未过(${uiProbe.notes.join(';')})`)
863
+ console.error(` 修复:补渲染对照证据到 ${UI_EVIDENCE_FILENAME};降级项补「用户裁决」留痕段。证据应在执行时按 flow start「UI 变更执行须知」随手产生——收口只验在场。`)
864
+ docGateFailures.push({ type: 'gate_rollback', detail: 'verify-contract', label: 'UI 视觉证据门未过(探针 12 error 档,明细见上)' })
865
+ } else if (uiProbe.level === 'warn') {
866
+ console.warn(`
867
+ ⚠️ UI 视觉证据(探针 12 warn 档):${uiProbe.notes.join(';')}`)
868
+ }
869
+ } catch (e) {
870
+ console.warn(`⚠️ UI 视觉证据探针异常(降级放行,fail-soft): ${(e && e.message) || e}`)
871
+ }
872
+ }
873
+
874
+ // ── 收集模式检查点 A(verify):纯文档门全清后才进入实测门(test/lint/parity/probe 保持
875
+ // fail-fast 不变)。brainstorm/plan/execute 不走本检查点(其实测面为空,检查点 B 收口)。──
876
+ if (stageName === 'verify' && docGateFailures.length > 0) {
877
+ console.error(`\n❌ verify 收口被 ${docGateFailures.length} 道纯文档门拦截(一次性全列——全部修复后重跑一次 --done,不必逐门连撞):`)
878
+ docGateFailures.forEach((f, i) => console.error(` ${i + 1}. ${f.label}`))
879
+ console.error(` (各门明细见上方对应 ❌ 段落;实测门(test/lint)在文档面全清后才执行)`)
880
+ const firstFail = docGateFailures[0]
881
+ return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: firstFail.type, detail: firstFail.detail })
882
+ }
883
+
864
884
  // verify 产物校验通过 + 结论非 FAIL(否则上面已阻断)。
865
885
  // 再由 CLI 亲自执行 local.yaml 的测试命令,与 verify-result.md 的自报告对账:
866
886
  // 自报告 PASS 但实测失败 → 阻断(防止"文案通过"绕过验证)。
@@ -911,16 +931,134 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
911
931
  const gateCwd = verifyGateSnap ? verifyGateSnap.snapshotRoot : cwd
912
932
  const gateSpecBase = verifyGateSnap ? join(verifyGateSnap.snapshotRoot, '.sillyspec') : specBase
913
933
  let testCheck
914
- if (reusableScan && reusableScan.testResult) {
934
+ // ── P2 三键账本复用(D-001@v1,2026-09-21-r5-efficiency-batch3 task-01):代码×测试面×环境
935
+ // 三键全等且有通过记录 → 免重跑(batch2 实证同码态全量被跑多遍)。fail-closed:键分量
936
+ // 不可得/键不等/无记录 → 真跑(走下方原路径,行为不变)。咨询异常整体按无账本(fail-open
937
+ // 不影响门禁)。优先级:P2 账本(三键精确)> P0-1 质量扫描复用 > 真跑。──
938
+ let ledgerReuse = null
939
+ // ── 账本停复用护栏(2026-09-24-fr-test-readside task-05,D-006@v1):trace 含 active 行
940
+ // 的变更残差并入跑面,v2 键(config+整目录面)不再描述实际执行——fail-closed 跳过
941
+ // consult/record(宁多跑一轮不误复用未含残差的旧绿;退役判据=账本键 v3 携 aggregate
942
+ // run plan)。空 trace/缺失 → 照用 v2(行为不变)。──
943
+ let traceHasActiveRows = false
944
+ try {
945
+ const { readChangeTrace } = await import('../test-bindings.js')
946
+ traceHasActiveRows = readChangeTrace(join(specBase, 'changes', changeName)).some(r => r.state === 'active')
947
+ if (traceHasActiveRows) console.log('ℹ️ 本变更含 active 测试绑定行——账本停复用(残差跑面静态不可键化,fail-closed)')
948
+ } catch { /* 读失败按无 trace(照用账本) */ }
949
+ if (!traceHasActiveRows) {
950
+ try {
951
+ const { consultTestLedger } = await import('./test-ledger.js')
952
+ const consult = consultTestLedger({
953
+ runtimeRoot: resolveRuntimeRoot(platformOpts, specBase),
954
+ changeName, projectRoot: gateCwd, testRoot: join(gateCwd, 'test'), specBase: gateSpecBase, cwd: gateCwd,
955
+ })
956
+ if (consult.reuse) ledgerReuse = consult
957
+ } catch { /* 账本咨询异常 → 真跑 */ }
958
+ }
959
+ if (ledgerReuse) {
960
+ testCheck = {
961
+ status: 'passed',
962
+ reason: `♻️ P2 三键账本复用(代码×测试面×环境全等;实测于 ${ledgerReuse.result.ranAt},耗时 ${Math.round((ledgerReuse.result.durationMs || 0) / 1000)}s)`,
963
+ command: `${ledgerReuse.runPlan?.[0]?.runner || 'npm test'} (ledger-reuse)`,
964
+ exitCode: 0, durationMs: 0, outputTail: null,
965
+ resultPath: null, strategy: ledgerReuse.result.strategy || null,
966
+ }
967
+ console.log(`\n♻️ Verify 测试对账:P2 账本复用(三键全等,免重跑;实测于 ${ledgerReuse.result.ranAt})`)
968
+ printVerifyTestCheck(testCheck)
969
+ } else if (reusableScan && reusableScan.testResult) {
915
970
  testCheck = reusableScan.testResult
916
971
  console.log(`\n♻️ Verify 测试对账:复用 noAI 质量扫描步的实测结果(代码指纹匹配,免重跑;实测于 ${reusableScan.ranAt || '本变更 verify 期间'}${testCheck.resultPath ? `,台账 ${testCheck.resultPath}` : ''})`)
917
972
  printVerifyTestCheck(testCheck)
918
973
  } else {
919
974
  // 测试实测是同步 execSync,长套件可跑 2~10min 且中途无输出——先预告避免 agent 误判卡死
920
975
  console.log(`\n⏳ Verify 测试对账:CLI 亲自执行 local.yaml 的 commands.test(同步,耗时可能较长,请等待…)`)
921
- testCheck = runVerifyTestCheck({ cwd: gateCwd, specBase: gateSpecBase, changeName, ctx })
976
+ // restrictFiles 接线(2026-09-26-verify-gate-restrictfiles,评审 P2 留痕缺口的清偿):
977
+ // quick 门(quick-audit.js:584)传声明文件收窄模块选择,verify 门此前没传——变更全提交后
978
+ // verify --done 同形下文件面解析 0 命中可能假 skip(该实测没实测)。与 :1135 lint scope
979
+ // 同源口径(includeWorkingTree 含 worktree 未提交);空清单不传(restrict 空数组反而制造
980
+ // 假 skip——空=解析失败应走全量硬门,不降级)。
981
+ let _restrict = null
982
+ try {
983
+ const { resolveVerifyChangedFiles } = await import('../verify-postcheck.js')
984
+ _restrict = resolveVerifyChangedFiles(cwd, changeName, null, { includeWorkingTree: true, specBase: gateSpecBase })
985
+ } catch { /* 解析异常走全量(restrictFiles 不传) */ }
986
+ testCheck = runVerifyTestCheck({ cwd: gateCwd, specBase: gateSpecBase, changeName, ctx, ...(Array.isArray(_restrict) && _restrict.length > 0 ? { restrictFiles: _restrict } : {}) })
922
987
  printVerifyTestCheck(testCheck)
988
+ // 自动绑定补全(full-autopilot-parity,R21 thin 迁移):测试已跑、test-result.json 已落盘,
989
+ // verify --done 收口时从测试结果自动补全 requirements.md 的空绑定槽——与 thin 同逻辑(逐行扫描)。
990
+ try {
991
+ const { verifyRequirementBindings } = await import('../flow-draft.js')
992
+ let _rb = verifyRequirementBindings({ changeDir: resolveChangeDir(cwd, { currentChange: changeName }, platformOpts?.specRoot) })
993
+ if (_rb.applicable && _rb.emptySlots.length > 0) {
994
+ const _runsDir = join(resolveRuntimeRoot(platformOpts, specBase), 'verify-runs')
995
+ let _testFiles = new Set()
996
+ if (existsSync(_runsDir)) {
997
+ for (const _rd of readdirSync(_runsDir).sort().reverse().slice(0, 3)) {
998
+ const _tr = join(_runsDir, _rd, 'test-result.json')
999
+ if (!existsSync(_tr)) continue
1000
+ try {
1001
+ const _j = JSON.parse(readFileSync(_tr, 'utf8'))
1002
+ // P1 修复:从 command 解析测试文件路径(匹配 test_*.py / *.test.ts 等)
1003
+ const _cmd = String(_j.command || '')
1004
+ for (const _fm of _cmd.matchAll(/([\w\/\\.-]+\.\w{1,5})/g)) {
1005
+ if (/test|spec/i.test(_fm[1])) _testFiles.add(_fm[1].replace(/\\/g, '/'))
1006
+ }
1007
+ if (_testFiles.size > 0) break
1008
+ } catch {}
1009
+ }
1010
+ }
1011
+ if (_testFiles.size > 0) {
1012
+ const _reqPath = join(resolveChangeDir(cwd, { currentChange: changeName }, platformOpts?.specRoot), 'requirements.md')
1013
+ const _lines = readFileSync(_reqPath, 'utf8').split('\n')
1014
+ const _bind = [..._testFiles].slice(0, 3).join('、')
1015
+ let _filled = 0
1016
+ for (let _li = 0; _li < _lines.length - 1; _li++) {
1017
+ if (/^<!--\s*AGENT:测试绑定/.test(_lines[_li]) && !_lines[_li + 1].trim()) {
1018
+ _lines[_li + 1] = _bind; _filled++; _li++
1019
+ }
1020
+ }
1021
+ if (_filled > 0) {
1022
+ const { writeAtomicSync } = await import('../fs-atomic.js')
1023
+ writeAtomicSync(_reqPath, _lines.join('\n'))
1024
+ console.log(`✅ [自动绑定] ${_filled} 个空绑定槽已从测试结果补全(${_bind.slice(0, 60)})——agent 可覆盖(full-autopilot-parity)`)
1025
+ }
1026
+ }
1027
+ }
1028
+ } catch { /* auto-bind fail-soft */ }
1029
+ // P2 记账(fail-closed 层②:只有通过结果落账本——失败永不缓存,修复后重跑才能记)
1030
+ if (!traceHasActiveRows) {
1031
+ try {
1032
+ const { recordTestLedger } = await import('./test-ledger.js')
1033
+ if (testCheck && testCheck.status === 'passed') {
1034
+ recordTestLedger({
1035
+ runtimeRoot: resolveRuntimeRoot(platformOpts, specBase),
1036
+ changeName, projectRoot: gateCwd, testRoot: join(gateCwd, 'test'), specBase: gateSpecBase, cwd: gateCwd,
1037
+ result: { pass: true, total: testCheck.total ?? null, failedFiles: [], durationMs: testCheck.durationMs ?? null, strategy: testCheck.strategy ?? null },
1038
+ })
1039
+ }
1040
+ } catch { /* 记账异常不影响门禁(下次仍真跑) */ }
1041
+ }
923
1042
  }
1043
+ // ── trace 晋升(2026-09-24-fr-test-bindings task-04,fr-test-binding §3.2/D-003@v1):
1044
+ // verify 门通过点按矩阵判定列晋升 candidate→active(covered/covered-service→active+
1045
+ // confirmed_by=agent;partial 留;uncovered/non-testable 删行)。fail-open:晋升异常不拦门。──
1046
+ try {
1047
+ const verifyMdForPromo = join(specBase, 'changes', changeName, 'verify-result.md')
1048
+ if (existsSync(verifyMdForPromo)) {
1049
+ const { extractAcceptanceMatrixSlots } = await import('../stage-contract.js')
1050
+ const { promoteTraceFromMatrix } = await import('../test-bindings.js')
1051
+ const slots = extractAcceptanceMatrixSlots(readFileSync(verifyMdForPromo, 'utf8'))
1052
+ if (slots.present && Array.isArray(slots.rows) && slots.rows.length > 0) {
1053
+ const promo = promoteTraceFromMatrix({
1054
+ changeDir: join(specBase, 'changes', changeName), changeName, matrixRows: slots.rows,
1055
+ })
1056
+ if (promo.promoted > 0 || promo.dropped > 0) {
1057
+ console.log(`🔗 绑定晋升:${promo.promoted} 行→active(confirmed_by=agent),${promo.dropped} 行删(uncovered/non-testable),${promo.kept} 行保持`)
1058
+ }
1059
+ }
1060
+ }
1061
+ } catch { /* 晋升 fail-open(不拦 verify 门,归档以 trace 现态为准) */ }
924
1062
  // tests 段二次回填(2026-09-08-ir-verify-facts FR-03 次序:实测在 runValidators 之后,
925
1063
  // tests 快照此刻才可得;不参与门禁——供 P3d 追溯)
926
1064
  try {
@@ -937,6 +1075,20 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
937
1075
  }
938
1076
  } catch { /* fail-soft:回填失败不影响门禁 */ }
939
1077
  if (testCheck.status === 'failed') {
1078
+ // ── 纯超时降档(R4 门禁价值考古 2026-09-21:227 跑 106 败里 36 次 600s 帽杀纯超时假拦
1079
+ // =失败桶最大单一来源)。超时=未完成非测试挂——所有失败单元 reason 均含超时 → advisory
1080
+ // 放行带处置指引;任一单元真实挂测/退出码 → 维持原硬拦(真拦 12 次的防线不动)。──
1081
+ let timeoutOnly = false
1082
+ try {
1083
+ const { isTimeoutOnlyTestFailure } = await import('../verify-postcheck.js')
1084
+ timeoutOnly = isTimeoutOnlyTestFailure(testCheck)
1085
+ } catch { /* 判定链异常维持硬拦(保守) */ }
1086
+ if (timeoutOnly) {
1087
+ if (verifyGateSnap) { try { verifyGateSnap.cleanup() } catch {} ; verifyGateSnap = null }
1088
+ console.warn('\n⚠️ Verify 实测门未在超时帽内完成(纯超时,无任何挂掉的测试)——降档 advisory 不阻断:')
1089
+ console.warn(` ${testCheck.reason || ''}`)
1090
+ console.warn(' 处置:① local.yaml 配置 modules: 块(name: { path, test })启用模块子集实测——触碰模块全套测试,时长与超时同步收窄;② 定向复跑触碰模块自证;③ 确需长跑设 SILLYSPEC_TEST_TIMEOUT_MS。')
1091
+ } else {
940
1092
  if (verifyGateSnap) {
941
1093
  try { const { printSnapshotFailureHint } = await import('./gate-snapshot.js')
942
1094
  printSnapshotFailureHint(verifyGateSnap) } catch { /* 提示失败不影响阻断 */ }
@@ -953,6 +1105,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
953
1105
  } catch { /* 归因提示失败不影响阻断语义 */ }
954
1106
  console.error(' 请修复失败的测试并更新 verify-result.md 后重新完成此步骤。')
955
1107
  return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-test' })
1108
+ } // else(纯超时降档分支)结束——真实测试失败分支的 rollback 语义不变
956
1109
  }
957
1110
  // lint 对账(2026-08-21 审查 CLI-1):test 侧"自报告 PASS 但实测失败→阻断"已闭环,
958
1111
  // lint 侧此前纯口头——CLI 亲自执行 commands.lint,advisory 起步(失败打印不阻断,观察期后升级)
@@ -983,6 +1136,22 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
983
1136
  // 逃生 SILLYSPEC_VERIFY_LINT_GATE=advisory)
984
1137
  const { shouldBlockVerifyLint } = await import('../verify-postcheck.js')
985
1138
  if (shouldBlockVerifyLint(lintCheck)) {
1139
+ // ── lint 归属鉴定降档(R4-S-F 实证:verify 侧 lint 硬门 7 轮全是 HEAD 存量债——隔离
1140
+ // 快照含基线债文件时恒败,agent 被逼范围外清偿或逃生口)。失败输出提及文件 × 本变更
1141
+ // 文件集零交集 → 存量债 advisory 降档放行(债非本变更引入,硬拦只是制造范围外功);
1142
+ // 有交集 / 输出无可识别路径(unattributable,纯配置错误类)→ 维持硬拦(保守)。──
1143
+ let lintOwnership = null
1144
+ try {
1145
+ const { triageLintOwnership, resolveVerifyChangedFiles } = await import('../verify-postcheck.js')
1146
+ const lintScopeFiles = (resolveVerifyChangedFiles(cwd, changeName, null, { includeWorkingTree: true, specBase }) || [])
1147
+ .map(f => String(f).replace(/\\/g, '/')).filter(f => !f.startsWith('.sillyspec/'))
1148
+ lintOwnership = triageLintOwnership({ failureFiles: lintCheck.failureFiles || [], changeFiles: lintScopeFiles })
1149
+ } catch { /* 鉴定链异常维持硬拦(保守) */ }
1150
+ if (lintOwnership && lintOwnership.verdict === 'pre-existing') {
1151
+ console.warn('\n⚠️ Verify lint 实测失败,但归属鉴定为 HEAD 存量债(失败提及文件与本变更文件集零交集)——降档 advisory 不阻断 verify 完成:')
1152
+ console.warn(` 存量债文件:${(lintCheck.failureFiles || []).join('、') || '(见上方输出)'}`)
1153
+ console.warn(' 处置建议:单独 quick 机械清偿(顺手清债解锁全仓 lint 门),或本变更确认无关时设 SILLYSPEC_VERIFY_LINT_GATE=advisory(审计留痕)。债随基线存在,与本变更交付无关。')
1154
+ } else {
986
1155
  console.error('\n❌ verify 阶段被阻断:CLI 亲自实测 lint 失败(agent 的 lint 自报告与实测不符时以实测为准)。')
987
1156
  if (usedSnapInfo) {
988
1157
  try { const { printSnapshotFailureHint } = await import('./gate-snapshot.js')
@@ -1006,6 +1175,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1006
1175
  }
1007
1176
  console.error(' 修复后重跑 --done(进度不丢);确认要跳过实测请设 SILLYSPEC_VERIFY_LINT_GATE=advisory(审计留痕)。')
1008
1177
  return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-lint' })
1178
+ } // else(pre-existing 降档分支)结束——维持硬拦分支的 rollback 语义不变
1009
1179
  }
1010
1180
  // 契约 parity 对账:扫前端 API 调用 vs execute 提取的 provider endpoint artifact。
1011
1181
  // 接线自 contract-matrix pipeline(verifyApiParity 的 CLI 入口)。
@@ -1018,7 +1188,8 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1018
1188
  const deletionCheck = runVerifyDeletionCheck({ cwd, specBase, changeName })
1019
1189
  printVerifyDeletionCheck(deletionCheck)
1020
1190
  // ── required-evidence 对账(2026-09-08-ir-verify-facts FR-03:v2 起 cannot_verify 不闭环阻断)──
1021
- // execute Task Review Gate 把 cannot_verify 任务的 evidence 落盘 verify-required-evidence.json。
1191
+ // verify-required-evidence.json 由退役前的 execute Task Review Gate 写入
1192
+ // (2026-09-26-task-review-retire 起停写;在场则消费=历史变更兼容读,缺席=无 cannot_verify 任务)。
1022
1193
  // v2 槽优先分类核验(FR-02):blocked = missing 无豁免或 satisfied 核验不过 → 阻断 verify 完成
1023
1194
  // (此前 advisory 只查「提及」——死链不闭环);无槽存量 md 降级 legacy 子串对账(warning 不阻断)。
1024
1195
  const { runVerifyRequiredEvidenceCheck, printVerifyRequiredEvidenceCheck, trackVerifyResultRegression } = await import('../verify-postcheck.js')
@@ -1076,6 +1247,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1076
1247
  // 放行态),fail-soft 不影响 gate 判定;②类阻断回执同样留档供平台/审计消费
1077
1248
  writeReconcileRunResult({ runtimeRoot: reconcileRuntimeRoot, changeName, envelope: reconcileEnvelope, result: reconcileCheck })
1078
1249
  if (reconcileBlocked) {
1250
+ writeVerifyGatePointer({ runtimeRoot: reconcileRuntimeRoot, changeName, blocked: true, note: 'target_files ②类(missing_declared)阻断' })
1079
1251
  return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
1080
1252
  }
1081
1253
  // ── verify-result.md 探针预填段一致性抽查(task-03 / ir-stage-p3b D-002@v1 方案A)──
@@ -1122,8 +1294,29 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1122
1294
  // 两检查间耦合;各写各的回执、按文件名订阅更干净。fail-soft 不影响 gate 判定
1123
1295
  writeProbeConsistencyRunResult({ runtimeRoot: reconcileRuntimeRoot, changeName, envelope: probeEnvelope, result: probeCheck })
1124
1296
  if (probeBlocked) {
1297
+ writeVerifyGatePointer({ runtimeRoot: reconcileRuntimeRoot, changeName, blocked: true, note: '探针一致性抽查 error 阻断' })
1125
1298
  return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
1126
1299
  }
1300
+ // ── verify 填槽制篡改门禁(r5l 方案3 / 评审护栏#2 承重件,v2 件二)──
1301
+ // verify-probes --init --draft 生成的机器预填段(任务完成度/风险等级/测试结果/决策追踪)带
1302
+ // sha256 指纹标记 + .runtime sidecar 台账;--done 对账:标记被删 / 内容哈希失配 → 阻断回滚
1303
+ // (heredoc 整份重写的典型形态——Write 可用与否均免疫,机制修法不做生成侧劝说)。修改机器
1304
+ // 段唯一通道 verify-probes --amend-draft(重锚 + amendment 审计)。无 sidecar(未用 draft /
1305
+ // 存量变更)→ not-applicable 零红;门禁自身异常 fail-soft(对齐探针抽查 degraded 语义)。
1306
+ try {
1307
+ const { checkDraftIntegrity } = await import('../verify-draft.js')
1308
+ const draftCheck = checkDraftIntegrity({
1309
+ mdPath: join(specBase, 'changes', changeName, 'verify-result.md'),
1310
+ changeName, runtimeRoot: reconcileRuntimeRoot,
1311
+ })
1312
+ if (draftCheck.applicable && !draftCheck.ok) {
1313
+ console.error(`\n❌ verify-result.md 机器预填段篡改门禁:${draftCheck.violations.length} 处失配——draft 预填段被整份重写或标记被删/被手工重锚。`)
1314
+ for (const v of draftCheck.violations) console.error(` - ${v}`)
1315
+ console.error(' 修复:还原机器段原文(AGENT 槽——结论枚举/移交项/审查叙述——不受本门限制);确要修改机器段,跑 sillyspec verify-probes --change ' + changeName + ' --amend-draft 留痕重锚后重新 --done。')
1316
+ writeVerifyGatePointer({ runtimeRoot: reconcileRuntimeRoot, changeName, blocked: true, note: 'draft 篡改门禁阻断' })
1317
+ return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
1318
+ }
1319
+ } catch { /* 门禁异常 fail-soft(不是篡改,对齐探针 degraded 放行) */ }
1127
1320
  // ── ceremony 双跑收口·第一出口(2026-09-18-ceremony-risk-pricing task-04 / FR-03 / D-003;
1128
1321
  // execute 评审 FAIL 项补接线):声明档(.runtime/ceremony-tier-<change>.json 开跑定价)vs
1129
1322
  // 实际 diff 重跑 blast+span 的事实档,mismatch(severity=error,懒 agent 低报)汇入 verify
@@ -1164,49 +1357,40 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1164
1357
  }
1165
1358
  }
1166
1359
  } catch { /* 自动重锚失败不影响收尾(docs gate 兜底) */ }
1167
- // ── module-impact 死信探针(blocking,债单 D-1/D-5):更新结果表 pending/待办行 → 阻断 verify ──
1168
- // 与 archive 移动前校验(extractPendingDocSyncRows)同一口径,把死信号从 archive 提前到 verify:
1169
- // agent 在 verify 阶段就须完成文档同步并回填 done/skipped,而非拖到归档被拦(修复 perf-remediation
1170
- // 类「verify PASS → archive 才发现 pending」的时序漏洞)。
1171
- const verifyChangeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
1172
- if (verifyChangeDir) {
1173
- const impactPath = join(verifyChangeDir, 'module-impact.md')
1174
- if (existsSync(impactPath)) {
1175
- const { extractPendingDocSyncRows } = await import('./complete-handlers.js')
1176
- const pendingRows = extractPendingDocSyncRows(readFileSync(impactPath, 'utf8'))
1177
- if (pendingRows.length > 0) {
1178
- console.error(`\n❌ verify 阶段被阻断:module-impact.md「更新结果」表存在 ${pendingRows.length} 个未清 pending/待办项(死信)`)
1179
- for (const row of pendingRows) console.error(` - ${row}`)
1180
- console.error(' 文档同步是 verify 的收尾义务:请完成模块文档同步并回填状态为 done/skipped(说明原因),再重新完成 verify。')
1181
- return rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
1182
- }
1183
- }
1184
- }
1185
- // ── 预填注清零 error 门(归档前最后一道;2026-09-18-artifact-prefill task-03 / FR-03 /
1186
- // D-003@v1 门禁梯度·error 档):白名单槽宿主文件(design.md + tasks/task-*.md)仍含
1187
- // 未删预填注 → 阻断 verify 完成(verify 完成是 archive 前置——「归档前注清零」的落点;
1188
- // brainstorm/plan --done 的 advisory 在此收口为硬门,注在场=预填未确认)。检测单点 =
1189
- // verify-probes 探针面 runProbe10PrefillNoteClearance(探针 10 --init 预填与本门同一
1190
- // 实现,不二算)。已知误报面:散文引用注字面量会命中——修法是核对后删注/改写散文
1191
- // 措辞,不是绕门。──
1192
- try {
1193
- const { runProbe10PrefillNoteClearance } = await import('../verify-probes.js')
1194
- const noteClearance = runProbe10PrefillNoteClearance({ specBase, changeName })
1195
- if (noteClearance.unclearedFiles.length > 0) {
1196
- console.error(`\n❌ verify 阶段被阻断:预填注清零校验未过(${noteClearance.unclearedFiles.length} 个文件的白名单槽仍含未删预填注——预填≠结论,注在场=未确认):`)
1197
- for (const f of noteClearance.unclearedFiles) console.error(` - ${f}`)
1198
- console.error(' 修复:逐槽核对预填值后删除行尾「(预填:核对后删本注)」注(删注=确认动作);散文含注字面量的命中改写该处措辞。清零后重新完成 verify(进度不丢)。')
1199
- return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
1200
- }
1201
- } catch (e) {
1202
- console.warn(`⚠️ 预填注清零校验异常(降级放行,fail-soft——异常不是未清零,ceremony 双跑同款处置): ${(e && e.message) || e}`)
1203
- }
1360
+ // ── module-impact 死信探针 + 预填注清零 error 门:已前移至 test/lint 实测门之前 ──
1361
+ // (fail-fast,R4-S-F 实证——纯文档 blocking 检查排在 9 分钟实测门后白烧门禁轮次;
1362
+ // 实现体见上方「fail-fast 前置」块,此处不再重复执行。)
1204
1363
  // ── verify 服务进程回收(坑 verify-service-process-leak;坑 verify-pids-cross-session-kill
1205
1364
  // 见 reapVerifyServices 注释)──
1206
1365
  try {
1207
1366
  reapVerifyServices(platformOpts, specBase, changeName)
1208
1367
  } catch (e) { console.warn(`⚠️ verify 服务进程回收异常(不阻断): ${e.message}`) }
1209
1368
 
1369
+ // ── 归档就绪度报告(P3 / FR-03 / D-003@batch3,task-04):构造归 buildArchiveReadinessReport
1370
+ // (本文件下方导出的可测函数),此处只渲染。全只读 fail-open;无发现零输出。──
1371
+ try {
1372
+ const report = await buildArchiveReadinessReport({ cwd, changeName, progress, specBase: platformOpts?.specRoot || specBase })
1373
+ if (report && (report.face.length > 0 || report.manifestGaps.length > 0 || report.impactUnlisted.length > 0 || report.agingHit)) {
1374
+ console.log('\n📋 归档就绪度报告(verify 收口前置——archive 三查预演,全只读):')
1375
+ if (report.face.length > 0) {
1376
+ console.log(' ⎿ 未-apply 交付面 ' + report.face.length + ' 个文件:' + report.face.slice(0, 5).join('、') + (report.face.length > 5 ? ' 等' : ''))
1377
+ console.log(' 建议 archive 前先 apply:sillyspec worktree apply ' + changeName)
1378
+ } else {
1379
+ console.log(' ⎿ 未-apply 交付面:0(干净)')
1380
+ }
1381
+ if (report.manifestGaps.length > 0) {
1382
+ console.log(' ⎿ manifest 缺行草拟【待确认——确认后自行补入 design.md §6,草拟不代写】:')
1383
+ for (const g of report.manifestGaps.slice(0, 5)) console.log(' | 修改 | ' + g + ' | 归档就绪度草拟(verify 收口前置探测) |')
1384
+ if (report.manifestGaps.length > 5) console.log(' … 共 ' + report.manifestGaps.length + ' 个')
1385
+ }
1386
+ if (report.impactUnlisted.length > 0) {
1387
+ console.log(' ⎿ module-impact 归因草拟【待确认——确认后补入未匹配/矩阵章节】:')
1388
+ for (const u of report.impactUnlisted.slice(0, 5)) console.log(' - `' + u + '` → 归因(共位测试/镜像/并行排除…)')
1389
+ if (report.impactUnlisted.length > 5) console.log(' … 共 ' + report.impactUnlisted.length + ' 个')
1390
+ }
1391
+ if (report.agingHit) console.log(' ⚠️ main 已前进且与交付面有交集(基线老化中——等待越久合并面越大,batch2 实证窗口期三方冲突):建议立即 apply')
1392
+ }
1393
+ } catch { /* 就绪度报告整体 fail-open:异常零输出不影响 verify 收尾 */ }
1210
1394
  console.log('\n✅ 验证通过,下一步:sillyspec run archive')
1211
1395
  }
1212
1396
 
@@ -1260,6 +1444,9 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1260
1444
 
1261
1445
  if (tier.tier === 'self') {
1262
1446
  console.log('\nℹ️ Stage Review: ' + stageName + ' tier=self(' + tier.reason + '),已降级为当前 agent 自审,不强制独立子代理。')
1447
+ } else if (readReviewUnsupervisedWaiver(reviewChangeDir)) {
1448
+ // 豁免凭据(review-unsupervised-exit):无嵌套派发能力环境不强制独立审查也不接受自审表演
1449
+ await waiveWithTelemetry({ changeName, gate: 'stage-review:' + stageName, specBase: effectiveSpecBase })
1263
1450
  } else {
1264
1451
  let reviewRunId = getLatestStageReviewRunId(runtimeRoot, stageName, changeName)
1265
1452
  if (!reviewRunId) {
@@ -1293,8 +1480,8 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1293
1480
  const reviewResult = gated.result
1294
1481
  printStageReviewResult(reviewResult, { stage: stageName, reviewRunId, runtimeRoot, changeName })
1295
1482
  if (!reviewResult.ok) {
1296
- return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'review_rejected', detail: 'stage-review' })
1297
- }
1483
+ docGateFailures.push({ type: 'review_rejected', detail: 'stage-review', label: 'Stage Review 未过(stage-review,明细见上)' })
1484
+ } else {
1298
1485
  // 降级自审留痕(2026-09-10 用户反馈①:PI agent 等宿主无 Agent tool,prompt 降级条款允许
1299
1486
  // 当前 agent 自审产出 review.json)。放行但独立性折损必须可见:⚠️ 审计行区分降级 review
1300
1487
  // 与真子代理 review(事后可审计),与 ⚖️/🔍 软归属同哲学——可见的折损优于静默的伪装。
@@ -1315,11 +1502,13 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1315
1502
  if (hasDelegatedWriteDisclosure(reviewResult.review)) {
1316
1503
  console.warn(`\n⚠️ Stage Review 代落盘披露:${stageName} review 的 reviewerNotes 首行标记「代落盘」——review.json 由主代理代(写通道故障的)审查者落盘,独立性依赖审查者结论回传的忠实性,建议人工抽查关键结论与回传文本一致性。`)
1317
1504
  }
1505
+ }
1318
1506
  }
1319
1507
  } catch (e) {
1320
- // fail-closed:Gate 自身异常阻断完成,不静默放行(与 Task Review Gate 一致)
1508
+ // fail-closed:Gate 自身异常阻断完成,不静默放行
1509
+ //(收集模式:异常也入列,检查点 B 统一 rollback)
1321
1510
  console.error('❌ Stage Review Gate 异常,阻断 ' + stageName + ' 完成: ' + e.message)
1322
- return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'review_rejected', detail: 'stage-review' })
1511
+ docGateFailures.push({ type: 'review_rejected', detail: 'stage-review', label: `Stage Review Gate 异常(${e.message})` })
1323
1512
  }
1324
1513
  }
1325
1514
 
@@ -1343,109 +1532,21 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1343
1532
  } catch { /* 提示失败不阻断 execute */ }
1344
1533
  }
1345
1534
 
1346
- // ── Execute Task Review Gate:所有 task 必须有 review.json 且 verdict 通过 ──
1347
- if (stageName === 'execute') {
1348
- try {
1349
- const { validateTaskReviews, printReviewResult, writeVerifyRequiredEvidence } = await import('../task-review.js')
1350
- const effectiveSpecBase = platformOpts?.specRoot || specBase
1351
- const planFile = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
1352
- const planPath = planFile ? join(planFile, 'plan.md') : null
1353
-
1354
- if (planPath && existsSync(planPath)) {
1355
- // 2026-08-20-task-truth-unify:任务清单源迁 tasks.md(validateTaskReviews 内按
1356
- // checkbox 行解析,tasks.md 行格式兼容原正则);缺失回退 plan.md(旧变更兼容读侧)
1357
- const tasksRegistryPath = join(planFile, 'tasks.md')
1358
- const planContent = existsSync(tasksRegistryPath)
1359
- ? readFileSync(tasksRegistryPath, 'utf8')
1360
- : readFileSync(planPath, 'utf8')
1361
- const runtimeRoot = resolveRuntimeRoot(platformOpts, effectiveSpecBase)
1362
-
1363
- // execute run id:从变更专属标记文件读取(agent 可写内容,格式校验防注入/穿越)
1364
- const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
1365
- let executeRunId = ''
1366
- try {
1367
- if (existsSync(runIdFile)) {
1368
- const c = readFileSync(runIdFile, 'utf8').trim()
1369
- const { isValidExecuteRunId } = await import('../task-review.js')
1370
- if (c && !isValidExecuteRunId(c)) {
1371
- console.warn(`⚠️ execute run marker 内容非法(期望 exec-YYYY-MM-DD-HHMMSS,实得 ${JSON.stringify(c.slice(0, 60))}),视为缺失回退扫描`)
1372
- } else {
1373
- executeRunId = c
1374
- }
1375
- }
1376
- } catch {}
1377
- if (!executeRunId) {
1378
- // marker 缺失:先扫描 execute-runs/ 既有目录找回真实 runId(与 getLatestStageReviewRunId
1379
- // 目录扫描兜底同语义),避免 marker 丢失而 agent 已用旧 runId 落盘时,直接 generate 新 ID
1380
- // 找不到旧 review、误判缺 review.json。仅当确实无既有 run 才 generate 新 ID 并落盘。
1381
- const { generateExecuteRunId, resolveLatestExecuteRunId, stampExecuteRunChange, claimExecuteRunId } = await import('../task-review.js')
1382
- executeRunId = resolveLatestExecuteRunId({ runtimeRoot, changeName }) || ''
1383
- if (!executeRunId) {
1384
- executeRunId = generateExecuteRunId(changeName)
1385
- // 落盘(marker 缺失时 fallback 生成后写盘,保证后续 checkbox/gate 读到同一 ID)
1386
- // D-001#1 fallback 写入点:mkdir execute-runs/<runId>/tasks 先于 marker(不变量:
1387
- // marker 在则目录在)。不 try/catch——异常直穿外层 catch 走 fail-closed 阻断
1388
- //(gate 自身写 run 目录失败不能静默放行完成)。
1389
- // 排他认领(坑 exec-run-id-same-second-collision):并行会话同秒碰撞在认领处消解
1390
- executeRunId = claimExecuteRunId(runtimeRoot, executeRunId)
1391
- mkdirSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'), { recursive: true })
1392
- writeAtomicSync(runIdFile, executeRunId + '\n')
1393
- stampExecuteRunChange(runtimeRoot, executeRunId, changeName)
1394
- }
1395
- }
1396
-
1397
- // git 真实性校验目录:worktree 存在则用 worktree(base/head commit 在其中),否则主仓库。
1398
- // 跨仓支持(task-07 / design §6 gates 行 + D-013):
1399
- // - 有 ctx(execute 启动入口 task-09 构造的 MultiRepoContext 实例)→ 用
1400
- // ctx.resolve('main').gitDir 作为主仓 task 的校验 cwd。ctx 的 main entry 已在
1401
- // _buildMainEntry 内统一编码 worktreePath/cwd + in-place-fallback 兜底(与 task-review.js:724
1402
- // generateTaskReviewDrafts 的 in-place 逻辑同源),避免两处漂移。Task Review Gate 内部循环
1403
- // 再按 review.repo ?? 'main' 从 ctx.resolve(repo).gitDir 切跨仓 task 的 gitDir(task-04 实现)。
1404
- // - 无 ctx(ctx=null 缺省,单仓 / 旧调用链)→ 走原逻辑(meta.worktreePath 或 cwd),零回归。
1405
- let reviewGitDir = cwd
1406
- if (ctx) {
1407
- const mainEntry = ctx.resolve('main')
1408
- if (mainEntry?.gitDir) reviewGitDir = mainEntry.gitDir
1409
- } else {
1410
- try {
1411
- const { WorktreeManager } = await import('../worktree.js')
1412
- const wm = new WorktreeManager({ cwd })
1413
- const meta = wm.getMeta(changeName)
1414
- if (meta?.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
1415
- reviewGitDir = meta.worktreePath
1416
- }
1417
- } catch {}
1418
- }
1419
-
1420
- const reviewResult = validateTaskReviews({ planContent, runtimeRoot, executeRunId, changeDir: planFile, gitDir: reviewGitDir, ctx })
1421
- printReviewResult(reviewResult, { runtimeRoot, executeRunId })
1422
-
1423
- if (!reviewResult.ok) {
1424
- // Task review 校验失败,阻断 execute 完成
1425
- // 检查是否存在 checkbox 已勾但 review 不通过的情况
1426
- const uncheckedTasks = reviewResult.errors.filter(e => e.includes('缺少 review.json'))
1427
- if (uncheckedTasks.length > 0) {
1428
- console.error('\n⚠️ 部分任务已在 tasks.md 中勾选,但 review.json 不存在。')
1429
- console.error(` 请取消勾选这些任务的 checkbox,或补充对应的 review.json(execute run ID: ${executeRunId})。`)
1430
- }
1431
- return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'review_rejected', detail: 'task-review' })
1432
- }
1433
-
1434
- // cannot_verify 的 requiredEvidence 写入 change 目录,供 verify 阶段消费
1435
- if (reviewResult.requiredEvidence.length > 0) {
1436
- const evidencePath = writeVerifyRequiredEvidence(join(effectiveSpecBase, 'changes', changeName), reviewResult.requiredEvidence)
1437
- if (evidencePath) {
1438
- console.log(`📄 verify-required-evidence.json 已写入: ${evidencePath}`)
1439
- console.log(' verify 阶段必须满足这些证据要求。')
1440
- }
1441
- }
1442
- }
1443
- } catch (e) {
1444
- // fail-closed:Gate 自身异常时不能默认放行,否则异常成了绕过评审的通道
1445
- console.error(`❌ Task Review Gate 异常,阻断 execute 完成: ${e.message}`)
1446
- console.error(' 请检查 review.json / plan.md 是否可读,修复后重新完成此步骤。')
1447
- return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'review_rejected', detail: 'task-review' })
1448
- }
1535
+ // ── Execute Task Review Gate 已退役(2026-09-26-task-review-retire)──
1536
+ // 原「所有 task 必须有 review.json 且 verdict 通过」块随层整体退役删除:R18 对撞实证该层在
1537
+ // 无嵌套派发环境全降自审表演(15 次拦截 5 次形式合规、实质拦截为零),前置豁免通道
1538
+ // (review-unsupervised-exit)已覆盖无派发环境。task 粒度审计由 execute 收口的代码证据核验
1539
+ // (checkExecuteCodeEvidence,detectExecuteBatchFinish 内)+ verify 测试对账承担;Stage Review
1540
+ // Gate(阶段粒度)保留。残存 review.json 走 task-review.js 保留导出(历史兼容读侧)。
1541
+ // ── 收集模式检查点 B(brainstorm/plan/execute):纯文档门(validators / Stage Review)
1542
+ // 全量收口——非空则一次打印清单、统一 rollback 一次(R15「连挡 4 次」的解;
1543
+ // detail 取首门,进度库信号语义不变)。verify 走检查点 A(其实测门在前)。──
1544
+ if (stageName !== 'verify' && docGateFailures.length > 0) {
1545
+ console.error(`\n❌ ${stageName} 收口被 ${docGateFailures.length} 道纯文档门拦截(一次性全列——全部修复后重跑一次 --done,不必逐门连撞):`)
1546
+ docGateFailures.forEach((f, i) => console.error(` ${i + 1}. ${f.label}`))
1547
+ console.error(` (各门明细见上方对应 ❌/⚠️ 段落)`)
1548
+ const firstFail = docGateFailures[0]
1549
+ return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: firstFail.type, detail: firstFail.detail })
1449
1550
  }
1450
1551
 
1451
1552
  // ── 仪式档位摩擦升档检查点(task-03 / D-008,design 生命周期契约表「阶段门升档」行)──
@@ -1721,6 +1822,27 @@ export async function completeStageGates({ stageName, cwd, changeName, platformO
1721
1822
  }
1722
1823
  }
1723
1824
 
1825
+ // ── full 流程绑定面补齐(2026-09-27-gate-face-binding-parity,R23-full 实证缺口)──
1826
+ // 绑定槽此前只在 thin flow start 收编时追加(flow.js ensureBindingSlots);full 流程
1827
+ // requirements 由 brainstorm 产出、无绑定面且不经过 flow start → test-trace 恒空 →
1828
+ // FR 入库永远无 tests 绑定,动态门 FR 回归源对 full 出身 FR 失效。此处单源复用
1829
+ // ensureBindingSlots(已有槽/无 FR 块均 no-op),verify --done 的 auto-bind 因槽在场闭环。
1830
+ // best-effort:追加失败只告警不阻断 brainstorm 收尾。
1831
+ if (stageName === 'brainstorm' && changeName) {
1832
+ try {
1833
+ const changeDir = join(specBase, 'changes', changeName)
1834
+ if (existsSync(join(changeDir, 'requirements.md'))) {
1835
+ const { ensureBindingSlots } = await import('../flow-draft.js')
1836
+ const b = ensureBindingSlots({ changeDir })
1837
+ if (b.appended) {
1838
+ console.log(`📌 追加测试绑定槽 ${b.slots} 枚(full 流程 requirements 无绑定面——plan/execute 写测试时作答,verify --done 将从实测结果自动补全空槽)`)
1839
+ }
1840
+ }
1841
+ } catch (e) {
1842
+ console.warn(`⚠️ 绑定槽追加失败(best-effort 不阻断): ${(e && e.message) || e}`)
1843
+ }
1844
+ }
1845
+
1724
1846
  // scan 平台 manifest + post-check(S1 平台受害者:noAI scanPostcheck 末步 / continueStep 现也走这里)
1725
1847
  const _scanResult = await handleScanStageCompleted({ stageName, currentIdx, cwd, progress, pm, stageData, changeName, outputText, platformOpts })
1726
1848
  if (_scanResult) return _scanResult
@@ -1760,7 +1882,7 @@ export async function completeStageGates({ stageName, cwd, changeName, platformO
1760
1882
  pm._write(cwd, progress, changeName)
1761
1883
  }
1762
1884
 
1763
- // 阶段完成校验 gate 级联(runValidators → verify-test → Plan→Execute → Stage Review → Task Review)
1885
+ // 阶段完成校验 gate 级联(runValidators → verify-test → Plan→Execute → Stage Review)
1764
1886
  if (settledCount === total && total > 0) {
1765
1887
  const _gateEarlyReturn = await runStageCompletionGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, ctx })
1766
1888
  if (_gateEarlyReturn) return _gateEarlyReturn
@@ -1890,6 +2012,42 @@ export function writeReconcileRunResult({ runtimeRoot, changeName, envelope, res
1890
2012
  }
1891
2013
  }
1892
2014
 
2015
+ /**
2016
+ * verify --done 阻断明细稳定锚点(quick-D,2026-09-20 报告问题 D):ts 目录(verify-runs/<ts>/
2017
+ * reconcile-result.json 等)的可发现性依赖 stdout——宿主把长命令转后台截断输出后,逐条打印的
2018
+ * ②类明细实际取不回(实证:target_files 阻断明细靠推断绕 3 轮才定位)。稳定路径指针(每次
2019
+ * 覆盖、不随 ts 变)指向本轮最新 verify-runs 目录,agent 无 stdout 也能直读。fail-soft:
2020
+ * 写失败只留痕不阻断 gate(对齐 writeReconcileRunResult 口径)。
2021
+ * @param {{ runtimeRoot: string, changeName: string|null, blocked?: boolean, note?: string }} opts
2022
+ * @returns {string|null} 成功返回指针路径;失败返回 null
2023
+ */
2024
+ export function writeVerifyGatePointer({ runtimeRoot, changeName, blocked = false, note = '' }) {
2025
+ try {
2026
+ const ts = new Date().toISOString().slice(0, 19).replace(/[-T:]/g, '')
2027
+ const runsDir = join(runtimeRoot, 'verify-runs')
2028
+ const pointerPath = join(runsDir, `gate-last-${changeName || 'change'}.json`)
2029
+ mkdirSync(runsDir, { recursive: true })
2030
+ writeFileSync(pointerPath, JSON.stringify({
2031
+ change: changeName,
2032
+ blocked,
2033
+ note,
2034
+ latest_run_dir: join(runsDir, ts),
2035
+ artifacts_hint: [
2036
+ `${ts}/reconcile-result.json(target_files ②类阻断明细在 missing 数组)`,
2037
+ `${ts}/probe-consistency-result.json(探针一致性抽查明细)`,
2038
+ ],
2039
+ written_at: new Date().toISOString(),
2040
+ }, null, 2) + '\n')
2041
+ if (blocked) {
2042
+ console.error(`\n📄 verify 门失败明细稳定锚点(stdout 被截断时直读此文件): ${pointerPath}\n 逐条明细在指针文件指向的 verify-runs/<ts>/ 目录(reconcile-result.json 的 missing 数组等)`)
2043
+ }
2044
+ return pointerPath
2045
+ } catch (e) {
2046
+ console.error(`⚠️ verify 门稳定锚点写入失败(不影响 gate): ${e.message}`)
2047
+ return null
2048
+ }
2049
+ }
2050
+
1893
2051
  /**
1894
2052
  * 打印 target_files 对账结果(task-05 接线配套的打印层)。
1895
2053
  *
@@ -2163,7 +2321,7 @@ export function killProcessTree(pid, sig = 'SIGTERM') {
2163
2321
  if (process.platform === 'win32') {
2164
2322
  // /T = 连同子进程树(shell 包装下的 python/node 服务本体);/F = 强制(长驻服务常吞 SIGTERM 等价物)
2165
2323
  try {
2166
- execFileSync('taskkill', ['/PID', String(pidNum), '/T', '/F'], { stdio: 'ignore', timeout: 15000 })
2324
+ execFileSync('taskkill', ['/PID', String(pidNum), '/T', '/F'], { stdio: 'ignore', timeout: 15000, windowsHide: true })
2167
2325
  return true
2168
2326
  } catch (e) {
2169
2327
  // taskkill 对不存在进程退出码 128(等价 ESRCH,已退出按回收静默);其余(拒绝访问等)上抛
@@ -2182,7 +2340,7 @@ export function killProcessTree(pid, sig = 'SIGTERM') {
2182
2340
  /** ps 枚举直接子进程 PID(POSIX;失败返回空——杀不到子进程不阻断父进程回收) */
2183
2341
  function listChildPids(parentPid) {
2184
2342
  try {
2185
- const out = execFileSync('ps', ['-axo', 'pid=,ppid='], { encoding: 'utf8', timeout: 5000 })
2343
+ const out = execFileSync('ps', ['-axo', 'pid=,ppid='], { encoding: 'utf8', timeout: 5000, windowsHide: true })
2186
2344
  const children = []
2187
2345
  for (const line of String(out).split('\n')) {
2188
2346
  const m = line.trim().match(/^(\d+)\s+(\d+)$/)
@@ -2223,3 +2381,59 @@ export function reapVerifyServices(platformOpts, specBase, changeName) {
2223
2381
  if (failedAll.length > 0) console.warn(`⚠️ 服务进程回收失败 ${failedAll.length} 个:${failedAll.join(', ')} — 手动处理:taskkill /PID <pid> /F(Windows)/ kill -9 <pid>`)
2224
2382
  return { reaped: reapedTotal, receiptPath }
2225
2383
  }
2384
+
2385
+
2386
+ /**
2387
+ * buildArchiveReadinessReport —— 归档就绪度报告构造(P3 / FR-03 / D-003@batch3,task-04)。
2388
+ * verify PASS 后、archive 前的三查前置(batch2 实证三查全压 archive 时段首查=15 回合主因)。
2389
+ * 纯读:①未-apply 交付面(applyWorktree checkOnly,含 ql-009②/ql-010 合并感知口径)②manifest
2390
+ * 缺行草拟集(面文件不在 design §6 清单——草拟不代写,agent 确认后自落)③module-impact 归因
2391
+ * 草拟集(面文件不在 module-impact.md 文本)④基线老化命中(main HEAD 前进过 worktree 基点
2392
+ * ∩ 前进文件与交付面有交集)。无 worktree(meta 缺/in-place/native)→ 全空(零噪音)。
2393
+ * fail-open:任一探测异常只置空该项,不抛(渲染层再兜一层)。
2394
+ */
2395
+ export async function buildArchiveReadinessReport({ cwd, changeName, progress, specBase }) {
2396
+ const out = { face: [], manifestGaps: [], impactUnlisted: [], agingHit: false }
2397
+ try {
2398
+ const { WorktreeManager } = await import('../worktree.js')
2399
+ const wm = new WorktreeManager({ cwd })
2400
+ const meta = wm.getMeta(changeName)
2401
+ if (!meta || meta.mode === 'in-place-fallback' || meta.mode === 'native-worktree'
2402
+ || !meta.worktreePath || !existsSync(meta.worktreePath)) return out
2403
+ const { applyWorktree } = await import('../worktree-apply.js')
2404
+ const probe = applyWorktree(changeName, { cwd, checkOnly: true })
2405
+ out.face = (probe && Array.isArray(probe.changedFiles)) ? probe.changedFiles : []
2406
+ if (out.face.length === 0) return out
2407
+ // ② manifest 缺行草拟
2408
+ try {
2409
+ const changeDir = resolveChangeDir(cwd, progress || {}, specBase)
2410
+ if (changeDir) {
2411
+ const designPath = join(changeDir, 'design.md')
2412
+ if (existsSync(designPath)) {
2413
+ const { parseFileChangeList } = await import('../change-list.js')
2414
+ const declared = parseFileChangeList(designPath, { keepSillyspecDocs: true })
2415
+ out.manifestGaps = out.face.filter(f => !declared.has(f))
2416
+ }
2417
+ }
2418
+ } catch { /* 草拟失败置空该项 */ }
2419
+ // ③ module-impact 归因草拟
2420
+ try {
2421
+ const changeDir2 = resolveChangeDir(cwd, progress || {}, specBase)
2422
+ const miPath = changeDir2 ? join(changeDir2, 'module-impact.md') : null
2423
+ const miText = (miPath && existsSync(miPath)) ? readFileSync(miPath, 'utf8') : ''
2424
+ out.impactUnlisted = out.face.filter(f => !miText.includes(f))
2425
+ } catch { /* 同上 */ }
2426
+ // ④ 基线老化命中
2427
+ try {
2428
+ if (meta.baseHash) {
2429
+ const { gitQuiet } = await import('../git-helper.js')
2430
+ const advanced = gitQuiet(cwd, ['diff', '--name-only', meta.baseHash + '..HEAD'])
2431
+ if (advanced) {
2432
+ const advSet = new Set(advanced.split('\n').filter(Boolean))
2433
+ out.agingHit = out.face.some(f => advSet.has(f))
2434
+ }
2435
+ }
2436
+ } catch { /* git 失败不提示 */ }
2437
+ } catch { /* 整体 fail-open → 全空 */ }
2438
+ return out
2439
+ }