sillyspec 3.28.9 → 3.28.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 (55) hide show
  1. package/.claude/skills/sillyspec-execute/SKILL.md +223 -222
  2. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  3. package/.claude/skills/sillyspec-quick/SKILL.md +8 -10
  4. package/.claude/skills/sillyspec-verify/SKILL.md +91 -87
  5. package/package.json +1 -1
  6. package/src/change-list.js +73 -20
  7. package/src/change-risk-profile.js +51 -3
  8. package/src/config-schema.js +51 -0
  9. package/src/contract-matrix.js +84 -5
  10. package/src/cross-repo-reconcile.js +134 -0
  11. package/src/design-facts.js +94 -18
  12. package/src/docs-check.js +14 -10
  13. package/src/docs-debt.js +7 -2
  14. package/src/docs-gate.js +42 -15
  15. package/src/endpoint-extractor.js +50 -2
  16. package/src/foreign-declared.js +25 -16
  17. package/src/hooks/worktree-guard.js +75 -2
  18. package/src/index.js +147 -2
  19. package/src/knowledge-classify.js +20 -1
  20. package/src/machine-interface.js +26 -0
  21. package/src/probe7-anchor-check.js +87 -0
  22. package/src/progress/stage-machine.js +34 -5
  23. package/src/progress.js +1 -1
  24. package/src/quicklog.js +240 -5
  25. package/src/run/command.js +25 -2
  26. package/src/run/complete-handlers.js +172 -1
  27. package/src/run/complete.js +143 -11
  28. package/src/run/gate-snapshot.js +372 -2
  29. package/src/run/gates.js +130 -12
  30. package/src/run/prompt.js +166 -7
  31. package/src/run/shared.js +81 -1
  32. package/src/scope-audit.js +101 -31
  33. package/src/stage-contract-spec.js +14 -0
  34. package/src/stage-contract.js +314 -8
  35. package/src/stage-review-checklist.js +3 -1
  36. package/src/stage-review.js +137 -2
  37. package/src/stages/archive.js +9 -5
  38. package/src/stages/brainstorm.js +7 -1
  39. package/src/stages/cmd-existence.js +9 -3
  40. package/src/stages/execute.js +62 -8
  41. package/src/stages/knowledge.js +24 -3
  42. package/src/stages/plan-postcheck.js +74 -10
  43. package/src/stages/plan.js +8 -1
  44. package/src/stages/quick.js +5 -5
  45. package/src/stages/verify.js +3 -2
  46. package/src/sync-noise.js +57 -1
  47. package/src/sync.js +77 -4
  48. package/src/task-review.js +84 -29
  49. package/src/verify-facts-schema.js +78 -9
  50. package/src/verify-postcheck.js +439 -68
  51. package/src/verify-probes.js +1156 -45
  52. package/src/worktree-apply.js +323 -8
  53. package/src/worktree-deps.js +24 -1
  54. package/src/worktree.js +274 -10
  55. package/src/write-audit.js +55 -0
@@ -17,6 +17,9 @@ import { evaluateRules } from './stage-contract-engine.js'
17
17
  import { getRule } from './stage-contract-spec.js'
18
18
  import { backfillFrontmatter } from './scan-postcheck.js'
19
19
  import { parseAllowedPaths } from './stages/plan-postcheck.js'
20
+ // design.md 文件清单解析(2026-09-17-pass-cap-semantics task-02 事实③):change-list 是纯 fs
21
+ // 叶子模块(design-facts / scope-audit / verify-probes 等同款直连惯例),静态引入无环。
22
+ import { parseFileChangeListDetailed } from './change-list.js'
20
23
 
21
24
  /**
22
25
  * 校验结果
@@ -403,8 +406,17 @@ function validatePlanOutputs(cwd, changeName, context = {}) {
403
406
  const sm = fm && fm[1].match(/^scale:[ \t]*['"]?(\w+)/m)
404
407
  if (sm) scale = sm[1]
405
408
  }
409
+ // plan.md frontmatter plan_level 传入引擎 ctx(ql-20260917-003):plan.global-constraints warning
410
+ // 的 condition 用。读不到(无 frontmatter/旧格式)→ undefined → eq:'full' 不成立 → 规则跳过
411
+ // (存量零误报 fail-safe,同 scale 读取模式)。
412
+ let planLevel = null
413
+ if (existsSync(planFile)) {
414
+ const pfm = readFileSync(planFile, 'utf8').match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/)
415
+ const plm = pfm && pfm[1].match(/^plan_level:[ \t]*['"]?(\w+)/m)
416
+ if (plm) planLevel = plm[1]
417
+ }
406
418
  // plan.md 存在性由引擎消费 manifest。entryPoint/id-trace/decisions 为 custom kind(下方保留)。
407
- const engineResult = evaluateRules('plan', { changeDir, scale })
419
+ const engineResult = evaluateRules('plan', { changeDir, scale, planLevel })
408
420
  const errors = [...engineResult.errors]
409
421
  const warnings = [...engineResult.warnings]
410
422
 
@@ -634,12 +646,23 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
634
646
  `若属关键词误伤(实际未触碰 daemon/session/启动入口/跨进程),可在 design.md frontmatter 加 risk_level: <真实等级>(如 unit-sufficient)显式覆盖后重跑。`
635
647
  )
636
648
  }
637
- // 显式 risk_level 声明(design frontmatter,非关键词误判)下,PASS WITH NOTES 视为对残留项的
638
- // 诚实声明,不强求全量集成证据——豁免本就来自 design 的明确判断。自动关键词判级维持严格:
639
- // PASS WITH NOTES 仍要求证据齐全,防 agent 用 PASS WITH NOTES 绕证据门控。
640
- const requiresEvidence = conclusion === 'PASS' || (conclusion === 'PASS WITH NOTES' && !changeRiskProfile.explicit)
641
- if (conclusion === 'PASS WITH NOTES' && changeRiskProfile.explicit) {
642
- warnings.push(`[${changeRiskProfile.level}] 结论 PASS WITH NOTES:design frontmatter 显式声明 risk_level=${changeRiskProfile.level},残留项须为真实集成证据缺口,并在 verify-result.md 如实说明`)
649
+ // ── 显式 risk_level 豁免洞分层(D-002@v1,2026-09-17-pass-cap-semantics task-02)──
650
+ // ① explicit + 降级 unit-sufficient/contract-required:不进本 critical 块(上方 level 门),
651
+ // PASS WITH NOTES 维持免证据——关键词误伤逃生保留;
652
+ // ② explicit 且仍 integration/deployment-critical + PASS WITH NOTES:必须携带结构化 handover
653
+ // (facts.handover 有效行;blocking 级计入封顶口径同 validatePassEligibility 条件②)或齐全
654
+ // 集成证据,二选一——无 handover 即挂证据门(checkIntegrationEvidence 不过 → error);
655
+ // ③ 非显式(关键词判级)判定式一字不动(防 PASS WITH NOTES 绕证据门控)。
656
+ const notesExplicitCritical = conclusion === 'PASS WITH NOTES' && changeRiskProfile.explicit
657
+ const handoverRows = notesExplicitCritical ? countFactsHandoverItems(changeDir) : 0
658
+ const requiresEvidence = conclusion === 'PASS'
659
+ || (conclusion === 'PASS WITH NOTES' && (!changeRiskProfile.explicit || handoverRows === 0))
660
+ if (notesExplicitCritical) {
661
+ if (handoverRows > 0) {
662
+ warnings.push(`[${changeRiskProfile.level}] 结论 PASS WITH NOTES:design frontmatter 显式声明 risk_level=${changeRiskProfile.level},缺口由结构化移交项承载(facts.handover ${handoverRows} 行,「## 移交项(结构化)」)——blocking 级行在结论=PASS 时按封顶口径拦截(validatePassEligibility 条件②),请如实分行勿漏报。`)
663
+ } else {
664
+ warnings.push(`[${changeRiskProfile.level}] 结论 PASS WITH NOTES:显式声明 risk_level=${changeRiskProfile.level} 且「## 移交项(结构化)」零有效行——按齐全集成证据口径校验(二选一:补结构化移交项承载缺口,或提供齐全集成证据;两者皆缺将 error)。`)
665
+ }
643
666
  }
644
667
  if (requiresEvidence) {
645
668
  // CLI 回执注入(坑 verify-literal-evidence-mismatch,2026-08-22 实证:证据第一轮就齐
@@ -677,6 +700,11 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
677
700
  extraEvidenceText: receiptText,
678
701
  ...(runtimeEvidence ? { runtimeEvidence } : {}),
679
702
  ...(context.verifyStartAt ? { verifyStartAt: context.verifyStartAt } : {}),
703
+ // 回执来源声明透传(X-10 / D-006,2026-09-17-pass-cap-semantics task-02):调用侧只透传
704
+ // context 携带的声明源(quality-scan 记录的命令 / verify_precedents 声明同思想),不在
705
+ // 本侧做任何分类——分类打标单点在 change-risk-profile(auditRuntimeReceipt 内按回执
706
+ // command 来源 classifyReceiptSourceTag);未透传时按各回执 command 就地分类。
707
+ ...(context.receiptSourceTag ? { sourceTag: context.receiptSourceTag } : {}),
680
708
  cwd,
681
709
  specBase: context.specRoot || join(cwd, '.sillyspec'),
682
710
  })
@@ -820,6 +848,12 @@ export function extractAcceptanceMatrixSlots(verifyMd) {
820
848
  * - 段缺失:严格档(isIrStrictVerifyChange / IR_STRICT_SINCE 同源常量,与结论槽
821
849
  * 「删槽回退已关闭」同口径)ERROR;非严格(存量变更)warning 提示补段不阻断
822
850
  * - 段在场:unfilled / missingEvidence > 0 → ERROR 列出违规行(task+acceptance 截断 40 字)
851
+ * - 段在场:partial/uncovered 行 >0 且 facts.handover 零有效行(任意 severity)→ ERROR
852
+ * 「部分实现必须有移交去向」(D-003/FR-04,task-03;矩阵 MD 槽解析即防篡改锚点 X-05——
853
+ * 不读 facts.matrixPartialRows producer 快照,锚定当前文档实态;factsExpected=false 存量
854
+ * 零行为变化,factsExpected=true 而 facts 缺失 → fail-closed 按零有效行处理)
855
+ * - 同分支放行路径 advisory(R-05 第一版):handover 有效行在场时逐行核对行标识(task 锚 /
856
+ * acceptance 文本)↔ 条目 item/condition 文本命中,未命中 → console.warn 攒实证,不阻断
823
857
  */
