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
package/src/run/gates.js CHANGED
@@ -322,12 +322,14 @@ export async function enforceDepsGate(stageName, cwd, changeName, step, steps, c
322
322
  // 下游已有物理目录存在性判定兜底(G2/R3),这里 warn 留下根因线索,避免静默误诊。
323
323
  console.warn(`⚠️ worktree meta 读取失败(deps gate 将走物理目录判定): ${e.message}`)
324
324
  }
325
- const depsStatus = meta?.depsStatus
326
- const mainOk = ['linked', 'installed', 'n/a'].includes(depsStatus)
327
- if (mainOk) {
328
- // ── 跨仓 worktree deps 校验(坑 cross-repo-no-worktree-isolation)──
329
- // 跨仓仓供给失败同样无构建/测试能力(前端仓无 node_modules 建不了 build)。只校验已建
330
- // worktree 的跨仓仓(legacy 直写模式无 meta,维持原语义,下游 Task Review 兜底)。
325
+ const depsOk = (s) => ['linked', 'installed', 'n/a'].includes(s)
326
+ const changeDir = changeName ? join(specBase, 'changes', changeName) : null
327
+
328
+ // main 侧就绪后的跨仓 worktree deps 校验(坑 cross-repo-no-worktree-isolation):
329
+ // 跨仓仓供给失败同样无构建/测试能力(前端仓无 node_modules 建不了 build)。只校验已建
330
+ // worktree 的跨仓仓(legacy 直写模式无 meta,维持原语义,下游 Task Review 兜底)。
331
+ // 放行返回 true;跨仓未就绪置 blocked + exit(1)。
332
+ const crossCheckOrExit = async () => {
331
333
  let crossFailed = []
332
334
  try {
333
335
  const crossSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
@@ -351,8 +353,39 @@ export async function enforceDepsGate(stageName, cwd, changeName, step, steps, c
351
353
  if (persist) { try { await persist() } catch { /* 落盘失败不吞阻断语义 */ } }
352
354
  process.exit(1)
353
355
  }
354
- const changeDir = changeName ? join(specBase, 'changes', changeName) : null
356
+
357
+ if (depsOk(meta?.depsStatus)) return await crossCheckOrExit()
358
+
359
+ // wave 级 opt-out(先于重供给:无需 deps 能力的 wave 不花 install 成本)
355
360
  if (isCurrentWaveAllNoDepsVerify(step?.name, changeDir)) return true
361
+
362
+ // ── --done 重供给重试(坑 deps-gate-manual-retry-ineffective,2026-09-15 wp 会话实证)──
363
+ // 阻断提示承诺「在 worktree 内手动安装依赖后重试」,但 manual install 不改 meta.depsStatus,
364
+ // failed 态直读 meta 永不翻绿 → 重试 --done 必撞同一堵墙,唯一出路 doctor --fix(wp 会话
365
+ // 正是手动编译成功后重试仍被拦,被迫多跑一轮 doctor)。对齐 run execute 入口
366
+ // ensureDepsFreshness 的同款供给:阻断前重供给一次并写回 meta——PATH 已修好的 shell 里
367
+ // 重跑 --done 即等效 doctor --fix;marker 型生态(nodejs)手动安装补齐 node_modules 后
368
+ // 重供给的 install 也能自然通过。仍失败则维持阻断,不无限重试。
369
+ let depsStatus = meta?.depsStatus
370
+ if (wm && changeName) {
371
+ let wtPath = null
372
+ try { wtPath = wm.getWorktreePath(changeName) } catch { /* meta 缺失走物理目录判定 */ }
373
+ if (wtPath && existsSync(wtPath)) {
374
+ try {
375
+ const { provisionDeps } = await import('../worktree-deps.js')
376
+ console.log(`🔄 deps 门控:depsStatus=${depsStatus || 'unknown'},阻断前按 --done 重试路径重供给一次(与 doctor --fix 同款)...`)
377
+ const deps = provisionDeps(wtPath, cwd, { specBase }) || {}
378
+ meta = { ...meta, ...deps }
379
+ writeAtomicSync(join(wtPath, 'meta.json'), JSON.stringify(meta, null, 2) + '\n')
380
+ depsStatus = meta.depsStatus
381
+ console.log(` 重供给完成:depsStatus=${depsStatus || 'unknown'}${meta.depsError ? `(${meta.depsError})` : ''}`)
382
+ if (depsOk(depsStatus)) return await crossCheckOrExit()
383
+ } catch (e) {
384
+ console.warn(`⚠️ deps 门控重供给失败(维持阻断判定):${e.message}`)
385
+ }
386
+ }
387
+ }
388
+
356
389
  if (steps && steps[currentIdx]) {
357
390
  steps[currentIdx].status = 'blocked'
358
391
  // 阻断原因持久化(坑 deps-gate-blocked-invisible):--status/诊断时可见,不依赖阻断当刻的
@@ -845,6 +878,11 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
845
878
  })
846
879
  const reconcileEnvelope = buildReconcileTargetFilesEnvelope(reconcileCheck)
847
880
  const reconcileBlocked = printReconcileTargetFilesCheck(reconcileCheck, reconcileEnvelope)
881
+ // 跨仓 per-repo 对账明细渲染(坑 cross-repo-reconcile-blindness,2026-09-15 复盘兑现 D-004
882
+ // 分期):reconcileTargetFiles 的 crossRepo 字段(advisory 不阻断)——各仓 对上/②缺/③多
883
+ // 逐仓一行 + ②缺明细(跨仓声明落空是该去对应仓干活的信号,但不翻主仓状态——锚点窗口
884
+ // 脆弱期会假信号)。无跨仓卡时 crossRepo 为空数组零输出。
885
+ printCrossRepoReconcile(reconcileCheck.crossRepo)
848
886
  // 结果落盘(design Wave 2 承诺:对齐 .runtime/verify-runs/<ts>/ 先例)——五状态全落(含
849
887
  // 放行态),fail-soft 不影响 gate 判定;②类阻断回执同样留档供平台/审计消费
850
888
  writeReconcileRunResult({ runtimeRoot: reconcileRuntimeRoot, changeName, envelope: reconcileEnvelope, result: reconcileCheck })
@@ -872,6 +910,23 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
872
910
  })
873
911
  const probeEnvelope = buildProbeConsistencyEnvelope(probeCheck)