824
858
  function validateAcceptanceMatrix(cwd, changeName, context = {}) {
825
859
  const { specRoot } = context
@@ -867,9 +901,279 @@ function validateAcceptanceMatrix(cwd, changeName, context = {}) {
867
901
  `修复:在证据列补测试锚点(如 \`test/foo.test.mjs\` 或 \`src/x.js:42\`)或 non-testable 理由。`
868
902
  )
869
903
  }
904
+
905
+ // ── partial/uncovered × facts.handover 联动(D-003/FR-04,2026-09-17-pass-cap-semantics task-03)──
906
+ // 部分实现必须有移交去向:矩阵含 partial/uncovered 行且 facts.handover 零有效行(任意
907
+ // severity)→ error;有行 → 本分支放行,blocking 级是否封顶归 validatePassEligibility 条件②
908
+ // (④管「有去向」、②管「去向是否 blocking」)。handover 数据只读 facts.handover(task-01
909
+ // producer 四列产出;不在此重复解析 MD 移交项表——防篡改锚点由上方矩阵 MD 槽解析承担,X-05)。
910
+ // factsExpected 口径复用 task-02 判定式:false(存量未跑管线)→ 分支空转零行为变化;
911
+ // true 而 facts 缺失 → 按零有效行 fail-closed,文案附「重跑 verify-probes」出路。
912
+ // 放行路径上做逐行关联 advisory(R-05 第一版:验收项 ID ↔ handover 条目文本命中,只攒实证
913
+ // 不做硬门):行标识(task / acceptance 文本)未命中任何条目 → console.warn 提示,不阻断不进 errors。
914
+ const partialRows = matrix.rows.filter(r => r.verdict === 'partial' || r.verdict === 'uncovered')
915
+ if (partialRows.length > 0 && resolveFactsExpected(changeDir)) {
916
+ const facts = readFactsForEligibility(changeDir)
917
+ const factsMissing = facts === null
918
+ // 有效行口径同 countFactsHandoverItems(task-02):只计对象行;此处需逐条取文本故就地展开
919
+ const handoverItems = facts && facts.handover && Array.isArray(facts.handover.items)
920
+ ? facts.handover.items.filter(it => it && typeof it === 'object') : []
921
+ if (handoverItems.length === 0) {
922
+ const list = partialRows.map(rowLabel).join(';')
923
+ errors.push(
924
+ `探针 7 验收×测试覆盖矩阵含 ${partialRows.length} 行 partial/uncovered 且「## 移交项(结构化)」零有效行(facts.handover)——部分实现必须有移交去向:${list}。` +
925
+ `修复:在 verify-result.md 补「## 移交项(结构化)」有效行(任意 severity 均可,blocking 级另受 PASS 封顶约束),或修正矩阵判定(实际已覆盖的行改 covered),或降级结论为 PASS WITH NOTES 承载。` +
926
+ (factsMissing
927
+ ? `另:verify-facts.json 缺失/不可读而 factsExpected=true,fail-closed 按零有效行处理——重跑 verify-probes(\`sillyspec verify-probes --change ${changeName} --init\` 幂等刷新底稿)。`
928
+ : '')
929
+ )
930
+ } else {
931
+ // 逐行关联 advisory(R-05 第一版,D-003):行标识(task 锚 / acceptance 文本)命中任一
932
+ // handover 条目的 item/condition 文本即视为有去向;未命中 → console.warn 攒实证(文本
933
+ // 命中是弱关联,误报/漏报均可能——先 advisory 攒数据,硬门等实证后再议),不阻断。
934
+ const itemTexts = handoverItems.map(it => `${it.item || ''}|${it.condition || ''}`)
935
+ for (const r of partialRows) {
936
+ const keys = [r.task, r.acceptance].map(k => String(k || '').trim()).filter(Boolean)
937
+ const hit = keys.some(k => itemTexts.some(t => t.includes(k)))
938
+ if (!hit) {
939
+ console.warn(`ℹ️ [advisory] 验收项 ${rowLabel(r)} 的移交去向未在 handover 条目中命中——建议条目文本含该标识(advisory,攒实证,D-003/R-05;不阻断)`)
940
+ }
941
+ }
942
+ }
943
+ }
870
944
  return { ok: errors.length === 0, errors, warnings }
871
945
  }
872
946
 
947
+ // ============ PASS 封顶事实面 validator(2026-09-17-pass-cap-semantics task-02 / D-001@v2 / D-010 / D-011) ============
948
+ //
949
+ // 「要不要集成证据」(validateVerifyOutputs 的 requiresEvidence)与「能不能写 PASS」(本
950
+ // validator)解耦:结论=PASS 时四个已知未验证区任一在场即 error——
951
+ // ① 集成实测未跑(facts.integrationRan=not-ran,D-006 判定表)
952
+ // ② blocking 级移交项在场(facts.handover[].severity,D-005@v2——④管「有去向」、②管「去向是否 blocking」)
953
+ // ③ db 脚本未声明执行(verify 时点文件集 ∩ db/**/*.sql 对账 facts.dbScriptDeclarations,D-012)
954
+ // ④ 矩阵含 partial/uncovered 且移交项零有效行(facts.matrixPartialRows × facts.handover)
955
+ // 附加:runtimeEndpointExcluded=true 且判级 integration/deployment-critical 且 handover 零行
956
+ // → 计入①事实面(D-004/FR-03,仅判级时计入)。
957
+ //
958
+ // 分层纪律(D-011):事实生产走 facts 管线(verify-probes backfillFactsFromMdAndTests 首次
959
+ // backfill 主路径无条件产出,X-08 时序——gates 收尾前置 backfill 先于 runValidators);本壳只做
960
+ // 取数组装,判定是 evaluatePassEligibility 纯函数(零 MD 解析、零 IO——MD 锚点防篡改兜底并入
961
+ // checkProbeConsistency 抽查面属 task-03)。双源 fail-closed:factsExpected=true 而 facts 缺失
962
+ // (篡改/管线故障)→ 全条件按触发拦下;factsExpected=false(存量未跑 --init)→ 兼容 ok 不误伤。
963
+
964
+ /**
965
+ * 读 <changeDir>/verify-facts.json(缺失/不可解析/非对象 → null)——与 checkProbeConsistency
966
+ * 的 readVerifyFacts(verify-postcheck.js)同口径「有 facts」判别。不静态 import
967
+ * verify-postcheck/verify-probes(verify-probes 顶层 await 动态 import 回指本模块,静态依赖
968
+ * 会在其 TLA 上成环死锁;分层单向,全局硬约束 3),就地等价实现。
969
+ */
970
+ function readFactsForEligibility(changeDir) {
971
+ const factsPath = join(changeDir, 'verify-facts.json')
972
+ if (!existsSync(factsPath)) return null
973
+ try {
974
+ const parsed = JSON.parse(readFileSync(factsPath, 'utf8'))
975
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null
976
+ } catch { return null }
977
+ }
978
+
979
+ /**
980
+ * factsExpected 判定(D-011 双源 fail-closed 的「应写」侧):isIrStrictVerifyChange(严格档
981
+ * 变更删 facts.json 另由 checkProbeConsistency error 级 MD 锚点兜底拦,通道 gates probeBlocked)
982
+ * || verify-facts.json 在场(readVerifyFacts 同口径,含探针子节判别的「有 facts」面)。
983
+ * factsExpected=false(存量未跑管线)→ 封顶兼容 skip 不误伤。
984
+ * @param {string} changeDir 变更目录
985
+ * @returns {boolean}
986
+ */
987
+ export function resolveFactsExpected(changeDir) {
988
+ return isIrStrictVerifyChange(changeDir) || readFactsForEligibility(changeDir) !== null
989
+ }
990
+
991
+ /** facts.handover 有效行数(producer parseHandoverRows 已滤占位/表头行;无 facts/handover → 0) */
992
+ function countFactsHandoverItems(changeDir) {
993
+ const facts = readFactsForEligibility(changeDir)
994
+ const items = facts && facts.handover && Array.isArray(facts.handover.items) ? facts.handover.items : []
995
+ return items.filter(it => it && typeof it === 'object').length
996
+ }
997
+
998
+ /**
999
+ * PASS 资格事实面封顶纯函数(X-09 双层形态的判定层:零 MD 解析、零 IO,消费 task-01 契约字段)。
1000
+ * @param {{
1001
+ * conclusion?: string|null,
1002
+ * facts?: object|null,
1003
+ * factsExpected?: boolean,
1004
+ * changeRiskProfile?: { level?: string }|null,
1005
+ * changeName?: string,
1006
+ * dbScriptCandidates?: string[],
1007
+ * }} [args] dbScriptCandidates = 壳内取数的 verify 时点文件集(design 清单 ∪ worktree 变更)∩ db 目录任意层级 .sql
1008
+ * @returns {{ ok: boolean, errors: string[], triggered: Array<{ fact: 'integration-not-run'|'blocking-handover-present'|'db-script-undeclared'|'matrix-partial-no-handover', detail: string }> }}
1009
+ */
1010
+ export function evaluatePassEligibility(args) {
1011
+ const errors = []
1012
+ const triggered = []
1013
+ const a = args || {}
1014
+ // factsExpected=false(存量未跑管线)→ 沿用存量兼容口径 ok 不误伤(checkProbeConsistency 同款边界)
1015
+ if (!a.factsExpected) return { ok: true, errors, triggered }
1016
+ // 封顶只管 PASS:结论 ≠ PASS(FAIL / PASS WITH NOTES / 未识别未填)→ 直接 ok
1017
+ if (a.conclusion !== 'PASS') return { ok: true, errors, triggered }
1018
+
1019
+ const factsMissing = !a.facts || typeof a.facts !== 'object'
1020
+ const facts = factsMissing ? {} : a.facts
1021
+ const handoverItems = (facts.handover && Array.isArray(facts.handover.items)) ? facts.handover.items : []
1022
+ const blockingRows = handoverItems.filter(it => it && it.severity === 'blocking')
1023
+ const criticalLevel = a.changeRiskProfile && ['integration-critical', 'deployment-critical'].includes(a.changeRiskProfile.level)
1024
+ const NOTES_FIX = '改写结论为 PASS WITH NOTES 并在「## 移交项(结构化)」表格如实分行(类型枚举 env-blocked/manual-acceptance/db-script/other)'
1025
+
1026
+ // ── factsExpected=true 而 facts 缺失 → 双源 fail-closed:全条件按触发(D-011),附重跑 verify-probes 出路(R-07)──
1027
+ if (factsMissing) {
1028
+ errors.push(
1029
+ `[pass-eligibility] verify-facts.json 缺失/不可读但 factsExpected=true(严格档或底稿曾在场后消失——疑似删除/管线故障),按双源 fail-closed 全条件触发拦下。` +
1030
+ `出路:重跑 verify-probes(sillyspec verify-probes --change ${a.changeName || '<变更名>'} --init 幂等刷新底稿后重跑 --done 回填),或${NOTES_FIX}降级承载。`
1031
+ )
1032
+ }
1033
+
1034
+ // ① 集成实测未跑(时序口径 X-01:validator 时点只认已落盘 quality-scan 记录,被拦出路=重跑质量扫描步或降级 NOTES,不算失败)
1035
+ if (factsMissing || facts.integrationRan === 'not-ran') {
1036
+ triggered.push({ fact: 'integration-not-run', detail: factsMissing ? 'facts 缺失(fail-closed 按未跑处理)' : 'facts.integrationRan=not-ran' })
1037
+ errors.push(
1038
+ `[fact integration-not-run] 集成实测未跑(${factsMissing ? 'verify-facts.json 缺失,fail-closed 按未跑处理' : 'facts.integrationRan=not-ran'})——结论=PASS 须集成实测已跑(D-006 判定表:quality-scan 实测记录(skip 除外)或跨层回执条目)。` +
1039
+ `出路:重跑质量扫描步落实测记录,或提供跨层实测回执后重跑 verify-probes 刷新 facts;仍未跑则${NOTES_FIX}承载(不算失败)。`
1040
+ )
1041
+ }
1042
+
1043
+ // ② blocking 级移交项在场(blocking 移交项与结论 PASS 互斥)
1044
+ if (factsMissing || blockingRows.length > 0) {
1045
+ const list = blockingRows.map(it => `${it.type || '未知类型'}|${String(it.item || '').slice(0, 60)}`).join(';')
1046
+ triggered.push({ fact: 'blocking-handover-present', detail: factsMissing ? 'facts 缺失(fail-closed)' : `${blockingRows.length} 条 blocking 行:${list}` })
1047
+ errors.push(
1048
+ `[fact blocking-handover-present] ${factsMissing
1049
+ ? '移交项 blocking 级行无法核对(verify-facts.json 缺失,fail-closed 按在场触发)'
1050
+ : `移交项含 ${blockingRows.length} 条 blocking 级行——承认有必须兜底的未竟事项即不是 PASS(④管「有去向」、②管「去向是否 blocking」)`}。` +
1051
+ `触发行:${factsMissing ? '(facts 缺失,无法核对——fail-closed)' : list}。修复:${NOTES_FIX};确属 advisory 的按降级文法「(降级:<理由>,依据 <file:line 或 D-xxx>)」显式降级。`
1052
+ )
1053
+ }
1054
+
1055
+ // ③ db 脚本未声明执行(D-012:verify 时点按声明面/diff 对账,apply/archive 兜底门归 task-04;零连库)
1056
+ const candidates = Array.isArray(a.dbScriptCandidates) ? a.dbScriptCandidates : []
1057
+ const declared = factsMissing ? [] : (Array.isArray(facts.dbScriptDeclarations) ? facts.dbScriptDeclarations : [])
1058
+ const undeclared = candidates.filter(f => !declared.includes(f))
1059
+ if (undeclared.length > 0) {
1060
+ triggered.push({ fact: 'db-script-undeclared', detail: `${undeclared.join('、')} 未进 facts.dbScriptDeclarations` })
1061
+ errors.push(
1062
+ `[fact db-script-undeclared] db 脚本在 verify 时点文件集(design 清单 ∪ worktree 变更)∩ db/**/*.sql 中但未声明执行:${undeclared.join('、')}。` +
1063
+ `修复:已对目标库执行则在 verify-result.md 补声明(「已对目标库执行:db/<file>.sql」或回执槽 command 含该文件)后重跑 verify-probes 刷新 facts;未执行则${NOTES_FIX}承载(db-script 类恒 blocking)。`
1064
+ )
1065
+ }
1066
+
1067
+ // ④ 矩阵含 partial/uncovered 且移交项零有效行(任意 severity——部分实现必须有移交去向)
1068
+ if (factsMissing || ((facts.matrixPartialRows || 0) > 0 && handoverItems.length === 0)) {
1069
+ triggered.push({ fact: 'matrix-partial-no-handover', detail: factsMissing ? 'facts 缺失(fail-closed)' : `matrixPartialRows=${facts.matrixPartialRows} 且 handover 零有效行` })
1070
+ errors.push(
1071
+ `[fact matrix-partial-no-handover] 验收×测试覆盖矩阵含 ${factsMissing ? '(facts 缺失,无法核对——fail-closed)' : `${facts.matrixPartialRows} 行 partial/uncovered`} 且「## 移交项(结构化)」零有效行——部分实现必须有移交去向。修复:${NOTES_FIX}承载(任意 severity 均可,blocking 级另受条件②约束)。`
1072
+ )
1073
+ }
1074
+
1075
+ // ── 附加条件(D-004/FR-03,仅判级 integration/deployment-critical 时计入①事实面):
1076
+ // Runtime Evidence「服务端点」行自声明「不涉及」(facts.runtimeEndpointExcluded,X-18 文法)
1077
+ // 且移交项零行——端点不得以「不涉及」免检,须真实回执或 handover 承载。
1078
+ if (criticalLevel && (factsMissing || facts.runtimeEndpointExcluded === true) && handoverItems.length === 0) {
1079
+ triggered.push({
1080
+ fact: 'integration-not-run',
1081
+ detail: factsMissing ? 'facts 缺失(fail-closed,runtimeEndpointExcluded 不可证伪)' : 'facts.runtimeEndpointExcluded=true 且 handover 零行',
1082
+ })
1083
+ errors.push(
1084
+ `[fact integration-not-run] 判级 ${a.changeRiskProfile.level} 且 Runtime Evidence 服务端点行自声明「不涉及」(facts.runtimeEndpointExcluded=true)且无移交项承载——端点不得以「不涉及」免检(D-004)。` +
1085
+ `出路:提供真实端点回执(跨层实测)后重跑 verify-probes,或${NOTES_FIX}承载。`
1086
+ )
1087
+ }
1088
+
1089
+ return { ok: errors.length === 0, errors, triggered }
1090
+ }
1091
+
1092
+ /**
1093
+ * 事实③文件集取数(D-012,纯声明面/diff 对账零连库):design.md 文件清单
1094
+ * (change-list.parseFileChangeListDetailed——design-facts.validateDesignFileList 同源解析器)
1095
+ * ∪ worktree changed files(worktree meta baseHash..HEAD diff ∪ porcelain——checkExecuteCodeEvidence
1096
+ * 同款取数形态,与 task-review resolveAttributionDiffFiles 内 resolveVerifyChangedFiles 同源口径;
1097
+ * 本模块不静态 import verify-postcheck/verify-probes,TLA 动态环,就地等价实现)∩ db 目录任意层级 .sql。
1098
+ * @param {{ cwd: string, changeName: string, specRoot?: string|null, changeDir: string }} opts
1099
+ * @returns {string[]} 排序后的候选 db 脚本(posix 相对路径)
1100
+ */
1101
+ function collectDbScriptCandidates({ cwd, changeName, specRoot, changeDir }) {
1102
+ const files = new Set()
1103
+ const add = (p) => {
1104
+ const n = String(p || '').trim().replace(/^"|"$/g, '').replace(/\\/g, '/')
1105
+ if (n && n !== '.sillyspec' && !n.startsWith('.sillyspec/')) files.add(n)
1106
+ }
1107
+ // design 清单半边(design.md 缺失/无清单段 → 空集,fail-soft,diff 半边仍覆盖)
1108
+ try {
1109
+ for (const entry of parseFileChangeListDetailed(join(changeDir, 'design.md'))) add(entry && entry.path)
1110
+ } catch { /* 解析异常退 diff 半边 */ }
1111
+ // worktree 变更半边:meta 双根候选(specBase 优先 + cwd/.sillyspec 兜底,_readWorktreeMeta 样板)
1112
+ try {
1113
+ const specBase = specRoot || join(cwd, '.sillyspec')
1114
+ const metaCandidates = [
1115
+ join(specBase, '.runtime', 'worktrees', changeName, 'meta.json'),
1116
+ join(cwd, '.sillyspec', '.runtime', 'worktrees', changeName, 'meta.json'),
1117
+ ]
1118
+ let meta = null
1119
+ for (const p of metaCandidates) {
1120
+ if (existsSync(p)) {
1121
+ try { meta = JSON.parse(readFileSync(p, 'utf8')) } catch { meta = null }
1122
+ if (meta) break
1123
+ }
1124
+ }
1125
+ if (meta && meta.baseHash) {
1126
+ const gitDir = (meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath))
1127
+ ? meta.worktreePath
1128
+ : cwd
1129
+ const diff = gitTry(gitDir, ['diff', '--name-only', `${meta.baseHash}..HEAD`])
1130
+ if (diff.ok) for (const f of diff.out.split('\n')) add(f)
1131
+ const status = gitTry(gitDir, ['status', '--porcelain'])
1132
+ if (status.ok) {
1133
+ for (const line of status.out.split('\n')) {
1134
+ if (!line.trim()) continue
1135
+ add(line.slice(3).trim().split(' -> ').pop())
1136
+ }
1137
+ }
1138
+ }
1139
+ } catch { /* meta/git 取数失败退 design 清单半边(fail-soft) */ }
1140
+ return [...files].filter(p => /^db\/.+\.sql$/i.test(p)).sort()
1141
+ }
1142
+
1143
+ /**
1144
+ * PASS 封顶 validator 注册壳(与 validateAcceptanceMatrix 同三参签名同构,D-010:注册进
1145
+ * contracts.verify.validators 即覆盖 gates / machine-interface 等全部 runValidators 调用方,
1146
+ * gates.js 零改动)。壳内取数组装:verify-result.md 结论(槽优先,legacy 回退)、
1147
+ * verify-facts.json(缺失容 null)、detectChangeRisk 判级、事实③文件集;判定调
1148
+ * evaluatePassEligibility 纯函数。
1149
+ */
1150
+ export function validatePassEligibility(cwd, changeName, context = {}) {
1151
+ const { specRoot } = context
1152
+ const errors = []
1153
+ const warnings = []
1154
+ const changeDir = resolveChangeDir(cwd, changeName, specRoot)
1155
+ // 变更目录缺失 → 引擎 manifest 存在性规则兜底(validateVerifyOutputs 同名 error),此处 no-op
1156
+ if (!existsSync(changeDir)) return { ok: true, errors, warnings }
1157
+ // verify-result.md 中间步骤未落盘 → no-op(同 validateAcceptanceMatrix:存在性归引擎末步规则)
1158
+ const verifyResultPath = join(changeDir, 'verify-result.md')
1159
+ if (!existsSync(verifyResultPath)) return { ok: true, errors, warnings }
1160
+
1161
+ const conclusion = resolveVerifyConclusion(readFileSync(verifyResultPath, 'utf8'))
1162
+ const facts = readFactsForEligibility(changeDir)
1163
+ const result = evaluatePassEligibility({
1164
+ conclusion,
1165
+ facts,
1166
+ factsExpected: isIrStrictVerifyChange(changeDir) || facts !== null,
1167
+ changeRiskProfile: detectChangeRisk({
1168
+ designContent: readIfExists(join(changeDir, 'design.md')),
1169
+ planContent: readIfExists(join(changeDir, 'plan.md')),
1170
+ }),
1171
+ changeName,
1172
+ dbScriptCandidates: collectDbScriptCandidates({ cwd, changeName, specRoot, changeDir }),
1173
+ })
1174
+ return { ok: result.ok, errors: result.errors, warnings }
1175
+ }
1176
+
873
1177
  /**
874
1178
  * 计算归档目标目录名:保持原变更名不变,直接移入 archive/。
875
1179
  */