874
912
  const probeBlocked = printProbeConsistencyCheck(probeCheck, probeEnvelope)
913
+ // 探针7 covered 证据锚点 advisory(坑 probe7-covered-anchor-missing,2026-09-15 复盘:7 行
914
+ // covered 证据缺 file:line 被审查打回——预填说明已载明口径,机器在 --done 时点再拦一道,
915
+ // 省一轮人工往返)。advisory 不阻断;独立模块零环(verify-probes 是多会话冲突面,不碰)。
916
+ try {
917
+ const { checkProbe7AnchorCoverage } = await import('../probe7-anchor-check.js')
918
+ const p7ReportPath = join(specBase, 'changes', changeName, 'verify-result.md')
919
+ if (existsSync(p7ReportPath)) {
920
+ const p7Anchor = checkProbe7AnchorCoverage(readFileSync(p7ReportPath, 'utf8'))
921
+ if (p7Anchor.applicable && p7Anchor.missingAnchors.length > 0) {
922
+ console.warn(`\n⚠️ 探针7 锚点校验:${p7Anchor.missingAnchors.length}/${p7Anchor.coveredRows} 行判定为 covered 但证据列缺 file:line / \`.test.\` 文件名 / 反引号路径锚点(advisory 不阻断,审查会要求回补):`)
923
+ for (const m of p7Anchor.missingAnchors.slice(0, 10)) {
924
+ console.warn(` - ${m.task}:${m.acceptance || '(空 acceptance)'}——证据当前为「${m.evidence || '(空)'}」,应给 \`测试文件路径:行号\` 首命中锚点、\`.test.\` 测试文件名或反引号包裹的路径/测试名(行号可省)`)
925
+ }
926
+ if (p7Anchor.missingAnchors.length > 10) console.warn(` …还有 ${p7Anchor.missingAnchors.length - 10} 行`)
927
+ }
928
+ }
929
+ } catch { /* 锚点校验异常 fail-soft 不影响 gate */ }
875
930
  // 结果落盘(对齐 writeReconcileRunResult 先例):独立文件 probe-consistency-result.json——
876
931
  // 与 reconcile-result.json 同 gate 运行同目录组织(verify-runs/<ts>/),但不并入后者:
877
932
  // reconcile-result.json 的 schema 已被 P3a 消费方锁定,塞探针字段(读改写/扩参)只会引入
@@ -996,8 +1051,14 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
996
1051
  // 不在自动刷新范围,照常 fail-closed。
997
1052
  const gated = validateStageReviewWithAutoRefresh({ stage: stageName, reviewType, runtimeRoot, reviewRunId, searchDirs, autoRefresh: true })
998
1053
  if (gated.autoRefreshed) {
999
- console.warn(`\n🔄 stage review docHash 失配已自动刷新(主文档改版后机械重算,verdict/checklist 保留——结论是否仍适用于新文档需人工确认)`)
1054
+ console.warn(`\n🔄 stage review docHash 失配已自动刷新(第 ${gated.refreshOrdinal || 1} 次;hash ${gated.hashDrift || 'n/a'};主文档改版后机械重算,verdict/checklist 保留——结论是否仍适用于新文档需人工确认)`)
1000
1055
  console.warn(` 已就地更新: ${gated.refreshedPath}`)
1056
+ // C3 升级提醒(2026-09-15 wp EHS 实证:NEW: 批注/需求调整/Reverse Sync 多次走此通道):
1057
+ // 同一 review 被反复刷新 = 主文档在审查通过后反复变更,「审查后的变更不再见审查」的
1058
+ // 面积在累积——序数 ≥2 从一句注记升级为显式人工确认提醒(仍放行,不伪造重审)。
1059
+ if ((gated.refreshOrdinal || 1) >= 2) {
1060
+ console.warn(` ⚠️ 该 review 已第 ${gated.refreshOrdinal} 次刷新——主文档在审查后反复变更,建议人工复核 verdict 结论续用性(涉及设计要点/接口变更时应重审,勿把自动放行当「新文档已审过」)`)
1061
+ }
1001
1062
  }
1002
1063
  const reviewResult = gated.result
1003
1064
  printStageReviewResult(reviewResult, { stage: stageName, reviewRunId, runtimeRoot, changeName })
@@ -1010,7 +1071,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1010
1071
  // channel 识别(2026-09-10 通道优先序批次):reviewer.channel 结构化落款优先,兼容
1011
1072
  // reviewerNotes 首行「降级:」约定;platform 通道(P2 review-dispatch 落地后)ℹ️ 留痕
1012
1073
  // missionId 供追溯,其余通道正常放行。
1013
- const { classifyReviewerChannel } = await import('../stage-review.js')
1074
+ const { classifyReviewerChannel, hasDelegatedWriteDisclosure } = await import('../stage-review.js')
1014
1075
  const reviewChannel = classifyReviewerChannel(reviewResult.review)
1015
1076
  if (reviewChannel === 'self') {
1016
1077
  console.warn(`\n⚠️ Stage Review 降级自审(tier=independent 但宿主无 Agent tool):${stageName} review 按 prompt 降级条款由当前 agent 自审产出——独立性折损已留痕(reviewerNotes 首行「降级:」),结论应附源码锚点补偿,建议人工抽查关键结论。`)
@@ -1018,6 +1079,12 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1018
1079
  const mid = reviewResult.review && reviewResult.review.reviewer && reviewResult.review.reviewer.missionId
1019
1080
  console.log(`\nℹ️ Stage Review 平台派发(channel=platform${mid ? `,missionId=${mid}` : ',missionId 未落款'}):${stageName} review 由平台独立 worker 产出(独立进程/会话)。`)
1020
1081
  }
1082
+ // C4 代落盘审计行(2026-09-15 wp EHS 实证:平台写通道故障下主流程代子代理落盘 review,
1083
+ // 独立性折损当时只在 notes 里、gate 不可见):reviewerNotes 首行「代落盘:」披露 → ⚠️ 留痕
1084
+ // (与降级自审同哲学:可见的折损优于静默伪装;首行约定识别,不阻断)。
1085
+ if (hasDelegatedWriteDisclosure(reviewResult.review)) {
1086
+ console.warn(`\n⚠️ Stage Review 代落盘披露:${stageName} review 的 reviewerNotes 首行标记「代落盘」——review.json 由主代理代(写通道故障的)审查者落盘,独立性依赖审查者结论回传的忠实性,建议人工抽查关键结论与回传文本一致性。`)
1087
+ }
1021
1088
  }
1022
1089
  } catch (e) {
1023
1090
  // fail-closed:Gate 自身异常阻断完成,不静默放行(与 Task Review Gate 一致)
@@ -1554,6 +1621,35 @@ export function writeReconcileRunResult({ runtimeRoot, changeName, envelope, res
1554
1621
  * @param {object} [envelope] 预构造信封(缺省现算;显式传入便于调用方先断言再打印)
1555
1622
  * @returns {boolean} true = 阻断(status='missing_declared')
1556
1623
  */
1624
+ /**
1625
+ * 打印跨仓 per-repo 对账结果(cross-repo-reconcile.js 产出的 advisory 明细,verify 块接线)。
1626
+ * 恒不阻断(返回 void):跨仓 diff 锚点=各仓最近提交窗口,锚点脆弱期(已 commit 多笔/全在
1627
+ * working-tree)会产②类假信号——明细供人工裁决,不翻主仓对账状态。
1628
+ * @param {Array<{repo:string,repoPath:string|null,declaredCount:number,actualCount:number,
1629
+ * matched:string[],missing:Array<{task:string,path:string,isNew?:boolean}>,
1630
+ * undeclared:string[],scaffoldCount:number,degradedReason:string|null}>|undefined} crossRepo
1631
+ */
1632
+ function printCrossRepoReconcile(crossRepo) {
1633
+ if (!Array.isArray(crossRepo) || crossRepo.length === 0) return
1634
+ console.log(`\n🔀 跨仓 per-repo 对账(advisory 不阻断,锚点=各仓最近提交窗口):`)
1635
+ for (const r of crossRepo) {
1636
+ if (r.degradedReason) {
1637
+ console.warn(` ⚠️ ${r.repo}:${r.degradedReason}`)
1638
+ continue
1639
+ }
1640
+ const flag = (r.missing.length > 0 || r.undeclared.length > 0) ? '⚠️ ' : '✅ '
1641
+ console.log(` ${flag}${r.repo}(${r.repoPath}):声明 ${r.declaredCount} / 实测 ${r.actualCount} / 对上 ${r.matched.length} / ②缺 ${r.missing.length} / ③多 ${r.undeclared.length}${r.scaffoldCount > 0 ? ` / 脚手架聚合 ${r.scaffoldCount}` : ''}`)
1642
+ for (const m of r.missing.slice(0, 10)) {
1643
+ console.warn(` ②缺 ${m.task}: ${m.path}${m.isNew ? '(NEW: 声明新建,该仓未见)' : ''}——锚点窗口外已提交或多笔时可能假信号,到该仓 git log 核实`)
1644
+ }
1645
+ if (r.missing.length > 10) console.warn(` …还有 ${r.missing.length - 10} 条②缺`)
1646
+ for (const u of r.undeclared.slice(0, 10)) {
1647
+ console.warn(` ③多 ${u}(该仓最近窗口实际改动,未在任何跨仓卡声明)`)
1648
+ }
1649
+ if (r.undeclared.length > 10) console.warn(` …还有 ${r.undeclared.length - 10} 条③多`)
1650
+ }
1651
+ }
1652
+
1557
1653
  export function printReconcileTargetFilesCheck(r, envelope = buildReconcileTargetFilesEnvelope(r)) {
1558
1654
  // ②类(声明未做=计划落空):ERROR 阻断
1559
1655
  if (r.status === 'missing_declared') {
@@ -1569,11 +1665,33 @@ export function printReconcileTargetFilesCheck(r, envelope = buildReconcileTarge
1569
1665
  // ③类(做了没声明=scope creep):WARNING 放行,suspectTask 尽力归因随行列出
1570
1666
  if (r.status === 'undeclared') {
1571
1667
  console.warn(`\n⚠️ target_files 对账发现 ${r.undeclared.length} 个实际改动未在任何 task 卡声明(③类 scope creep,advisory 不阻断):`)
1572
- for (const u of r.undeclared.slice(0, 20)) {
1668
+ if (r.undeclared.length <= 20) {
1669
+ for (const u of r.undeclared) {
1573
1670
  console.warn(` - ${u.path}${u.suspectTask ? `(疑似 ${u.suspectTask} 的改动——该 task 的 review.json changedFiles 提及)` : ''}`)
1574
1671
  }
1575
- if (r.undeclared.length > 20) console.warn(` …还有 ${r.undeclared.length - 20} 个`)
1576
- console.warn(` evidence: matched=${envelope.evidence.matched_count} missing=${envelope.evidence.missing_declared_count} undeclared=${envelope.evidence.undeclared_count}`)
1672
+ } else {
1673
+ // >20 条逐行刷屏改按顶层目录聚合(坑 reconcile-undeclared-noise,2026-09-15 wp EHS 会话
1674
+ // 实证:58 条③类多为 worktree baseline 拷入的环境文件——agent 需要的是形状不是清单):
1675
+ // 目录计数降序 + 每目录至多 2 个样例;完整清单随后由 writeReconcileRunResult 落盘
1676
+ // reconcile-result.json(gate 输出 📄 行含具体路径),suspectTask 归因以落盘为准。
1677
+ const byDir = new Map()
1678
+ for (const u of r.undeclared) {
1679
+ const p = String(u.path || '')
1680
+ const slash = p.indexOf('/')
1681
+ const dir = slash > 0 ? p.slice(0, slash + 1) : '(仓根)'
1682
+ const rec = byDir.get(dir) || { count: 0, samples: [] }
1683
+ rec.count++
1684
+ if (rec.samples.length < 2) rec.samples.push(p)
1685
+ byDir.set(dir, rec)
1686
+ }
1687
+ const dirs = [...byDir.entries()].sort((a, b) => b[1].count - a[1].count)
1688
+ for (const [dir, rec] of dirs.slice(0, 8)) {
1689
+ console.warn(` - ${dir} ×${rec.count}(样例:${rec.samples.join('、')})`)
1690
+ }
1691
+ if (dirs.length > 8) console.warn(` …另有 ${dirs.length - 8} 个目录`)
1692
+ console.warn(` 共 ${r.undeclared.length} 条,>20 触发按目录聚合;完整逐条清单(含疑似 task 归因)见对账落盘 reconcile-result.json(下方 📄 行)。`)
1693
+ }
1694
+ console.warn(` evidence: matched=${envelope.evidence.matched_count} missing=${envelope.evidence.missing_declared_count} undeclared=${envelope.evidence.undeclared_count}`)
1577
1695
  for (const fix of envelope.supportedFixes) console.warn(` 提示:${fix}`)
1578
1696
  console.warn(` 对账口径 actual=${(r.sources || []).join(' + ') || 'n/a'}(③类误报先核对 sources 与并行会话剔除提示)。`)
1579
1697
  return false
package/src/run/prompt.js CHANGED
@@ -316,6 +316,68 @@ export function buildKnowledgeInjection({ knowledgeDir, runtimeDir, change, quer
316
316
 
317
317
  // parseModuleMapSimple 复用 modules.js 的 canonical 实现(合并历史 copy-paste 副本,2026-08-07;
318
318
  // 无循环依赖:modules.js 仅 import fs/path/db.js,prompt.js → modules.js 单向)。
319
+
320
+ /**
321
+ * 等效验证先例库(坑 verify-precedent-only-in-prose,2026-09-15 EHS 生产实证:mvn test 被
322
+ * 公司框架 parent pom pluginManagement 硬编码 surefire skip=true、-D 覆盖无效——等效口径
323
+ * dependency:build-classpath+javac+JUnitCore 第二次复用靠 agent 翻旧 verify-result.md 正文
324
+ * 续命;先例只活在散文里 = 换个 agent/换个变更就断档)。结构化登记进 local.yaml
325
+ * verify_precedents 段,verify prompt 时点注入提示(与 test_strategy 预检提示同挂点)。
326
+ *
327
+ * 对象数组形态(行扫描,同 extractKnownFailures 免 YAML 依赖口径——注释/空行/块界容错):
328
+ * verify_precedents:
329
+ * - id: mvn-test-surefire-skip
330
+ * standard_command: mvn test
331
+ * reason: <标准命令为何不可用>
332
+ * equivalent: <已验证的等效口径>
333
+ * established_by: <确立该口径的变更名>
334
+ * notes: <可选补充>
335
+ * 残项(standard_command 与 equivalent 双缺)过滤——登记不完整的先例没有可执行口径。
336
+ * @param {string|null} yamlText local.yaml 全文
337
+ * @returns {Array<object>} 先例对象数组
338
+ */
339
+ export function parseVerifyPrecedents(yamlText) {
340
+ const lines = String(yamlText || '').split(/\r?\n/)
341
+ const headIdx = lines.findIndex(l => /^verify_precedents:\s*(?:#.*)?$/.test(l))
342
+ if (headIdx < 0) return []
343
+ const items = []
344
+ let cur = null
345
+ const clean = (v) => String(v || '').replace(/\s+#.*$/, '').trim()
346
+ for (let i = headIdx + 1; i < lines.length; i++) {
347
+ const l = lines[i]
348
+ if (/^\S/.test(l)) break // 下一个顶层键 → 块结束
349
+ if (!l.trim() || /^\s*#/.test(l)) continue
350
+ const item = l.match(/^\s*-\s+([A-Za-z_][\w-]*):\s*(.*)$/)
351
+ if (item) {
352
+ cur = {}
353
+ items.push(cur)
354
+ cur[item[1]] = clean(item[2])
355
+ continue
356
+ }
357
+ const kv = l.match(/^\s*([A-Za-z_][\w-]*):\s*(.*)$/)
358
+ if (kv && cur) cur[kv[1]] = clean(kv[2])
359
+ }
360
+ return items.filter(x => x && (x.standard_command || x.equivalent))
361
+ }
362
+
363
+ /**
364
+ * 先例提示渲染(verify prompt 注入用;空输入 → '',调用方零输出)。
365
+ * 行动指引刻意指向「把等效命令配进 commands.test 照常全量实测对账」而非 test_strategy: skip
366
+ * 放行——EHS 实证里被环境阻断 deferred 的集成测试正是 P1 缺陷藏身处,能实测就实测。
367
+ * @param {Array<object>|null} precedents
368
+ * @returns {string}
369
+ */
370
+ export function renderVerifyPrecedentHint(precedents) {
371
+ if (!Array.isArray(precedents) || precedents.length === 0) return ''
372
+ const L = [`【验证先例提示】本仓已登记 ${precedents.length} 条等效验证先例(标准命令不可用时的既有替代口径):`]
373
+ for (const p of precedents.slice(0, 5)) {
374
+ L.push(`- ${p.id || '(无 id)'}:\`${p.standard_command || '?'}\` 不可用(${p.reason || '原因未注'})→ 等效口径 \`${p.equivalent || '?'}\`(确立于 ${p.established_by || '?'}${p.notes ? `;${p.notes}` : ''})`)
375
+ }
376
+ if (precedents.length > 5) L.push(`- …另有 ${precedents.length - 5} 条未展开(读 local.yaml verify_precedents 段看全量)`)
377
+ L.push('适用时把等效命令配进 local.yaml commands.test(或 modules.<m>.test)后照常全量实测对账,而不是只靠 test_strategy: skip 放行;先例不适用(环境已变/口径失效)可忽略并在 verify-result 注记。')
378
+ return L.join('\n')
379
+ }
380
+
319
381
  /**
320
382
  * 提示词路径根的单一解析:specRoot(平台) > specDriftAnchor(worktree 漂移锚定主仓) > cwd/.sillyspec(本地)。
321
383
  * 治 execute review.json 提示路径写 worktree 副本、--done 校验读主仓的分裂(坑 execute-prompt-spec-drift):
@@ -980,7 +1042,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
980
1042
  if (['brainstorm', 'plan', 'execute'].includes(stageName) && promptText.includes('{REVIEW_TIER}')) {
981
1043
  try {
982
1044
  const { classifyReviewTier } = await import('../review-tier.js')
983
- const { generateStageReviewRunId, renderReviewJsonContract, stageReviewMarkerPath, readReviewChannelPriority } = await import('../stage-review.js')
1045
+ const { generateStageReviewRunId, renderReviewJsonContract, stageReviewMarkerPath, readReviewChannelPriority, getLatestStageReviewRunId, collectSameStagePriorReview, renderPriorRoundFindingsMd } = await import('../stage-review.js')
984
1046
  const tierSpecBase = resolvePromptSpecBase(platformOpts, cwd)
985
1047
  const tierChangeDir = changeName ? join(tierSpecBase, 'changes', changeName) : null
986
1048
  const designPath = tierChangeDir ? join(tierChangeDir, 'design.md') : null
@@ -1020,11 +1082,52 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1020
1082
  // 头部渲染「审查执行通道」段(按用户配置序,见 local.yaml review_dispatch.channel_priority)。
1021
1083
  const reviewChannelPriority = readReviewChannelPriority(cwd)
1022
1084
  const reviewContractMd = renderReviewJsonContract({ stage: stageName, changeDir: tierChangeDir, reviewRunId, tier: tier.tier, channelPriority: reviewChannelPriority })
1085
+ // C2 前阶段实证清单机械附带(坑 review-subagent-redundant-verify,2026-09-15 wp EHS 实证:
1086
+ // plan 审查把 brainstorm 已两轮实证的三仓源码全量重验——94 分钟不收敛被用户三催。此前只有
1087
+ // prompt 散文说「前序实证勿重验」,子代理并不知道前序具体审过了什么;现把前序 stage review
1088
+ // 的 checklist pass 项机械注入派发 prompt(封顶 15 条防灌爆),审查子代理据此跳过已实证面。
1089
+ let priorFactsMd = ''
1090
+ if (tier.tier === 'independent') {
1091
+ try {
1092
+ const priorStages = stageName === 'plan' ? ['brainstorm'] : stageName === 'execute' ? ['brainstorm', 'plan'] : []
1093
+ const factLines = []
1094
+ for (const ps of priorStages) {
1095
+ let priorRunId = null
1096
+ try { priorRunId = getLatestStageReviewRunId(tierRuntimeRoot, ps, changeName) } catch {}
1097
+ if (!priorRunId) continue
1098
+ const priorReviewPath = join(tierRuntimeRoot, 'stage-reviews', `${ps}-${priorRunId}`, 'review.json')
1099
+ if (!existsSync(priorReviewPath)) continue
1100
+ try {
1101
+ const priorReview = JSON.parse(readFileSync(priorReviewPath, 'utf8'))
1102
+ for (const c of (Array.isArray(priorReview.checklist) ? priorReview.checklist : [])) {
1103
+ if (c && c.result === 'pass' && c.item) factLines.push(`- [${ps}] ${String(c.item).replace(/\s+/g, ' ').slice(0, 100)}`)
1104
+ }
1105
+ } catch {}
1106
+ }
1107
+ if (factLines.length > 0) {
1108
+ const shown = factLines.slice(0, 15)
1109
+ priorFactsMd = `\n**前序阶段独立审查已实证(pass)结论——直接引用勿重验**(展示 ${shown.length}/${factLines.length} 条;EHS 复盘实证:重验已实证面 = 94 分钟不收敛的同款浪费;本阶段只审自己特有面,结论存疑时才定向补证):\n${shown.join('\n')}\n`
1110
+ }
1111
+ } catch {}
1112
+ }
1113
+ // 同阶段上一轮复审基线(ql-20260916-021,Superpowers scoped re-review 采纳①):上一轮
1114
+ // FAIL 的 findings 与已实证 pass 面机械回灌给复审子代理——复审以增量为主(核验修复 +
1115
+ // 本次改版新增面),防全量重读与重复报告已修问题(独立复审子代理无对话历史,不知上一轮
1116
+ // 审过什么/报过什么)。骨架轮/他变更轮由采集端过滤;tier=self 不注入(当前 agent 自带
1117
+ // 上一轮上下文)。
1118
+ try {
1119
+ const priorRound = collectSameStagePriorReview(tierRuntimeRoot, stageName, changeName)
1120
+ if (priorRound) {
1121
+ const blockMd = renderPriorRoundFindingsMd(priorRound)
1122
+ priorFactsMd = priorFactsMd ? priorFactsMd + blockMd : blockMd
1123
+ }
1124
+ } catch {}
1023
1125
  promptText = promptText
1024
1126
  .split('{REVIEW_TIER}').join(tier.tier)
1025
1127
  .split('{REVIEW_TIER_REASON}').join(tier.reason)
1026
1128
  .split('{STAGE_REVIEW_RUN_ID}').join(reviewRunId)
1027
1129
  .split('{REVIEW_JSON_CONTRACT}').join(reviewContractMd)
1130
+ .split('{PRIOR_REVIEW_FACTS}').join(priorFactsMd)
1028
1131
  } catch (e) {
1029
1132
  // 降级 self,避免 prompt 残留占位符
1030
1133
  promptText = promptText
@@ -1032,6 +1135,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1032
1135
  .split('{REVIEW_TIER_REASON}').join('分级异常降级 self: ' + e.message)
1033
1136
  .split('{STAGE_REVIEW_RUN_ID}').join('review-unknown')
1034
1137
  .split('{REVIEW_JSON_CONTRACT}').join('(review 契约注入失败,按 schemaVersion=1 + reviewType + verdicts∈pass/fail/cannot_verify + reviewedFiles + docHash=主文档 sha256 产出)')
1138
+ .split('{PRIOR_REVIEW_FACTS}').join('')
1035
1139
  }
1036
1140
  }
1037
1141
 
@@ -1129,13 +1233,14 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1129
1233
  // 推荐组合、无从否决——故在 prompt 时点经 {EVIDENCE_AUTO_RECOMMENDATION} 把 task-11
1130
1234
  // resolveTestStrategy 的 evidence_auto_recommendation.summary 渲染进 step prompt(含推荐
1131
1235
  // 理由、降级注记与「可在 verify-result.md 否决并改跑全量」路径),供用户否决(FR-11)。
1132
- // 仅 test_strategy=evidence-auto 且 recommendation 非空时渲染;full/module/skip/未配置
1133
- // (recommendation=null)替换为空串零输出。fail-soft:读取/解析异常降级单行说明不抛,
1236
+ // 仅 test_strategy=evidence-auto 且 recommendation 非空时渲染推荐;full/module/skip/已配置
1237
+ // 替换为空串零输出;**未配置**且 commands.test 已配时渲染 test_strategy 预检提示(2026-09-15
1238
+ // 复盘:环境硬伤的全量实测打回返工,提前在 prompt 时点给配置指引)。fail-soft:读取/解析异常降级单行说明不抛,
1134
1239
  // 绝不阻断 verify prompt 输出(对齐上方 {WORKTREE_BASELINE_INFO} 注入先例)。
1135
1240
  if (stageName === 'verify' && promptText.includes('{EVIDENCE_AUTO_RECOMMENDATION}')) {
1136
1241
  let eaInjected = ''
1137
1242
  try {
1138
- const { resolveTestStrategy } = await import('../verify-postcheck.js')
1243
+ const { resolveTestStrategy, extractTestCommand, extractTestStrategy } = await import('../verify-postcheck.js')
1139
1244
  const eaSpecBase = resolvePromptSpecBase(platformOpts, cwd)
1140
1245
  // 信任边界:local.yaml 只读 specBase(平台 specRoot / 漂移锚定主仓 / cwd 本地),与
1141
1246
  // runVerifyTestCheck 的 specBase 来源同口径,不读 agent 可写的 worktree 副本。
@@ -1144,7 +1249,20 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1144
1249
  const eaChangeDir = changeName ? join(eaSpecBase, 'changes', changeName) : null
1145
1250
  const resolution = resolveTestStrategy({ yamlText: eaYamlText, changeDir: eaChangeDir })
1146
1251
  const rec = resolution.evidence_auto_recommendation
1147
- if (rec && rec.summary) eaInjected = rec.summary
1252
+ if (rec && rec.summary) {
1253
+ eaInjected = rec.summary
1254
+ } else if (eaYamlText && extractTestCommand(eaYamlText) && !extractTestStrategy(eaYamlText)) {
1255
+ // test_strategy 预检提示(2026-09-15 复盘教训「test_strategy 该在进 verify 前配好,
1256
+ // 而不是被 verify-test gate 打回后才补」):已配 commands.test 但顶层 test_strategy
1257
+ // 未设(缺省=全量实测对账),prompt 时点先给配置指引——环境注定跑不了全量时省一轮
1258
+ // 「--done 实测打回 → 补配置 → 重跑」返工
1259
+ eaInjected = '【test_strategy 预检提示】local.yaml 已配置 commands.test 但未设顶层 test_strategy(缺省=全量实测对账)。若该命令在当前环境无法真实执行(框架/环境硬伤,如测试框架硬编码 skip、依赖外部服务),先在 local.yaml 显式配 `test_strategy: skip`(或 `module` + modules 映射收窄)并注释理由再继续——避免 verify --done 实测对账打回后返工。'
1260
+ }
1261
+ // 等效验证先例注入(verify-precedent-only-in-prose,2026-09-15 EHS 实证):先例与
1262
+ // 预检提示同挂点追加——标准命令不可用但已有验证过的等效口径时,指引优先配进
1263
+ // commands.test 实测对账而非 skip 放行。
1264
+ const _precHint = renderVerifyPrecedentHint(parseVerifyPrecedents(eaYamlText))
1265
+ if (_precHint) eaInjected = eaInjected ? `${eaInjected}\n\n${_precHint}` : _precHint
1148
1266
  } catch (e) {
1149
1267
  eaInjected = `(evidence-auto 推荐注入失败:${e.message}——请读 .sillyspec/local.yaml 确认 test_strategy;--done 时 CLI 仍会按 resolveTestStrategy 实测对账,推荐不可用时可显式设 test_strategy: full/module)`
1150
1268
  }
@@ -1194,6 +1312,42 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1194
1312
  promptText = promptText.split('{SCOPE_AUDIT_TABLE}').join(scopeTable)
1195
1313
  }
1196
1314
 
1315
+ // {HANDOVER_SUMMARY}(2026-09-17-pass-cap-semantics task-04,FR-06 注入态):archive「确认
1316
+ // 归档」步的移交项清单注入——读 changes/<name>/verify-facts.json 的 facts.handover(task-01
1317
+ // 产出 { count, items[] },items 行含 type/item/condition/severity),blocking 置顶 + severity
1318
+ // 标注 + 封顶渲染(maxRows 8,对齐 {SCOPE_AUDIT_TABLE} 60/量级收窄——移交项是裁决面不是
1319
+ // 对账面),表尾指路 verify-facts.json 看全量。fail-soft:handover 段缺失/零条目/facts.json
1320
+ // 缺失 → 空串(正常态零输出);读取异常降级单行指引——注入永不阻断归档 prompt 输出
1321
+ //(与门阻断语义分离,对齐 {SCOPE_AUDIT_TABLE} 注入先例)。
1322
+ if (stageName === 'archive' && promptText.includes('{HANDOVER_SUMMARY}')) {
1323
+ let handoverBlock = ''
1324
+ try {
1325
+ const hsSpecBase = resolvePromptSpecBase(platformOpts, cwd)
1326
+ const hsFactsPath = changeName ? join(hsSpecBase, 'changes', changeName, 'verify-facts.json') : null
1327
+ if (hsFactsPath && existsSync(hsFactsPath)) {
1328
+ const hsFacts = JSON.parse(readFileSync(hsFactsPath, 'utf8'))
1329
+ const hsItems = (hsFacts && hsFacts.handover && Array.isArray(hsFacts.handover.items)) ? hsFacts.handover.items : []
1330
+ if (hsItems.length > 0) {
1331
+ const HS_MAX_ROWS = 8
1332
+ const sevOf = (it) => (it && it.severity === 'blocking') ? 'blocking' : 'advisory'
1333
+ const sorted = [...hsItems].sort((a, b) => (sevOf(a) === 'blocking' ? 0 : 1) - (sevOf(b) === 'blocking' ? 0 : 1))
1334
+ const rows = sorted.slice(0, HS_MAX_ROWS).map((it) => {
1335
+ const cond = it && it.condition ? ` | 条件: ${it.condition}` : ''
1336
+ return `- [${sevOf(it)}] ${(it && it.type) || 'unknown'} | ${(it && it.item) || ''}${cond}`
1337
+ })
1338
+ handoverBlock = [
1339
+ `【移交项清单(CLI 机械注入,共 ${hsItems.length} 条,blocking 置顶)】`,
1340
+ ...rows,
1341
+ ...(sorted.length > HS_MAX_ROWS ? [`……(其余 ${sorted.length - HS_MAX_ROWS} 条略——全量见 verify-facts.json)`] : []),
1342
+ ].join(String.fromCharCode(10))
1343
+ }
1344
+ }
1345
+ } catch (e) {
1346
+ handoverBlock = '(移交项清单注入失败:' + (e && e.message ? e.message : e) + '——回退读 changes/<变更名>/verify-facts.json 的 handover 段核对后再确认归档,注入失败不阻断归档)'
1347
+ }
1348
+ promptText = promptText.split('{HANDOVER_SUMMARY}').join(handoverBlock)
1349
+ }
1350
+
1197
1351
  // 注入模块上下文(brainstorm/plan/execute 阶段全步 + quick 首步「理解任务」——刀①:quick
1198
1352
  // step1 原让 agent cat module-map 再挑模块卡读,改为按任务描述匹配后注入,基于 Module Context Index)
1199
1353
  const quickFirstStep = stageName === 'quick' && step && step.name === '理解任务'
@@ -1352,10 +1506,15 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1352
1506
  // archive-batch-31-tool-notes ②:通用 --done 模板不带该 flag,agent 照抄执行撞
1353
1507
  // 「请添加 --confirm」确认门,误以为参数没带。
1354
1508
  const confirmFlag = step.requiresConfirm === true ? ' --confirm' : ''
1355
- // auto driver(2026-09-08-auto-driver D-002/P1-③):auto 模式下命令一律 run auto 形态——
1509
+ // auto driver(2026-09-08-auto-driver D-002/P1-Ⅲ):auto 模式下命令一律 run auto 形态——
1356
1510
  // 与 SS-META 的 doneCommand 同源(同一 cmdStage 变量),消灭「正文 run <stage> / 元数据 run auto」双命令
1357
1511
  const cmdStage = autoMeta ? 'auto' : stageName
1358
- const doneCommand = `sillyspec run ${cmdStage} --done${confirmFlag}${changeFlag} --output "你的摘要"`
1512
+ // 意图断言(坑 execute-concurrent-done-skips-next-wave 终解):execute 的 Wave 步骤是动态
1513
+ // 步骤表,并发 --done 会落到错误 Wave 且用旧摘要静默完成——完成命令带 --step "<本步名>"
1514
+ //(CLI 校验一致才放行),照抄命令即得并发防护。仅 execute 注入(quick/静态阶段测试断言
1515
+ // 提示格式,且静态步骤名稳定无漂移风险)。
1516
+ const stepAssertFlag = (stageName === 'execute' && step?.name) ? ` --step "${step.name}"` : ''
1517
+ const doneCommand = `sillyspec run ${cmdStage} --done${confirmFlag}${stepAssertFlag}${changeFlag} --output "你的摘要"`
1359
1518
  if (requiresWait) {
1360
1519
  console.log(`本步骤必须等待用户输入,不能直接 --done:`)
1361
1520
  console.log(`sillyspec run ${cmdStage} --wait --reason "${step.waitReason || '等待用户输入'}" --options "${(step.waitOptions || ['确认']).join(',')}"${changeFlag} --output "你的问题/方案摘要"`)
package/src/run/shared.js CHANGED
@@ -124,6 +124,12 @@ export function resolvePromptIncludes(text) {
124
124
  * home 拒绝守卫:home 下的 .sillyspec 恒不命中——历史污染源(smoke 测试在 home 下临时目录
125
125
  * 跑 CLI,_ensureDB 读路径即建库,~/.sillyspec 长出一整套平行进度库后,任何 home 子目录
126
126
  * 跑命令都会向上撞它,污染自我延续)。撞 home 即停,回退 cwd/.sillyspec(子目录自建)。
127
+ * tmpdir 硬边界(坑 temp-root-stray-sillyspec,2026-09-16-friction5-hardening verify 移交项①
128
+ * 实证):某次 cwd=%TEMP% 直系的调用在 Temp 根遗留游离 .sillyspec 后,所有 temp 夹具的祖先
129
+ * 链都被它劫持(agent-automation-batch4『add frontend 建档』/feedback-batch2 两测试假败的真
130
+ * 根因——specBase 解析到 Temp\.sillyspec,夹具内断言读自身路径 ENOENT)。temp 下合法的
131
+ * .sillyspec 只会在夹具自身内部:起点在 tmpdir 子树内时,tmpdir 层及其上永不检查、走到即停
132
+ * (比 home 守卫更硬:home 是「跳过一层继续走」,tmp 是「整段封顶」——tmpdir 之上无合法项目根)。
127
133
  * @param {string} cwd
128
134
  * @param {object} [opts]
129
135
  * @param {string} [opts.specDir] - 用户指定的 specDir(通过 --spec-dir 或 --spec-root)
@@ -132,11 +138,15 @@ export function resolvePromptIncludes(text) {
132
138
  export function resolveSpecDir(cwd, opts = {}) {
133
139
  if (opts.specDir) return resolve(opts.specDir)
134
140
  const home = os.homedir()
141
+ const tmp = os.tmpdir()
135
142
  const origin = resolve(cwd)
136
143
  const originBelowHome = origin.startsWith(home + sep) // 严格子目录,不含 home 自身
144
+ const originInTmp = origin === tmp || origin.startsWith(tmp + sep) // 含 tmpdir 自身
137
145
  let dir = origin
138
146
  let passedHome = false
139
147
  while (true) {
148
+ // tmpdir 硬边界:起点在 tmpdir 子树内时,走到 tmpdir 层即封顶(该层及其上是游离库温床,永不命中)
149
+ if (originInTmp && dir === tmp) break
140
150
  // home 拒绝守卫:起点在 home 子树内时,home 层不检查 .sillyspec,
141
151
  // 防止 home 子目录遍历经过 home 误命中 ~/.sillyspec;
142
152
  // passedHome:经过 home 后不再检查(覆盖遍历到真实 home 的场景)。
@@ -635,6 +645,71 @@ export function checkPlatformManaged(cwd) {
635
645
  }
636
646
  }
637
647
 
648
+ /**
649
+ * 平台模式产物落点指针(坑 platform-docs-dual-location,2026-09-15 EHS 生产实证:
650
+ * 变更 9 文档落 daemon specs 目录,工作树 .sillyspec/ 只剩旧变更残留、worktree 无
651
+ * .sillyspec——人类在工作树里找变更文档扑空,只有平台 changes files API 能读,双位置
652
+ * 造成「文件去哪了」困惑)。阶段 --done 完成时在工作树 .sillyspec/ 落一份人类可读指针
653
+ * PLATFORM-DOCS-POINTER.md:specRoot/runtimeRoot 物理路径 + workspaceId + 本变更文档
654
+ * 清单(.md 文件名+mtime + tasks/ 计数)+ 获取方式提示。
655
+ *
656
+ * 口径与 writePlatformPointer 三写同款豁免:本地模式(无 specRoot/runtimeRoot)零行为;
657
+ * 自指回环(repo-native junction 指回 cwd/.sillyspec)不写;temp 残留形态(specRoot 在
658
+ * 系统 temp 而项目不在)不写。fail-soft:任何写失败只 warn 不抛(对齐 user-inputs.md
659
+ * 追加先例)——指针是便利件,绝不阻断阶段完成。
660
+ *
661
+ * @param {string} cwd 项目根(指针落 cwd/.sillyspec/)
662
+ * @param {string|null} changeName 变更名(可空——无变更上下文时只写根路径头部)
663
+ * @param {{specRoot?: string|null, runtimeRoot?: string|null, workspaceId?: string|null}} [platformOpts]
664
+ * @param {string|null} [stageName] 触发阶段(展示用)
665
+ * @returns {{written: boolean, reason?: string, path?: string, error?: string}}
666
+ */
667
+ export function writePlatformDocsPointer(cwd, changeName, platformOpts = {}, stageName = null) {
668
+ try {
669
+ const specRoot = platformOpts?.specRoot || null
670
+ const runtimeRoot = platformOpts?.runtimeRoot || null
671
+ if (!specRoot && !runtimeRoot) return { written: false, reason: 'local-mode' }
672
+ if (specRoot && isSelfReferentialSpecRoot(cwd, specRoot)) return { written: false, reason: 'self-referential' }
673
+ if (isTempResidueSpecRoot(cwd, specRoot)) return { written: false, reason: 'temp-residue' }
674
+ const localSpecDir = join(cwd, '.sillyspec')
675
+ mkdirSync(localSpecDir, { recursive: true })
676
+ const ts = new Date().toLocaleString('zh-CN', { hour12: false })
677
+ const lines = [
678
+ '<!-- sillyspec 平台模式产物落点指针(CLI 在阶段 --done 完成时自动维护;本地模式不生成本文件) -->',
679
+ '# 变更文档实际落点(工作树 .sillyspec/ 不是本仓权威数据面)',
680
+ '',
681
+ `- 平台 spec 根(proposal/design/verify-result 等变更文档): ${specRoot || '(未指定)'}`,
682
+ `- 运行时根(worktrees/contract-artifacts/verify-logs): ${runtimeRoot || '(未指定)'}`,
683
+ platformOpts?.workspaceId ? `- 工作区 ID: ${platformOpts.workspaceId}` : null,
684
+ `- 最近更新: ${stageName || '?'} 阶段完成 @ ${ts}${changeName ? `(change: ${changeName})` : ''}`,
685
+ ].filter(Boolean)
686
+ const changeDir = (specRoot && changeName) ? join(specRoot, 'changes', changeName) : null
687
+ if (changeDir && existsSync(changeDir)) {
688
+ const docs = []
689
+ for (const f of readdirSync(changeDir)) {
690
+ if (!f.endsWith('.md')) continue
691
+ try {
692
+ const mtime = statSync(join(changeDir, f)).mtime.toLocaleString('zh-CN', { hour12: false })
693
+ docs.push(`- ${f}(mtime ${mtime})`)
694
+ } catch { docs.push(`- ${f}`) }
695
+ }
696
+ const tasksDir = join(changeDir, 'tasks')
697
+ if (existsSync(tasksDir)) {
698
+ const cards = readdirSync(tasksDir).filter(f => f.endsWith('.md'))
699
+ if (cards.length > 0) docs.push(`- tasks/(${cards.length} 张任务卡)`)
700
+ }
701
+ if (docs.length > 0) lines.push('', `## 本变更文档(${changeName})`, ...docs)
702
+ }
703
+ lines.push('', '获取方式:直接读上行物理路径,或经平台 changes files API(/changes/{id}/files/raw?path=...)逐文件下载。')
704
+ const pointerPath = join(localSpecDir, 'PLATFORM-DOCS-POINTER.md')
705
+ writeFileSync(pointerPath, lines.join('\n') + '\n')
706
+ return { written: true, path: pointerPath }
707
+ } catch (e) {
708
+ console.warn('⚠️ 平台产物落点指针写入失败(不阻断):', e && e.message ? e.message : e)
709
+ return { written: false, reason: 'error', error: e && e.message }
710
+ }
711
+ }
712
+
638
713
  /**
639
714
  * 自指 specRoot 判定(repo-native junction 回环检测,变更 2026-08-23-repo-native-spec-backfill)。
640
715
  *
@@ -1035,7 +1110,12 @@ export function isQuickMetadata(p, linkedChanges = []) {
1035
1110
  const file = normalizeGitPath(p)
1036
1111
  if (file.startsWith('.sillyspec/quicklog/')
1037
1112
  || file.startsWith('.sillyspec/.runtime/')
1038
- || file === '.sillyspec/knowledge/uncategorized.md'
1113
+ // 知识库整体白名单(2026-09-15 用户实测反馈:登记知识条目是 quick 收尾常规动作,此前
1114
+ // INDEX/known-issues/patterns/conventions/decisions 非元数据 → 危险门 blocked 只能 --force-baseline,
1115
+ // 且 perFileNotes 强制 --file-notes 覆盖):knowledge/ 是 CLI 自管面(classify append-only +
1116
+ // validate 守门),与 quicklog 同级。可追溯性靠 quicklog 结构化条目 + git diff,不靠文件行。
1117
+ // 前缀带尾斜杠:knowledge-base/ 等相近目录不命中。
1118
+ || file.startsWith('.sillyspec/knowledge/')
1039
1119
  || (/^\.sillyspec\/docs\/[^/]+\/modules\/[^/]+\.md$/.test(file))
1040
1120
  || (/^\.sillyspec\/docs\/[^/]+\/modules\/_module-map\.yaml$/.test(file))) return true
1041
1121
  if (file.startsWith('.sillyspec/changes/')) {