@@ -1064,7 +1368,9 @@ const contracts = {
1064
1368
  allowedTo: ['archive'],
1065
1369
  // validateAcceptanceMatrix:探针 7 矩阵槽 fail-closed(2026-09-14-acceptance-test-matrix FR-02),
1066
1370
  // 注册进 validator 链即覆盖 gates / machine-interface 等全部 runValidators 调用方(gates.js 零改动)。
1067
- validators: [validateVerifyOutputs, validateAcceptanceMatrix],
1371
+ // validatePassEligibility:PASS 封顶事实面(2026-09-17-pass-cap-semantics task-02 / D-001@v2 /
1372
+ // D-010)——注册即覆盖同上;回退 = 移除本注册行(纯加法,未触发四条件行为零变化)。
1373
+ validators: [validateVerifyOutputs, validateAcceptanceMatrix, validatePassEligibility],
1068
1374
  },
1069
1375
  archive: {
1070
1376
  stage: 'archive',
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * 条目为纯文本,不含序号/checkbox/缩进等结构性前缀(渲染层负责前缀形态):
10
10
  * - brainstorm:前 3 条 =「交叉审查模型」三层检查(prompt 内渲染为「N. 」序号行),
11
- * 后 6 条 =「交叉点抽取」(渲染为「- 」列表行);
11
+ * 后 8 条 =「交叉点抽取」(渲染为「- 」列表行);
12
12
  * - plan:渲染为「- [ ] 」checkbox 行(审查清单段);
13
13
  * - execute:渲染为 2 空格缩进的「N. 」序号行(QA 段「以下三项始终必查」)。
14
14
  */
@@ -23,6 +23,8 @@ export const REVIEW_CHECKLISTS = {
23
23
  'tasks.md 的执行范围与 design.md 的非目标',
24
24
  'decisions.md 的 D-xxx@vN 与 design.md 当前说法',
25
25
  'scan/module docs 或源码中的真实约束与 design.md 假设',
26
+ '涉及角色/权限/字典类数据时,实证不能只看静态定义:须以生产查询口径可解析到目标结果——用真实租户/库下可执行的查询(如按 enname+tenant 查询)确认返回非空,仅 enname 存在不算通过',
27
+ '涉及新页面/前端路由时,用户入口 × 菜单/注册 DML 对账:入口路由与菜单表、角色-权限注册 DML 逐一对账——能点到的入口必有对应菜单与授权注册行,菜单/注册 SQL 必须出现在交付清单(design.md 文件变更清单或 db/ 脚本)',
26
28
  ],
27
29
  plan: [
28
30
  'task 编号与 Wave checkbox 格式正确,execute 依赖此格式解析任务',
@@ -422,6 +422,111 @@ export function getLatestStageReviewRunId(runtimeRoot, stage, changeName) {
422
422
  }
423
423
  }
424
424
 
425
+ /**
426
+ * 同阶段上一轮审查结论采集(ql-20260916-021,Superpowers scoped re-review 采纳①)。
427
+ *
428
+ * 复审派发 prompt 原本只有跨阶段 pass 结论注入(prompt.js {PRIOR_REVIEW_FACTS} 的前序阶段段),
429
+ * 同 stage 上一轮 FAIL 的 findings 与已实证 pass 面不回灌——独立复审子代理全量重读并重复报告
430
+ * 已修问题。本函数机械提取「该 stage 本变更最近一轮有效 review」的两份清单供回灌:
431
+ * - openFindings:checklist result=fail/gap 的未决项(verdict=fail 但 checklist 无明细时以
432
+ * reviewerNotes 合成一条,防 fail 轮零线索);
433
+ * - passItems:checklist result=pass 的已实证面(复审勿重复报告/重验,除非修复改动触及该面)。
434
+ *
435
+ * 采集规则:
436
+ * - runId 字典序 = 时间序(generateStageReviewRunId 时间戳格式),取最新有效轮——含 marker
437
+ * 当前指向 run 的已写 review(同目录覆盖式复审路径下它就是最新轮,无需调用方区分);
438
+ * - 跨变更过滤:reviewedFiles[0] 不含 `changes/<changeName>/` 的轮不采(与
439
+ * getLatestStageReviewRunId 的 cross-change 防护同口径);
440
+ * - 骨架轮跳过(register-stage-review 生成的双 cannot_verify 待审骨架);
441
+ * - 坏 JSON 轮跳过不抛(best-effort);两者皆空(无未决也无 pass 面)→ null。
442
+ *
443
+ * @param {string} runtimeRoot - .runtime 绝对路径
444
+ * @param {string} stage - brainstorm|plan|execute
445
+ * @param {string} changeName - 变更名(跨变更过滤锚)
446
+ * @returns {{ priorRunId: string, verdicts: string, openFindings: string[], passItems: string[], notesPreview: string }|null}
447
+ */
448
+ export function collectSameStagePriorReview(runtimeRoot, stage, changeName) {
449
+ if (!runtimeRoot || !stage || !changeName) return null
450
+ const dir = join(runtimeRoot, 'stage-reviews')
451
+ if (!existsSync(dir)) return null
452
+ let entries
453
+ try {
454
+ entries = readdirSync(dir, { withFileTypes: true })
455
+ .filter(e => e.isDirectory() && e.name.startsWith(`${stage}-review-`))
456
+ .map(e => e.name)
457
+ } catch { return null }
458
+ if (entries.length === 0) return null
459
+
460
+ const candidates = []
461
+ for (const name of entries) {
462
+ const rp = join(dir, name, 'review.json')
463
+ if (!existsSync(rp)) continue
464
+ try {
465
+ const review = JSON.parse(readFileSync(rp, 'utf8'))
466
+ const r0 = Array.isArray(review.reviewedFiles) ? String(review.reviewedFiles[0]) : ''
467
+ if (!r0.includes(`changes/${changeName}/`)) continue
468
+ // 骨架占位跳过(register-stage-review 待审骨架:双 cannot_verify + 骨架 notes 标记)
469
+ const isSkeleton = review.specVerdict === 'cannot_verify' && review.qualityVerdict === 'cannot_verify'
470
+ && String(review.reviewerNotes || '').includes('骨架由 register-stage-review')
471
+ if (isSkeleton) continue
472
+ candidates.push({ runId: name.slice(stage.length + 1), review })
473
+ } catch { continue }
474
+ }
475
+ if (candidates.length === 0) return null
476
+
477
+ candidates.sort((a, b) => a.runId.localeCompare(b.runId))
478
+ const latest = candidates[candidates.length - 1]
479
+ const openFindings = []
480
+ const passItems = []
481
+ for (const c of (Array.isArray(latest.review.checklist) ? latest.review.checklist : [])) {
482
+ if (!c || typeof c !== 'object' || !c.item) continue
483
+ const text = String(c.item).replace(/\s+/g, ' ').slice(0, 120)
484
+ if (c.result === 'fail' || c.result === 'gap') openFindings.push(`[${c.result}] ${text}`)
485
+ else if (c.result === 'pass') passItems.push(text)
486
+ }
487
+ // verdict=fail 但 checklist 无明细:fail 结论不丢线索——notes 合成一条(复审者据此核验)
488
+ if (openFindings.length === 0 && (latest.review.specVerdict === 'fail' || latest.review.qualityVerdict === 'fail')) {
489
+ const notes = String(latest.review.reviewerNotes || '').replace(/\s+/g, ' ').slice(0, 160)
490
+ openFindings.push(`[verdict-fail] verdict=fail 但 checklist 无明细——以 reviewerNotes 为准:${notes || '(无 notes)'}`)
491
+ }
492
+ if (openFindings.length === 0 && passItems.length === 0) return null
493
+ return {
494
+ priorRunId: latest.runId,
495
+ verdicts: `spec=${latest.review.specVerdict}, quality=${latest.review.qualityVerdict}`,
496
+ openFindings,
497
+ passItems,
498
+ notesPreview: String(latest.review.reviewerNotes || '').replace(/\s+/g, ' ').slice(0, 160),
499
+ }
500
+ }
501
+
502
+ /**
503
+ * renderPriorRoundFindingsMd——把 collectSameStagePriorReview 产物渲染为复审派发 prompt 的
504
+ * 回灌块(prompt.js 注入 {PRIOR_REVIEW_FACTS},与前序阶段 pass 结论段同串拼接)。
505
+ *
506
+ * 语义双向:未决项**必须**逐项核验(已修给证据、未解决如实再次 fail——漏放行=假通过);
507
+ * pass 面**勿重复报告/重验**(除非修复改动明确触及该面)——一堵一疏,复审以增量为主。
508
+ *
509
+ * @param {{ priorRunId: string, verdicts: string, openFindings: string[], passItems: string[] }|null} collected
510
+ * @param {number} [cap=15] 每段展示封顶(防 prompt 灌爆,对齐前序实证段的 15 条惯例)
511
+ * @returns {string} markdown 块;collected 空 → 空串
512
+ */
513
+ export function renderPriorRoundFindingsMd(collected, cap = 15) {
514
+ if (!collected) return ''
515
+ const L = []
516
+ L.push(`**同阶段上一轮审查结论(run ${collected.priorRunId},verdict ${collected.verdicts})——本次为复审,以增量为主,不重演全量审查**:`)
517
+ if (collected.openFindings.length > 0) {
518
+ const shown = collected.openFindings.slice(0, cap)
519
+ L.push(`- **上一轮未决问题(逐项核验修复状态:已修给证据、未解决必须如实再次 fail——漏放行=假通过)**(${shown.length}/${collected.openFindings.length} 条):`)
520
+ for (const f of shown) L.push(` - ${f}`)
521
+ }
522
+ if (collected.passItems.length > 0) {
523
+ const shown = collected.passItems.slice(0, cap)
524
+ L.push(`- **上一轮已实证 pass 面(勿重复报告/重验——除非本次修复改动明确触及该面才定向复查)**(${shown.length}/${collected.passItems.length} 条):`)
525
+ for (const p of shown) L.push(` - ${p}`)
526
+ }
527
+ return `\n${L.join('\n')}\n`
528
+ }
529
+
425
530
  /**
426
531
  * 降级自审检测(2026-09-10 用户反馈①:PI agent 等宿主环境无 Agent tool,tier=independent
427
532
  * 硬要求子代理时只能降级自审——stage prompts 降级条款约定 reviewerNotes **首行**记
@@ -437,6 +542,19 @@ export function isDegradedSelfReview(review) {
437
542
  return /^降级[::]/.test(firstLine)
438
543
  }
439
544
 
545
+ /**
546
+ * 代落盘披露识别(C4,坑 delegated-write-audit-blind,2026-09-15 wp EHS 会话实证:平台写通道
547
+ * 故障下主流程 4 次代子代理落盘 review.json/交付物——独立性全靠 agent 自觉 + notes 留痕,gate
548
+ * 无任何可见标记。审查派发 prompt 已要求代落盘时 reviewerNotes 首行记「代落盘:…」,本识别让
549
+ * gate 侧补 ⚠️ 审计行(与 isDegradedSelfReview 同哲学:可见的折损优于静默的伪装——首行约定
550
+ * 识别,不承诺全文语义判定)。
551
+ */
552
+ export function hasDelegatedWriteDisclosure(review) {
553
+ const notes = review && typeof review.reviewerNotes === 'string' ? review.reviewerNotes : ''
554
+ const firstLine = notes.split(/\r?\n/, 1)[0].trim()
555
+ return /^代落盘[::]/.test(firstLine)
556
+ }
557
+
440
558
  /**
441
559
  * stage-level review 总校验(brainstorm/plan/execute-acceptance 的 done gate)
442
560
  *
@@ -572,10 +690,20 @@ export function validateStageReviewWithAutoRefresh(opts) {
572
690
  if (!docHash) return { result, autoRefreshed: false }
573
691
 
574
692
  const refreshedAt = new Date().toISOString().slice(0, 19).replace('T', ' ')
693
+ // C3 声明化(坑 dochash-autorefresh-silent-drift,2026-09-15 wp EHS 会话实证:审查通过后
694
+ // 主文档多次改版——NEW: 前缀批注、需求调整改名、Reverse Sync——gate 每次都静默机械重算放行,
695
+ // verdict 原样保留,「审查后的变更不再见审查」零痕迹可读。旧内容不可复原(只有 hash),改用
696
+ // 可实现且更贴风险的两个信号做声明:① hash 漂移(旧→新短前缀)② 刷新序数——同一 review 被
697
+ // 反复刷新 = 主文档在审查后反复变更,序数 ≥2 时 gate 侧升级提醒人工确认结论续用。
698
+ const priorNotes = String(result.review.reviewerNotes || '')
699
+ const refreshOrdinal = (priorNotes.match(/docHash auto-refreshed at /g) || []).length + 1
700
+ const oldHashShort = String(result.review.docHash || '').slice(0, 8)
701
+ const newHashShort = String(docHash).slice(0, 8)
702
+ const auditLine = `docHash auto-refreshed at ${refreshedAt}(第 ${refreshOrdinal} 次;hash ${oldHashShort}→${newHashShort};gate 检测主文档改版,机械重算;verdict 保留,结论是否仍适用于新文档需人工确认${refreshOrdinal >= 2 ? '——已多次刷新:主文档在审查后反复变更,建议人工复核结论续用或重审' : ''})`
575
703
  const refreshed = {
576
704
  ...result.review,
577
705
  docHash,
578
- reviewerNotes: `${result.review.reviewerNotes || ''}\ndocHash auto-refreshed at ${refreshedAt}(gate 检测主文档改版,机械重算;verdict 保留,结论是否仍适用需人工确认)`.trim(),
706
+ reviewerNotes: `${priorNotes}\n${auditLine}`.trim(),
579
707
  }
580
708
  try {
581
709
  writeAtomicSync(reviewPath, JSON.stringify(refreshed, null, 2) + '\n')
@@ -583,7 +711,7 @@ export function validateStageReviewWithAutoRefresh(opts) {
583
711
  return { result, autoRefreshed: false } // 写失败回落原失败结果,不静默放行
584
712
  }
585
713
  const recheck = validateStageReview(opts)
586
- return { result: recheck, autoRefreshed: recheck.ok, refreshedPath: reviewPath, previousErrors: result.errors }
714
+ return { result: recheck, autoRefreshed: recheck.ok, refreshedPath: reviewPath, previousErrors: result.errors, refreshOrdinal, hashDrift: `${oldHashShort}→${newHashShort}` }
587
715
  }
588
716
 
589
717
  /**
@@ -708,6 +836,13 @@ export function registerStageReview({ changeName, stage, fromFile, cwd, platform
708
836
  } catch (e) {
709
837
  throw new Error(`register-stage-review: 既有 review.json 解析失败 ${existingPath}: ${e.message}`)
710
838
  }
839
+ // 幂等跳过(2026-09-17 用户反馈⑥-②,ql-20260917-005):docHash 已一致时 refresh 仍整文件
840
+ // 重写 + 追加审计行——会话内多次 refresh 每次都 bump mtime/改内容,agent 的 Read→Edit 窗口
841
+ // 被「file modified since read」打断(本会话两次实证)。hash 未变 = 无可刷新内容,直接
842
+ // no-op 返回(不写盘不 bump mtime);行为差异只在「少一次无意义重写」。
843
+ if (existing.docHash === docHash && JSON.stringify(existing.reviewedFiles) === JSON.stringify(reviewedFiles)) {
844
+ return { ok: true, reviewRunId: existingRunId, reviewPath: existingPath, markerPath: stageReviewMarkerPath(runtimeRoot, stage, changeName), mode: 'noop-unchanged', mainDoc, review: existing }
845
+ }
711
846
  const refreshedAt = new Date().toISOString().slice(0, 19).replace('T', ' ')
712
847
  const review = {
713
848
  ...existing,
@@ -75,13 +75,17 @@ export const definition = {
75
75
 
76
76
  {SCOPE_AUDIT_TABLE}
77
77
 
78
- 3. 确保任务清单(tasks.md)所有 task checkbox 都已勾选;对照下方完成度快照做归档前最后注视(未完成/存疑 → 停下勿确认):
78
+ 3. 展示移交项清单(verify 阶段 facts.handover——CLI 机械注入如下,勿手改勿手算;blocking 置顶 + severity 标注,超封顶截断;确认前逐项核对 blocking 移交项已有处置安排(db-script 类须先在 verify-result.md 补执行声明——归档门会拦未声明/互斥态,env-blocked 确认复跑条件已登记,manual-acceptance 确认验收安排),advisory 项记录备查;无移交项时本区为空,注入异常时置换单行降级指引,全量见 changes/<变更名>/verify-facts.json):
79
+
80
+ {HANDOVER_SUMMARY}
81
+
82
+ 4. 确保任务清单(tasks.md)所有 task checkbox 都已勾选;对照下方完成度快照做归档前最后注视(未完成/存疑 → 停下勿确认):
79
83
  {TASK_COMPLETION_REPORT}
80
- 4. 若上一步机械决策提炼输出「⚠️ rejected 决策缺否决理由/复潮条件(needsWait)」:先按其指引处理——补录 decisions.md 缺失字段后重跑提炼命令(幂等),或经用户裁决跳过该条;处理结果写进本步 --output
81
- 5. **git 暂存已由 CLI 自动完成**(归档完成时自动 add:\`.sillyspec/changes/archive/\`、本次同步的模块文档、\`.sillyspec/knowledge/decisions/\`——未提交,由用户通过统一提交工具处理)——你不需要也不应该手动 git add
82
- 6. 让用户确认后,用 \`--confirm\` 完成本步骤:
84
+ 5. 若上一步机械决策提炼输出「⚠️ rejected 决策缺否决理由/复潮条件(needsWait)」:先按其指引处理——补录 decisions.md 缺失字段后重跑提炼命令(幂等),或经用户裁决跳过该条;处理结果写进本步 --output
85
+ 6. **git 暂存已由 CLI 自动完成**(归档完成时自动 add:\`.sillyspec/changes/archive/\`、本次同步的模块文档、\`.sillyspec/knowledge/decisions/\`——未提交,由用户通过统一提交工具处理)——你不需要也不应该手动 git add
86
+ 7. 让用户确认后,用 \`--confirm\` 完成本步骤:
83
87
  \`sillyspec run archive --done --confirm --output "确认归档"\`
84
- 7. CLI 会创建 \`.sillyspec/changes/archive/\`,并将变更目录移动到 \`.sillyspec/changes/archive/<原变更名>/\`
88
+ 8. CLI 会创建 \`.sillyspec/changes/archive/\`,并将变更目录移动到 \`.sillyspec/changes/archive/<原变更名>/\`
85
89
 
86
90
  ### 输出
87
91
  归档完成 + archive 目录路径`,
@@ -286,10 +286,11 @@ design.md 第一行标题必须用中文:# 设计文档(Design)— <变更
286
286
 
287
287
  | 操作 | 文件路径 | 说明 |
288
288
  |---|---|---|
289
- | 新增 | src/xxx/NewFile.java | ... |
289
+ | 新增 | NEW:src/xxx/NewFile.java | ... |
290
290
  | 修改 | src/xxx/ExistingFile.java | 新增 xx 方法 |
291
291
  | 删除 | src/xxx/OldFile.java | 已被 xx 替代 |
292
292
 
293
+ **路径存在性核验(NEW: 前缀铁律,2026-09-17 用户反馈⑥-①前移)**:清单条目在「生成规范文件」步 \`--done\` 门禁会逐一核验仓内存在性——**计划新建的文件必须在路径前加 \`NEW:\` 前缀**(冒号后不加空格,\`NEW:src/foo.js\` 合法、\`NEW: src/foo.js\` 属书写错误);未加前缀的新建路径会以 \`design_file_ref_invalid\`(幻觉路径/书写错误)在末步被拦,返工一轮。已有文件写仓根相对正斜杠路径,禁绝对路径/glob。
293
294
  **字段数据流标注**(避免新增字段到 execute 才发现没透传):当清单含「新增/修改对外字段、接口、DTO、响应体、事件 payload、配置键」时,对应行「说明」列必须交代 producer→consumer 数据流——产出方(producer)→ 每跳流转/归一化点(序列化/反序列化/字段映射/默认值兜底)→ 消费方(consumer)。漏标 = 字段在某跳 dormant(声明了却没透传),到 execute/verify 才暴露,属设计层缺口。
294
295
  - 示例:「修改 daemon.ts:新增 budget_tokens,producer=backend/api-types.ts → ws_hub envelope 透传 → daemon.ts normalizeUsage(snake→camel)→ consumer=runtime/lease.ts 写 lease」
295
296
  - 仅改内部实现、无对外字段变动时,说明列照常写「新增 xx 方法」即可,无需数据流。
@@ -385,8 +386,12 @@ tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
385
386
  - tier=self:当前 agent 直接执行下方交叉审查(小变更)
386
387
  - tier=independent:必须用 Agent tool 启动一个独立的设计审查子代理(独立上下文,不共享你的分析与倾向),子代理按下方"交叉审查模型"审查 design.md 并输出 review.json。review.json 产物契约(CLI Stage Review Gate 将硬校验,schema + 完整示例 + docHash 算法如下,照抄改值):
387
388
  宿主环境无 Agent tool 可用(调用报 Unknown agent / Available agents: none)→ 不卡死:主代理切换为审查者角色自审替代,reviewerNotes 首行记录「降级:环境无子代理可用」,逐条结论附源码锚点(file:line 或 grep/read 证据)补偿独立性。
389
+ {PRIOR_REVIEW_FACTS}
388
390
  {REVIEW_JSON_CONTRACT}
389
391
  子代理只产出 review + Unresolved Blockers,**是否调用 sillyspec run brainstorm --wait 仍由你(主 agent)根据其 verdict 决定**(子代理不直接操作 CLI 状态机)。
392
+ **子代理执行纪律(坑 review-subagent-stall,2026-09-15 wp EHS 会话实证:独立审查 94 分钟无收敛被用户三催、写通道故障空转 35 分钟)**:
393
+ - 时间盒收敛:审查子代理的调研是收敛动作不是发散——必读材料读完即逐条出结论并落盘 review.json;仅个别结论存疑时定向补证,禁止循环扩大核验面(连续读文件 10+ 次仍零结论 = 立即停止扩展、基于已读材料收敛)。
394
+ - 写通道降级:子代理写操作持续被平台拒绝(session not in running turn 类故障)→ 重试 ≤3 次即停,把完整审查结论(含 review.json 全文)作为最终文本回传主代理,由主代理代为落盘并在 reviewerNotes 首行留痕「代落盘:子代理写通道故障」;禁止长时间空转重试。
390
395
 
391
396
  ### 默认行为
392
397
  1. 默认必须执行一次交叉审查;不要让用户凭主观判断决定"要不要 Grill"。
@@ -413,6 +418,7 @@ ${REVIEW_CHECKLISTS.brainstorm.slice(0, 3).map((item, index) => (index + 1) + '.
413
418
  ### 交叉点抽取
414
419
  重点找这些交叉点:
415
420
  ${REVIEW_CHECKLISTS.brainstorm.slice(3).map((item) => '- ' + item).join('\n')}
421
+ 后两条为命中条件注入项:变更涉及角色/权限/字典类数据或新页面/前端路由时必查。
416
422
 
417
423
  ### 问答处理
418
424
  1. 先自动交叉审查,不要一上来问用户。