sillyspec 3.28.3 → 3.28.5

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 (77) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +54 -54
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  3. package/.claude/skills/sillyspec-continue/SKILL.md +17 -17
  4. package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
  5. package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -40
  6. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  7. package/.claude/skills/sillyspec-propose/SKILL.md +16 -16
  8. package/.claude/skills/sillyspec-resume/SKILL.md +21 -21
  9. package/.claude/skills/sillyspec-state/SKILL.md +25 -25
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +33 -33
  11. package/AGENTS.md +25 -0
  12. package/INSTRUCTIONS.md +12 -0
  13. package/package.json +1 -1
  14. package/results-review-tier.md +18 -0
  15. package/src/agent-session-log.js +4 -3
  16. package/src/archive-delta.js +82 -0
  17. package/src/change-delete.js +2 -1
  18. package/src/change-list.js +18 -3
  19. package/src/change-risk-profile.js +77 -6
  20. package/src/commit-suggest.js +52 -1
  21. package/src/config-cat.js +17 -1
  22. package/src/constants.js +3 -1
  23. package/src/decision-distill.js +45 -5
  24. package/src/decisions-io.js +96 -0
  25. package/src/design-facts.js +3 -0
  26. package/src/dispatch/probe.js +31 -8
  27. package/src/docs-check.js +102 -3
  28. package/src/doctor-diagnostics.js +441 -5
  29. package/src/foreign-declared.js +53 -2
  30. package/src/index.js +606 -22
  31. package/src/mcp-server.js +144 -0
  32. package/src/module-impact.js +84 -12
  33. package/src/modules.js +46 -34
  34. package/src/plan-adopt-waves.js +13 -5
  35. package/src/progress/change-registry.js +34 -0
  36. package/src/progress.js +75 -2
  37. package/src/quick-recommend.js +6 -1
  38. package/src/quicklog.js +70 -8
  39. package/src/review-dispatch.js +377 -0
  40. package/src/run/archive-distill.js +48 -0
  41. package/src/run/command.js +48 -13
  42. package/src/run/complete-handlers.js +215 -9
  43. package/src/run/complete.js +184 -2
  44. package/src/run/gate-snapshot.js +88 -0
  45. package/src/run/gates.js +206 -58
  46. package/src/run/prompt.js +150 -14
  47. package/src/run/quick-audit.js +38 -13
  48. package/src/run/shared.js +183 -12
  49. package/src/run/stage.js +52 -2
  50. package/src/run/verify-quality-scan.js +336 -0
  51. package/src/runtime-hygiene.js +84 -0
  52. package/src/scope-audit.js +735 -0
  53. package/src/sillyhub-mcp/client.js +58 -3
  54. package/src/spec-sync.js +63 -8
  55. package/src/stage-contract.js +83 -3
  56. package/src/stage-review-checklist.js +44 -0
  57. package/src/stage-review.js +125 -4
  58. package/src/stages/archive.js +60 -167
  59. package/src/stages/brainstorm.js +7 -17
  60. package/src/stages/doctor.js +41 -515
  61. package/src/stages/execute.js +13 -10
  62. package/src/stages/plan-postcheck.js +191 -7
  63. package/src/stages/plan.js +7 -35
  64. package/src/stages/quick.js +11 -11
  65. package/src/stages/verify.js +24 -39
  66. package/src/sync-noise.js +158 -0
  67. package/src/sync.js +93 -27
  68. package/src/task-review.js +133 -17
  69. package/src/taskcard.js +46 -3
  70. package/src/validate-artifacts.js +163 -0
  71. package/src/verify-facts-schema.js +141 -0
  72. package/src/verify-postcheck.js +347 -29
  73. package/src/verify-probes.js +275 -24
  74. package/src/workflow.js +6 -0
  75. package/src/worktree-apply.js +167 -24
  76. package/src/worktree.js +5 -1
  77. package/templates/agents-instruction.md +1 -0
package/src/run/gates.js CHANGED
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import { basename, join, relative, dirname } from 'node:path'
23
23
  import { createHash } from 'node:crypto'
24
+ import { execFileSync } from 'node:child_process'
24
25
  import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs'
25
26
  import { writeAtomicSync } from '../fs-atomic.js'
26
27
  import { triggerSync, resolveChangeDir, resolveRuntimeRoot } from './shared.js'
@@ -552,7 +553,35 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
552
553
  // runtimeRoot 进 context(坑 verify-receipt-runtime-split):verify 回执写入方
553
554
  // reapVerifyServices 落 resolveRuntimeRoot(platformOpts, specBase),校验器读侧不透传时
554
555
  // 平台/漂移模式恒读不到回执 → 证据门退回纯字面匹配(回执机制正是为消灭它而生)
555
- const contractResult = runValidators(stageName, cwd, changeName, { projectName, specRoot: platformOpts?.specRoot, runtimeRoot: resolveRuntimeRoot(platformOpts, specBase) })
556
+ //
557
+ // ── verify 收尾前置:facts slot-backfill + verifyStartAt(2026-09-08-ir-verify-facts FR-03)──
558
+ // 执行次序(design Phase 3):backfill(结论/证据/回执槽固化)先行 → runValidators(结论门/
559
+ // 集成证据门读得到固化值与 context.verifyStartAt)→ verify 专属块(test 实测 → tests 段二次
560
+ // 回填 → cannot_verify 硬门)。verifyStartAt 锚点(2026-09-10 用户反馈②放宽):execute 行
561
+ // **started_at**(证据合法产自 execute 或 verify 两窗口)→ 缺列/旧行退 completed_at →
562
+ // 再缺走 R-05 design created_at fallback。
563
+ let verifyStartAtIso = null
564
+ if (stageName === 'verify') {
565
+ try {
566
+ verifyStartAtIso = (pm.getStageStartedAt && pm.getStageStartedAt(cwd, changeName, 'execute'))
567
+ || pm.getStageCompletedAt(cwd, changeName, 'execute')
568
+ } catch { verifyStartAtIso = null }
569
+ try {
570
+ const verifyMdPath = join(specBase, 'changes', changeName, 'verify-result.md')
571
+ if (existsSync(verifyMdPath)) {
572
+ const verifyMd = readFileSync(verifyMdPath, 'utf8')
573
+ const { backfillFactsFromMdAndTests } = await import('../verify-probes.js')
574
+ const { extractVerifyConclusionSlot } = await import('../stage-contract.js')
575
+ backfillFactsFromMdAndTests(join(specBase, 'changes', changeName, 'verify-facts.json'), {
576
+ verifyMd,
577
+ conclusion: extractVerifyConclusionSlot(verifyMd),
578
+ })
579
+ }
580
+ } catch (e) {
581
+ console.warn(`⚠️ facts slot-backfill 失败(fail-soft,不阻断;门禁仍读 md 槽): ${e && e.message ? e.message : e}`)
582
+ }
583
+ }
584
+ const contractResult = runValidators(stageName, cwd, changeName, { projectName, specRoot: platformOpts?.specRoot, runtimeRoot: resolveRuntimeRoot(platformOpts, specBase), ...(verifyStartAtIso ? { verifyStartAt: verifyStartAtIso } : {}) })
556
585
  if (contractResult.errors.length > 0) {
557
586
  console.error(`\n❌ 阶段 ${stageName} 校验失败:`)
558
587
  for (const err of contractResult.errors) {
@@ -584,7 +613,9 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
584
613
  try {
585
614
  const { resolveApplyAllowSet } = await import('../worktree-apply.js')
586
615
  const { warnFacadeCandidateFiles } = await import('../facade-hint.js')
587
- const allowMap = resolveApplyAllowSet(cwd, changeName)
616
+ // specBase 显式传(2026-09-08 平台模式修复):changes/ 实体在 specRoot,本地目录常空——
617
+ // 旧硬编码在平台模式 allow 集恒空,facade 候选预检对空集全量误报
618
+ const allowMap = resolveApplyAllowSet(cwd, changeName, { specBase })
588
619
  warnFacadeCandidateFiles({ cwd, changeName, allowSet: allowMap.get('main') })
589
620
  } catch { /* 预检失败不阻断 plan 完成 */ }
590
621
  }
@@ -594,50 +625,46 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
594
625
  // 自报告 PASS 但实测失败 → 阻断(防止"文案通过"绕过验证)。
595
626
  if (stageName === 'verify') {
596
627
  const { runVerifyTestCheck, printVerifyTestCheck } = await import('../verify-postcheck.js')
597
- // 测试实测是同步 execSync,长套件可跑 2~10min 且中途无输出——先预告避免 agent 误判卡死
598
- console.log(`\n⏳ Verify 测试对账:CLI 亲自执行 local.yaml 的 commands.test(同步,耗时可能较长,请等待…)`)
599
- const testCheck = runVerifyTestCheck({ cwd, specBase, changeName, ctx })
600
- printVerifyTestCheck(testCheck)
628
+ // P0-1 指纹复用(noai-ir-roadmap §3 前置一·防重复跑):noAI 质量扫描步已实测且代码指纹
629
+ // 未变 → 直接复用免重跑(长套件 2~10 分钟不再跑两遍);无记录/失配/git 不可用 → 照旧
630
+ // 亲测(本路径语义与产物逐字不变)。
631
+ const { loadReusableQualityScan } = await import('./verify-quality-scan.js')
632
+ const reusableScan = loadReusableQualityScan({ specBase, cwd, changeName })
633
+ let testCheck
634
+ if (reusableScan && reusableScan.testResult) {
635
+ testCheck = reusableScan.testResult
636
+ console.log(`\n♻️ Verify 测试对账:复用 noAI 质量扫描步的实测结果(代码指纹匹配,免重跑;实测于 ${reusableScan.ranAt || '本变更 verify 期间'}${testCheck.resultPath ? `,台账 ${testCheck.resultPath}` : ''})`)
637
+ printVerifyTestCheck(testCheck)
638
+ } else {
639
+ // 测试实测是同步 execSync,长套件可跑 2~10min 且中途无输出——先预告避免 agent 误判卡死
640
+ console.log(`\n⏳ Verify 测试对账:CLI 亲自执行 local.yaml 的 commands.test(同步,耗时可能较长,请等待…)`)
641
+ testCheck = runVerifyTestCheck({ cwd, specBase, changeName, ctx })
642
+ printVerifyTestCheck(testCheck)
643
+ }
644
+ // tests 段二次回填(2026-09-08-ir-verify-facts FR-03 次序:实测在 runValidators 之后,
645
+ // tests 快照此刻才可得;不参与门禁——供 P3d 追溯)
646
+ try {
647
+ const verifyMdPath2 = join(specBase, 'changes', changeName, 'verify-result.md')
648
+ if (existsSync(verifyMdPath2)) {
649
+ const { backfillFactsFromMdAndTests } = await import('../verify-probes.js')
650
+ const { extractVerifyConclusionSlot } = await import('../stage-contract.js')
651
+ const verifyMd2 = readFileSync(verifyMdPath2, 'utf8')
652
+ backfillFactsFromMdAndTests(join(specBase, 'changes', changeName, 'verify-facts.json'), {
653
+ verifyMd: verifyMd2,
654
+ conclusion: extractVerifyConclusionSlot(verifyMd2),
655
+ testCheckResult: testCheck,
656
+ })
657
+ }
658
+ } catch { /* fail-soft:回填失败不影响门禁 */ }
601
659
  if (testCheck.status === 'failed') {
602
660
  console.error('\n❌ verify 阶段被阻断:verify-result.md 自报告通过,但 CLI 实测测试失败。')
603
- // 并行 WIP 归因鉴别(坑 verify-reconcile-foreign-wip):实测跑在主仓共享工作区,并行会话
604
- // 在途 WIP 物理在场——他者文件命中声明集时提示复验归因(EADDRINUSE 鉴别同款风格),不静默背锅。
661
+ // 并行 WIP 归因鉴别(坑 verify-reconcile-foreign-wip):单一实现迁至 verify-quality-scan.js
662
+ // renderVerifyTestAttribution(P0-1 前置二:noAI 质量扫描失败路径与本 --done 路径共用,
663
+ // 输出逐字保持)——实测跑在主仓共享工作区,并行会话在途 WIP 物理在场,他者文件命中
664
+ // 声明集时提示复验归因(含判据②生成产物旧基线 / verify-attr2 双过滤),不静默背锅。
605
665
  try {
606
- const { collectForeignDeclaredFiles } = await import('../verify-postcheck.js')
607
- const { gitQuiet } = await import('../git-helper.js')
608
- const foreignMap = collectForeignDeclaredFiles(cwd, changeName, { specBase, runtimeRoot: resolveRuntimeRoot(platformOpts, specBase) })
609
- // dirty 集提前算好:归因提示①(他者在途)与②(生成产物旧基线)共用
610
- const dirty = (gitQuiet(cwd, ['diff', '--name-only', 'HEAD']) || '')
611
- .split('\n').filter(Boolean).map(f => f.replace(/\\/g, '/'))
612
- const dirtySet = new Set(dirty)
613
- if (foreignMap.size > 0) {
614
- const hitForeign = dirty.filter(f => foreignMap.has(f))
615
- if (hitForeign.length > 0) {
616
- console.error(` ℹ️ 归因提示:主仓检出 ${hitForeign.length} 个并行会话声明的在途文件(${hitForeign.slice(0, 5).join(', ')}${hitForeign.length > 5 ? ' 等' : ''})——实测失败可能混入他者 WIP 而非本变更问题。待其提交/收尾后复验,仍失败才是本变更的。`)
617
- }
618
- }
619
- // 第二判据(坑 derived-artifact-stale-baseline):本变更 apply 的文件(apply-pathspec 落盘)
620
- // 与主仓近期提交重叠——本变更旧基线的生成产物可能覆盖了已合入内容,先在新基线重跑生成命令再复验。
621
- // 坑 verify-attr2-foreign-wip-misdirection(2026-08-28 实证):并行会话在途/近期收尾文件
622
- // 让重叠集虚高(51 文件),每轮误导 agent 重跑 gen:types,实际失败源是他者 WIP。两道过滤:
623
- // - 必须 dirty:apply 落盘的旧内容此刻物理在场(verify gate 时本变更未提交,旧基线覆盖
624
- // 必然表现为对 HEAD 的修改)——内容与 HEAD 一致时重跑生成命令改变不了本轮实测;
625
- // - 剔除他者声明集:那是归因提示①的领地(待其收尾后复验),不是生成产物过期。
626
- try {
627
- const pathspecFile = join(resolveRuntimeRoot(platformOpts, specBase), `apply-pathspec-${changeName}.txt`)
628
- if (changeName && existsSync(pathspecFile)) {
629
- const ownFiles = new Set(readFileSync(pathspecFile, 'utf8').split('\n').map(l => l.trim()).filter(Boolean).map(f => f.replace(/\\/g, '/')))
630
- if (ownFiles.size > 0) {
631
- const recent = (gitQuiet(cwd, ['log', '-n', '10', '--name-only', '--pretty=format:']) || '')
632
- .split('\n').map(l => l.trim().replace(/\\/g, '/')).filter(Boolean)
633
- const overlap = [...new Set(recent)].filter(f => ownFiles.has(f))
634
- const actionable = filterStaleBaselineOverlap(overlap, dirtySet, foreignMap)
635
- if (actionable.length > 0) {
636
- console.error(` ℹ️ 归因提示②:本变更有 ${actionable.length} 个文件与主仓最近 10 条提交重叠且旧内容仍在工作区(${actionable.slice(0, 5).join(', ')}${actionable.length > 5 ? ' 等' : ''})——若为生成产物(api-types 等),本变更可能在旧基线生成并覆盖了已合入内容;先在新基线重跑生成命令(如 gen:types)再复验。(重叠共 ${overlap.length} 个,其余未在工作区改动或属他者会话在途文件,已归提示①/无需处理)`)
637
- }
638
- }
639
- }
640
- } catch { /* 判据②失败不影响主流程 */ }
666
+ const { renderVerifyTestAttribution } = await import('./verify-quality-scan.js')
667
+ await renderVerifyTestAttribution({ cwd, changeName, specBase, runtimeRoot: resolveRuntimeRoot(platformOpts, specBase) })
641
668
  } catch { /* 归因提示失败不影响阻断语义 */ }
642
669
  console.error(' 请修复失败的测试并更新 verify-result.md 后重新完成此步骤。')
643
670
  return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
@@ -645,11 +672,30 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
645
672
  // lint 对账(2026-08-21 审查 CLI-1):test 侧"自报告 PASS 但实测失败→阻断"已闭环,
646
673
  // lint 侧此前纯口头——CLI 亲自执行 commands.lint,advisory 起步(失败打印不阻断,观察期后升级)
647
674
  const { runVerifyLintCheck, printVerifyLintCheck } = await import('../verify-postcheck.js')
648
- const lintCheck = runVerifyLintCheck({ cwd, specBase })
649
- if (lintCheck.status !== 'skipped') {
650
- console.log(`\n⏳ Verify lint 对账:CLI 亲自执行 local.yaml 的 commands.lint…`)
675
+ // P0-1 指纹复用:noAI 质量扫描记录同指纹时 lint 一并复用(lint tally 只记真实执行次数)
676
+ let lintCheck = (reusableScan && reusableScan.lintResult) || null
677
+ if (lintCheck) {
678
+ console.log(`\n♻️ Verify lint 对账:复用 noAI 质量扫描步的实测结果(代码指纹匹配,免重跑)`)
679
+ } else {
680
+ lintCheck = runVerifyLintCheck({ cwd, specBase })
681
+ if (lintCheck.status !== 'skipped') {
682
+ console.log(`\n⏳ Verify lint 对账:CLI 亲自执行 local.yaml 的 commands.lint…`)
683
+ }
651
684
  }
652
685
  printVerifyLintCheck(lintCheck)
686
+ // lint 硬门(2026-09-09 升硬:观察期 14 次 5 败全真阳性,pre-push 本就硬拦——fail-fast 前移;
687
+ // 逃生 SILLYSPEC_VERIFY_LINT_GATE=advisory)
688
+ const { shouldBlockVerifyLint } = await import('../verify-postcheck.js')
689
+ if (shouldBlockVerifyLint(lintCheck)) {
690
+ console.error('\n❌ verify 阶段被阻断:CLI 亲自实测 lint 失败(agent 的 lint 自报告与实测不符时以实测为准)。')
691
+ if (lintCheck.outputTail) {
692
+ const tail = lintCheck.outputTail.split('\n').slice(-8).join('\n')
693
+ console.error(' 输出(末尾):')
694
+ for (const line of tail.split('\n')) console.error(` | ${line}`)
695
+ }
696
+ console.error(' 修复后重跑 --done(进度不丢);确认要跳过实测请设 SILLYSPEC_VERIFY_LINT_GATE=advisory(审计留痕)。')
697
+ return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
698
+ }
653
699
  // 契约 parity 对账:扫前端 API 调用 vs execute 提取的 provider endpoint artifact。
654
700
  // 接线自 contract-matrix pipeline(verifyApiParity 的 CLI 入口)。
655
701
  const { runVerifyParityCheck, printVerifyParityCheck } = await import('../verify-postcheck.js')
@@ -660,12 +706,36 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
660
706
  const { runVerifyDeletionCheck, printVerifyDeletionCheck } = await import('../verify-postcheck.js')
661
707
  const deletionCheck = runVerifyDeletionCheck({ cwd, specBase, changeName })
662
708
  printVerifyDeletionCheck(deletionCheck)
663
- // ── required-evidence 对账(advisory,不阻断):闭合 execute→verify evidence 死链 ──
664
- // execute Task Review Gate 把 cannot_verify 任务的 evidence 落盘 verify-required-evidence.json,
665
- // 本探针查每个 cannot_verify 任务是否在 verify-result.md 体现(CLI 只查提及,满足度 agent 自报告)。
666
- const { runVerifyRequiredEvidenceCheck, printVerifyRequiredEvidenceCheck } = await import('../verify-postcheck.js')
667
- const evidenceCheck = runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName })
709
+ // ── required-evidence 对账(2026-09-08-ir-verify-facts FR-03:v2 起 cannot_verify 不闭环阻断)──
710
+ // execute Task Review Gate 把 cannot_verify 任务的 evidence 落盘 verify-required-evidence.json。
711
+ // v2 槽优先分类核验(FR-02):blocked = missing 无豁免或 satisfied 核验不过 → 阻断 verify 完成
712
+ // (此前 advisory 只查「提及」——死链不闭环);无槽存量 md 降级 legacy 子串对账(warning 不阻断)。
713
+ const { runVerifyRequiredEvidenceCheck, printVerifyRequiredEvidenceCheck, trackVerifyResultRegression } = await import('../verify-postcheck.js')
714
+ const evidenceCheck = runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName, verifyStartAt: verifyStartAtIso })
668
715
  printVerifyRequiredEvidenceCheck(evidenceCheck)
716
+ // verify-result.md 内容回退检测(2026-09-10 用户反馈③:报告被平台同步覆盖回旧版一次,
717
+ // 只能靠人眼发现重写):高水位指纹(hash+mtime)检出「内容变且 mtime 回退」→ ⚠️ 告警。
718
+ // 纯 advisory——真回退也由 agent 决定重写,CLI 只保证可见。
719
+ try {
720
+ const vr = trackVerifyResultRegression(specBase, changeName, join(specBase, 'changes', changeName, 'verify-result.md'))
721
+ if (vr.regressed) {
722
+ console.warn(`\n⚠️ verify-result.md 疑被覆盖回旧版(内容变化 + mtime 回退至 ${new Date(vr.prevMtime).toLocaleString('zh-CN')} 之前)——平台/daemon 按服务端旧版本回写的指纹。`)
723
+ console.warn(` 若你刚重写过请忽略本行(下次读取刷新高水位);否则当前盘上可能是旧内容,需对照记忆/git 重新补写。`)
724
+ }
725
+ } catch { /* 检测失败静默(advisory) */ }
726
+ if (evidenceCheck.status === 'blocked') {
727
+ console.error(`\n❌ verify 阶段被阻断:cannot_verify 证据账未闭环(${evidenceCheck.summary})。`)
728
+ for (const d of (evidenceCheck.detailed || [])) {
729
+ console.error(` - ${d.task} [${d.status}${d.exempt ? '/豁免' : ''}] ${d.reason}`)
730
+ for (const v of (d.verification || [])) {
731
+ if (v && v.reason) console.error(` · ${v.path || '(无路径)'} [${v.pathClass || '?'}] ${v.reason}(filesExist=${v.filesExist} mtimeOk=${v.mtimeOk} diffHit=${v.diffHit})`)
732
+ }
733
+ }
734
+ console.error(` 修复:在 verify-result.md「## 证据账(cannot_verify 任务)」槽段逐 task 一行——`)
735
+ console.error(` - task-NN: satisfied | verifiedFiles: <精确路径>(CLI 核验:代码类 存在×mtime×diff 交集;日志/文档类豁免 diff)`)
736
+ console.error(` - 确实无法验证 → 填 missing 并加(豁免:<一句话理由>)后缀;部分满足 → partial + 已核验路径`)
737
+ return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
738
+ }
669
739
  // ── target_files 声明 ↔ 实际改动 对账(task-05 / ir-stage-p3a:②ERROR 阻断 / ③WARNING 放行)──
670
740
  // 「计划落空」(task 卡声明没做)此前全盲区:review.json changedFiles 是 agent 手写不能当
671
741
  // 事实源,execute 的 scope creep 只有 symbol-impact 试图抓。reconcileTargetFiles 用 git 三源
@@ -721,6 +791,24 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
721
791
  if (probeBlocked) {
722
792
  return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
723
793
  }
794
+ // ── 行号漂移自动重锚(2026-09-09 §7-6:quick --done 已接,verify/archive 收尾补接)──
795
+ // 本变更 diff 中的 .md 文档经 autoReanchorDocRefs 定点重锚(fixable 唯一/优选命中才改 +
796
+ // 同口径回执),消灭「src 改动平移行号 → 下次 docs gate 拦」的人工往返。挂 verify 收尾即
797
+ // 覆盖 archive 面(漂移源是 src 改动,archive 的文档同步步不产生新 src 平移)。fail-open。
798
+ try {
799
+ const { resolveVerifyChangedFiles } = await import('../verify-postcheck.js')
800
+ const mdChangedVa = (resolveVerifyChangedFiles(cwd, changeName, null, { includeWorkingTree: true, specBase }) || [])
801
+ .filter(f => f.endsWith('.md') && !f.startsWith('.sillyspec/changes/'))
802
+ if (mdChangedVa.length > 0) {
803
+ const { autoReanchorDocRefs, readDocsCheckConfig } = await import('../docs-check.js')
804
+ let fixCfgVa = {}
805
+ try { fixCfgVa = readDocsCheckConfig(cwd) || {} } catch { fixCfgVa = {} }
806
+ const rVa = autoReanchorDocRefs(cwd, mdChangedVa, fixCfgVa)
807
+ if (rVa.applied > 0) {
808
+ console.log(`🔧 文档行号漂移自动重锚(${stageName} 收尾):${rVa.applied} 处已改写(${rVa.invalidBefore} → ${rVa.invalidAfter};剩余 ${rVa.remaining} 处需人工 sillyspec docs check)`)
809
+ }
810
+ }
811
+ } catch { /* 自动重锚失败不影响收尾(docs gate 兜底) */ }
724
812
  // ── module-impact 死信探针(blocking,债单 D-1/D-5):更新结果表 pending/待办行 → 阻断 verify ──
725
813
  // 与 archive 移动前校验(extractPendingDocSyncRows)同一口径,把死信号从 archive 提前到 verify:
726
814
  // agent 在 verify 阶段就须完成文档同步并回填 done/skipped,而非拖到归档被拦(修复 perf-remediation
@@ -827,6 +915,20 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
827
915
  if (!reviewResult.ok) {
828
916
  return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
829
917
  }
918
+ // 降级自审留痕(2026-09-10 用户反馈①:PI agent 等宿主无 Agent tool,prompt 降级条款允许
919
+ // 当前 agent 自审产出 review.json)。放行但独立性折损必须可见:⚠️ 审计行区分降级 review
920
+ // 与真子代理 review(事后可审计),与 ⚖️/🔍 软归属同哲学——可见的折损优于静默的伪装。
921
+ // channel 识别(2026-09-10 通道优先序批次):reviewer.channel 结构化落款优先,兼容
922
+ // reviewerNotes 首行「降级:」约定;platform 通道(P2 review-dispatch 落地后)ℹ️ 留痕
923
+ // missionId 供追溯,其余通道正常放行。
924
+ const { classifyReviewerChannel } = await import('../stage-review.js')
925
+ const reviewChannel = classifyReviewerChannel(reviewResult.review)
926
+ if (reviewChannel === 'self') {
927
+ console.warn(`\n⚠️ Stage Review 降级自审(tier=independent 但宿主无 Agent tool):${stageName} review 按 prompt 降级条款由当前 agent 自审产出——独立性折损已留痕(reviewerNotes 首行「降级:」),结论应附源码锚点补偿,建议人工抽查关键结论。`)
928
+ } else if (reviewChannel === 'platform') {
929
+ const mid = reviewResult.review && reviewResult.review.reviewer && reviewResult.review.reviewer.missionId
930
+ console.log(`\nℹ️ Stage Review 平台派发(channel=platform${mid ? `,missionId=${mid}` : ',missionId 未落款'}):${stageName} review 由平台独立 worker 产出(独立进程/会话)。`)
931
+ }
830
932
  }
831
933
  } catch (e) {
832
934
  // fail-closed:Gate 自身异常阻断完成,不静默放行(与 Task Review Gate 一致)
@@ -890,14 +992,16 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
890
992
  // marker 缺失:先扫描 execute-runs/ 既有目录找回真实 runId(与 getLatestStageReviewRunId
891
993
  // 目录扫描兜底同语义),避免 marker 丢失而 agent 已用旧 runId 落盘时,直接 generate 新 ID
892
994
  // 找不到旧 review、误判缺 review.json。仅当确实无既有 run 才 generate 新 ID 并落盘。
893
- const { generateExecuteRunId, resolveLatestExecuteRunId, stampExecuteRunChange } = await import('../task-review.js')
995
+ const { generateExecuteRunId, resolveLatestExecuteRunId, stampExecuteRunChange, claimExecuteRunId } = await import('../task-review.js')
894
996
  executeRunId = resolveLatestExecuteRunId({ runtimeRoot, changeName }) || ''
895
997
  if (!executeRunId) {
896
- executeRunId = generateExecuteRunId()
998
+ executeRunId = generateExecuteRunId(changeName)
897
999
  // 落盘(marker 缺失时 fallback 生成后写盘,保证后续 checkbox/gate 读到同一 ID)
898
1000
  // D-001#1 fallback 写入点:mkdir execute-runs/<runId>/tasks 先于 marker(不变量:
899
1001
  // marker 在则目录在)。不 try/catch——异常直穿外层 catch 走 fail-closed 阻断
900
1002
  //(gate 自身写 run 目录失败不能静默放行完成)。
1003
+ // 排他认领(坑 exec-run-id-same-second-collision):并行会话同秒碰撞在认领处消解
1004
+ executeRunId = claimExecuteRunId(runtimeRoot, executeRunId)
901
1005
  mkdirSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'), { recursive: true })
902
1006
  writeAtomicSync(runIdFile, executeRunId + '\n')
903
1007
  stampExecuteRunChange(runtimeRoot, executeRunId, changeName)
@@ -1175,9 +1279,13 @@ export async function completeStageGates({ stageName, cwd, changeName, platformO
1175
1279
  validateFileLocations(cwd, stageName, progress, changeName, specBase)
1176
1280
  }
1177
1281
 
1178
- // 辅助阶段完成后重置步骤(scan 等 auxiliary 阶段重置回 pending 可重跑)
1282
+ // 辅助阶段完成后重置步骤(scan 等 auxiliary 阶段重置回 pending 可重跑)。
1283
+ // archive 例外(P0-4 并步补丁):确认归档成为末步后本重置会与 unregisterChange 的终态
1284
+ // (steps 全 completed + current_stage=archive——平台按步骤数渲染完成度的数据源)互相
1285
+ // 覆盖,把已注销变更抹回 0/N。archive 是终态阶段(变更已注销,不存在「重跑归档」流),
1286
+ // 不参与可重跑重置。
1179
1287
  const stageDef = stageRegistry[stageName]
1180
- if (stageDef?.auxiliary) {
1288
+ if (stageDef?.auxiliary && stageName !== 'archive') {
1181
1289
  const freshSteps = (stageDef.steps || []).map(s => ({
1182
1290
  name: s.name,
1183
1291
  status: 'pending',
@@ -1527,11 +1635,50 @@ export function printProbeConsistencyCheck(r, envelope = buildProbeConsistencyEn
1527
1635
  * 一天多——verify prompt 要求 PID 按变更分片登记 .runtime/verify-services-<change>.pids。
1528
1636
  * 坑 verify-pids-cross-session-kill(2026-08-23):原单文件无归属,A 会话 --done 会把 B 正在
1529
1637
  * 收集 Runtime Evidence 的服务一并杀掉——分片后各回收各的;旧单文件兼容回收(升级过渡期)。
1638
+ * 坑 verify-service-process-leak 子进程泄漏(2026-09-10 驾驭小结第三批③,用户实锤 python
1639
+ * 子进程泄漏):原 process.kill(pid) 只杀登记的 shell 包装进程(`cmd /c` / `sh -c` 起的
1640
+ * uvicorn 等),子进程成了孤儿继续挂机——改 killProcessTree 进程树击杀(win taskkill /T /F
1641
+ * + POSIX 递归枚举子进程);prompt 指引同步补「优先登记服务本体(叶子)PID」。
1530
1642
  * 回执同样按变更分片(原单份被并行会话后写覆盖),checkIntegrationEvidence(stage-contract)
1531
1643
  * 读分片名注入证据匹配。best-effort:ESRCH(已退出)静默,kill 失败 warn 不阻断 verify。
1532
1644
  *
1533
1645
  * @returns {{ reaped: number, receiptPath: string|null }}
1534
1646
  */
1647
+ export function killProcessTree(pid, sig = 'SIGTERM') {
1648
+ const pidNum = parseInt(pid, 10)
1649
+ if (!Number.isInteger(pidNum) || pidNum <= 0) return false
1650
+ if (process.platform === 'win32') {
1651
+ // /T = 连同子进程树(shell 包装下的 python/node 服务本体);/F = 强制(长驻服务常吞 SIGTERM 等价物)
1652
+ try {
1653
+ execFileSync('taskkill', ['/PID', String(pidNum), '/T', '/F'], { stdio: 'ignore', timeout: 15000 })
1654
+ return true
1655
+ } catch (e) {
1656
+ // taskkill 对不存在进程退出码 128(等价 ESRCH,已退出按回收静默);其余(拒绝访问等)上抛
1657
+ if (e && (e.status === 128 || /could not be found|找不到/i.test(String(e.stderr || '') + String(e.message || '')))) return false
1658
+ throw e
1659
+ }
1660
+ }
1661
+ // POSIX:先递归杀子进程(shell 包装的子进程是服务本体),再杀登记进程本身
1662
+ for (const child of listChildPids(pidNum)) {
1663
+ try { killProcessTree(child, sig) } catch { /* 子进程已退出/权限不足不连坐父进程回收 */ }
1664
+ }
1665
+ try { process.kill(pidNum, sig); return true }
1666
+ catch (e) { if (e.code === 'ESRCH') return false; throw e }
1667
+ }
1668
+
1669
+ /** ps 枚举直接子进程 PID(POSIX;失败返回空——杀不到子进程不阻断父进程回收) */
1670
+ function listChildPids(parentPid) {
1671
+ try {
1672
+ const out = execFileSync('ps', ['-axo', 'pid=,ppid='], { encoding: 'utf8', timeout: 5000 })
1673
+ const children = []
1674
+ for (const line of String(out).split('\n')) {
1675
+ const m = line.trim().match(/^(\d+)\s+(\d+)$/)
1676
+ if (m && Number(m[2]) === parentPid) children.push(Number(m[1]))
1677
+ }
1678
+ return children
1679
+ } catch { return [] }
1680
+ }
1681
+
1535
1682
  export function reapVerifyServices(platformOpts, specBase, changeName) {
1536
1683
  const runtimeDir = resolveRuntimeRoot(platformOpts, specBase)
1537
1684
  const reapFile = (path, label) => {
@@ -1539,11 +1686,12 @@ export function reapVerifyServices(platformOpts, specBase, changeName) {
1539
1686
  const pids = readFileSync(path, 'utf8').split('\n').map(l => l.trim()).filter(l => /^\d+$/.test(l))
1540
1687
  const reaped = []; const failed = []
1541
1688
  for (const pid of pids) {
1542
- try { process.kill(parseInt(pid, 10), 'SIGTERM'); reaped.push(pid) }
1543
- catch (e) { if (e.code !== 'ESRCH') failed.push(`${pid}(${e.code || e.message})`) }
1689
+ // 进程树击杀(坑 verify-service-process-leak 子进程泄漏):连带 shell 包装下的服务本体
1690
+ try { if (killProcessTree(pid)) reaped.push(pid) }
1691
+ catch (e) { if (e.code !== 'ESRCH') failed.push(`${pid}(${e.code || e.status || e.message})`) }
1544
1692
  }
1545
1693
  try { unlinkSync(path) } catch {}
1546
- if (reaped.length > 0) console.log(`🧹 verify 服务进程已回收 ${reaped.length} 个${label}(PID: ${reaped.join(', ')})`)
1694
+ if (reaped.length > 0) console.log(`🧹 verify 服务进程已回收 ${reaped.length} 个${label}(PID: ${reaped.join(', ')},进程树击杀)`)
1547
1695
  return { reaped: reaped.length, failed }
1548
1696
  }
1549
1697
  const own = reapFile(join(runtimeDir, `verify-services-${changeName}.pids`), '')
package/src/run/prompt.js CHANGED
@@ -19,7 +19,7 @@
19
19
  * - loadModuleContextIndex/buildModuleContextInjection 内 require('fs'/'path') 改顶部静态 import
20
20
  */
21
21
  import { basename, join } from 'node:path'
22
- import { existsSync, readFileSync, mkdirSync } from 'node:fs'
22
+ import { existsSync, readFileSync, mkdirSync, readdirSync } from 'node:fs'
23
23
  import { writeAtomicSync } from '../fs-atomic.js'
24
24
  import { stageRegistry } from '../stages/index.js'
25
25
  import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, WAIT_MARKER_RE, QUICK_SID_RE } from './shared.js'
@@ -124,6 +124,63 @@ function buildModuleContextInjection(taskDescription, moduleIndex, specBase, pro
124
124
  return injection
125
125
  }
126
126
 
127
+ /**
128
+ * 读 quick session guard 的任务描述(刀①):启动 --input 由 stage.js 落 guard.taskDescription,
129
+ * 渲染 step1 模块上下文注入时作匹配源(changeName 是 quick-<hash> 无语义)。
130
+ * 与 <quicklog-id>/<linked-changes> 占位符同源同容错(session guard 优先,legacy 单文件回退)。
131
+ */
132
+ export function readQuickGuardField(changeName, specBase, field) {
133
+ try {
134
+ const sessionGuardFile = join(specBase, '.runtime', 'quick-sessions', changeName, 'guard.json')
135
+ const legacyGuardFile = join(specBase, '.runtime', 'quick-guard.json')
136
+ const guard = existsSync(sessionGuardFile)
137
+ ? JSON.parse(readFileSync(sessionGuardFile, 'utf8'))
138
+ : (existsSync(legacyGuardFile) ? JSON.parse(readFileSync(legacyGuardFile, 'utf8')) : null)
139
+ return guard ? (guard[field] || '') : ''
140
+ } catch {
141
+ return ''
142
+ }
143
+ }
144
+
145
+ /**
146
+ * quick step1 项目上下文摘要(刀①):CLI 代读 projects/*.yaml 与 CONVENTIONS.md 开头注入
147
+ * prompt——替代 agent 两轮 cat(quick 是最高频路径,省 token + 省轮次)。fail-soft:
148
+ * 读不到落单行说明,绝不阻断 prompt 输出。
149
+ */
150
+ export function buildQuickContextDigest(specBase, projectName) {
151
+ try {
152
+ const sections = []
153
+ // 项目登记摘要(name/path/status 一行一个——技术栈等扩展字段按行透传 top-level 短行)
154
+ const projectsDir = join(specBase, 'projects')
155
+ if (existsSync(projectsDir)) {
156
+ const lines = []
157
+ for (const f of readdirSync(projectsDir).filter(f => f.endsWith('.yaml'))) {
158
+ const text = readFileSync(join(projectsDir, f), 'utf8')
159
+ const name = (text.match(/^name:\s*(.+)$/m) || [])[1]
160
+ const path = (text.match(/^path:\s*(.+)$/m) || [])[1]
161
+ const status = (text.match(/^status:\s*(.+)$/m) || [])[1]
162
+ lines.push(`- ${name ? name.trim() : f.replace(/\.yaml$/, '')}(${path ? path.trim() : '?'})${status ? ` — ${status.trim()}` : ''}`)
163
+ }
164
+ if (lines.length > 0) sections.push('### 项目登记(projects/*.yaml 摘要)\n' + lines.join('\n'))
165
+ }
166
+ // CONVENTIONS.md 开头(截断保底——全文可能很长,注入是省读取税不是塞全文)
167
+ const convPath = join(specBase, 'docs', projectName, 'scan', 'CONVENTIONS.md')
168
+ if (existsSync(convPath)) {
169
+ const text = readFileSync(convPath, 'utf8')
170
+ const HEAD_CHARS = 1200
171
+ const head = text.length > HEAD_CHARS
172
+ ? text.slice(0, HEAD_CHARS) + `\n…(截断——全文 ${convPath},需要时再读)`
173
+ : text
174
+ sections.push('### 项目约定(CONVENTIONS.md 开头,CLI 代读)\n' + head)
175
+ }
176
+ return sections.length > 0
177
+ ? sections.join('\n\n')
178
+ : '(无项目登记/约定文档——需要时按 prompt 内路径自查)'
179
+ } catch (e) {
180
+ return `(项目上下文注入失败:${e && e.message ? e.message : e}——需要时再 cat {SPEC_ROOT}/projects/*.yaml 与 CONVENTIONS.md)`
181
+ }
182
+ }
183
+
127
184
  // parseModuleMapSimple 复用 modules.js 的 canonical 实现(合并历史 copy-paste 副本,2026-08-07;
128
185
  // 无循环依赖:modules.js 仅 import fs/path/db.js,prompt.js → modules.js 单向)。
129
186
  /**
@@ -566,6 +623,14 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
566
623
  promptText = promptText.replace(/\{LOCAL_COMMANDS\}/g, localBlock)
567
624
  }
568
625
 
626
+ // ①b {QUICK_CONTEXT_DIGEST}(刀①,2026-09-08):quick step1 的项目/约定上下文 CLI 代读注入
627
+ // ——替代 agent cat projects/*.yaml 与 CONVENTIONS.md 两轮文件读取(quick 是最高频路径)。
628
+ // fail-soft 同三件套;无 quick 占位符自然零输出。
629
+ if (stageName === 'quick' && promptText.includes('{QUICK_CONTEXT_DIGEST}')) {
630
+ const digestSpecBase = resolvePromptSpecBase(platformOpts, cwd)
631
+ promptText = promptText.replace(/\{QUICK_CONTEXT_DIGEST\}/g, buildQuickContextDigest(digestSpecBase, projectName))
632
+ }
633
+
569
634
  // ② {GIT_DIRTY}:工作区脏文件清单(git status --porcelain,quick 收尾步消费)
570
635
  if (promptText.includes('{GIT_DIRTY}')) {
571
636
  let dirtyBlock = ''
@@ -738,13 +803,15 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
738
803
  }
739
804
  } catch {}
740
805
  if (!runId) {
741
- const { generateExecuteRunId } = await import('../task-review.js')
742
- runId = generateExecuteRunId()
806
+ const { generateExecuteRunId, claimExecuteRunId } = await import('../task-review.js')
807
+ runId = generateExecuteRunId(changeName)
743
808
  // 落盘(与启动站点一致),保证 agent 收到的 ID == gate/checkbox 读取的 ID
744
809
  // D-001#1 fallback 写入点:mkdir execute-runs/<runId>/tasks 先于 marker(不变量:marker 在则目录在)。
745
810
  // 渲染路径异常不能炸 prompt 输出 → catch 内 console.error 留痕 + 保留降级(ID 注入继续,
746
811
  // 目录由下游 review 写入 / 其他写入点补齐)。
747
812
  try {
813
+ // 排他认领(坑 exec-run-id-same-second-collision):并行会话同秒碰撞在认领处消解
814
+ runId = claimExecuteRunId(runtimeRoot, runId)
748
815
  mkdirSync(join(runtimeRoot, 'execute-runs', runId, 'tasks'), { recursive: true })
749
816
  writeAtomicSync(runIdFile, runId + '\n')
750
817
  } catch (e) {
@@ -760,7 +827,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
760
827
  if (['brainstorm', 'plan', 'execute'].includes(stageName) && promptText.includes('{REVIEW_TIER}')) {
761
828
  try {
762
829
  const { classifyReviewTier } = await import('../review-tier.js')
763
- const { generateStageReviewRunId, renderReviewJsonContract, stageReviewMarkerPath } = await import('../stage-review.js')
830
+ const { generateStageReviewRunId, renderReviewJsonContract, stageReviewMarkerPath, readReviewChannelPriority } = await import('../stage-review.js')
764
831
  const tierSpecBase = resolvePromptSpecBase(platformOpts, cwd)
765
832
  const tierChangeDir = changeName ? join(tierSpecBase, 'changes', changeName) : null
766
833
  const designPath = tierChangeDir ? join(tierChangeDir, 'design.md') : null
@@ -794,8 +861,12 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
794
861
  console.log(` 📁 Stage Review 写入目录(直接复制给 review 子代理,勿手拼 runId):${reviewDir}/`)
795
862
  } catch {}
796
863
  }
797
- // review.json 产物契约(schema + 示例 + docHash 算法 + 重算提示)事前注入,与 validateStageReviewSchema 同源
798
- const reviewContractMd = renderReviewJsonContract({ stage: stageName, changeDir: tierChangeDir, reviewRunId, tier: tier.tier })
864
+ // review.json 产物契约(schema + 示例 + docHash 算法 + 重算提示)事前注入,与 validateStageReviewSchema 同源。
865
+ // 通道优先序显式传入(readReviewChannelPriority 吃精确 cwd——平台模式 specRoot 异位时
866
+ // process.cwd 兜底会读错 local.yaml,2026-09-10 通道配置批次);tier=independent 时契约
867
+ // 头部渲染「审查执行通道」段(按用户配置序,见 local.yaml review_dispatch.channel_priority)。
868
+ const reviewChannelPriority = readReviewChannelPriority(cwd)
869
+ const reviewContractMd = renderReviewJsonContract({ stage: stageName, changeDir: tierChangeDir, reviewRunId, tier: tier.tier, channelPriority: reviewChannelPriority })
799
870
  promptText = promptText
800
871
  .split('{REVIEW_TIER}').join(tier.tier)
801
872
  .split('{REVIEW_TIER_REASON}').join(tier.reason)
@@ -927,13 +998,62 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
927
998
  promptText = promptText.split('{EVIDENCE_AUTO_RECOMMENDATION}').join(eaInjected)
928
999
  }
929
1000
 
930
- // 注入模块上下文(brainstorm/plan/execute 阶段,基于 Module Context Index)
931
- if (['brainstorm', 'plan', 'execute'].includes(stageName) && projectName) {
1001
+ // {ARCHIVE_IMPACT_AUDIT}(2026-09-09 ql-20260909-004):archive extract-module-impact 步的
1002
+ // 三重核对机械预填——auditModuleImpactAgainstDiff 代算(module-impact × diff × map 归属),
1003
+ // agent 只裁决不一致项。fail-soft:审计异常降级单行指引(回退手跑 git diff)。
1004
+ if (stageName === 'archive' && promptText.includes('{ARCHIVE_IMPACT_AUDIT}')) {
1005
+ let auditBlock = ''
1006
+ try {
1007
+ const { auditModuleImpactAgainstDiff } = await import('../archive-delta.js')
1008
+ const auditSpecBase = resolvePromptSpecBase(platformOpts, cwd)
1009
+ const r = auditModuleImpactAgainstDiff({ cwd, changeName, specDir: auditSpecBase })
1010
+ auditBlock = [
1011
+ '【三重核对报告(CLI 机械预填)】',
1012
+ r.summary,
1013
+ ...(r.mismatches.length > 0 ? ['不一致项:', ...r.mismatches.map(m => ' - ' + m), '→ 逐项裁决并修正 module-impact.md;其余(未列出的 diff 文件为 .sillyspec 产物不参与核对)无需处理。'] : ['一致 ✓——第 3 步修正可跳过。']),
1014
+ ].join(String.fromCharCode(10))
1015
+ } catch (e) {
1016
+ auditBlock = '(三重核对机械预填失败:' + (e && e.message ? e.message : e) + '——回退手跑 git diff --name-only 比对)'
1017
+ }
1018
+ promptText = promptText.split('{ARCHIVE_IMPACT_AUDIT}').join(auditBlock)
1019
+ }
1020
+
1021
+ // {SCOPE_AUDIT_TABLE}(2026-09-10-change-scope-audit task-05,FR-03):archive「确认归档」步的
1022
+ // 变更范围对账全表注入——computeChangeScopeAudit + renderScopeAuditTable 代算(计划×实际
1023
+ // 三态 + 行数,D-003 同源:只 import scope-audit.js 导出,不自研采集)。maxRows 60 截断防
1024
+ // prompt 膨胀(R-03,表尾自带「完整表跑 scope-audit」指引)。fail-soft:注入异常降级单行
1025
+ // 指引(回退手跑 scope-audit 命令),不阻断归档 prompt 输出(对齐 {ARCHIVE_IMPACT_AUDIT}
1026
+ // 注入先例);ok=false(quick 会话不存在 / 实际侧整体失败等)同样附手跑指引——归档零新增
1027
+ // 阻断面(D-006)。
1028
+ if (stageName === 'archive' && promptText.includes('{SCOPE_AUDIT_TABLE}')) {
1029
+ let scopeTable = ''
1030
+ try {
1031
+ const { computeChangeScopeAudit, renderScopeAuditTable } = await import('../scope-audit.js')
1032
+ const saSpecBase = resolvePromptSpecBase(platformOpts, cwd)
1033
+ const saResult = await computeChangeScopeAudit({ cwd, specBase: saSpecBase, changeName, platformOpts })
1034
+ scopeTable = renderScopeAuditTable(saResult, { maxRows: 60 })
1035
+ if (saResult && saResult.ok === false) {
1036
+ scopeTable += '\n(对账降级——回退手跑 sillyspec scope-audit --change <name> 查看原因后再确认归档,注入失败不阻断归档)'
1037
+ }
1038
+ } catch (e) {
1039
+ scopeTable = '(变更范围对账表注入失败:' + (e && e.message ? e.message : e) + '——回退手跑 sillyspec scope-audit --change <name> 查看后再确认归档,注入失败不阻断归档)'
1040
+ }
1041
+ promptText = promptText.split('{SCOPE_AUDIT_TABLE}').join(scopeTable)
1042
+ }
1043
+
1044
+ // 注入模块上下文(brainstorm/plan/execute 阶段全步 + quick 首步「理解任务」——刀①:quick
1045
+ // step1 原让 agent cat module-map 再挑模块卡读,改为按任务描述匹配后注入,基于 Module Context Index)
1046
+ const quickFirstStep = stageName === 'quick' && step && step.name === '理解任务'
1047
+ if ((['brainstorm', 'plan', 'execute'].includes(stageName) || quickFirstStep) && projectName) {
932
1048
  const effectiveSpecBase = resolvePromptSpecBase(platformOpts, cwd)
933
1049
  const moduleIndex = loadModuleContextIndex(effectiveSpecBase, projectName)
934
1050
  if (moduleIndex && Object.keys(moduleIndex).length > 0) {
935
- // 尝试从 step prompt / changeName 匹配模块
936
- const taskDesc = step.prompt || changeName || ''
1051
+ // 尝试从 step prompt / changeName 匹配模块;quick 用 guard.taskDescription(启动 --input)——
1052
+ // changeName 是 quick-<hash> 无语义、step.prompt 全文噪音大(刀①)
1053
+ let taskDesc = step.prompt || changeName || ''
1054
+ if (stageName === 'quick') {
1055
+ taskDesc = readQuickGuardField(changeName, effectiveSpecBase, 'taskDescription') || taskDesc
1056
+ }
937
1057
  const injection = buildModuleContextInjection(taskDesc, moduleIndex, effectiveSpecBase, projectName)
938
1058
  if (injection) {
939
1059
  promptText = injection + '\n' + promptText
@@ -996,7 +1116,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
996
1116
  console.log('- CLI 子命令以本 prompt 或上一条 --done 输出的字面为准;不确定时停下问用户,猜测命令会让状态机推进到错误位置。')
997
1117
  console.log('- 本步骤产物落盘后立即执行 prompt 末尾的 --done:CLI 据此校验产出并推进状态机;不跑则进度永远停在本步。')
998
1118
  console.log('- 变更目录改名用 `sillyspec change-rename <旧名> <新名>`:mv/rename 会漏改进度库引用,导致变更失联。')
999
- console.log('- 文档类型文件(.md/.yaml/.json 等)头部必须包含 author(git 用户名)和 created_at(精确到秒)')
1119
+ console.log('- 变更产物文档一律用 CLI 骨架命令生成(fourpiece-init / design-init / taskcard / module-impact / verify-probes --init)——骨架已预填 author/created_at/generated_by 元数据,勿删勿手拼 frontmatter;仅从零手写的补充文档才需手填 author(git 用户名)与 created_at(精确到秒)')
1000
1120
  console.log('- 执行构建/测试前必须先读 local.yaml,优先使用其中配置的命令、路径和环境变量;未配置时才使用默认值')
1001
1121
  }
1002
1122
  // 平台模式 + 路径规则(安全关键,每步注入;step1+ 起带精简标题,不复述通用铁律)
@@ -1032,6 +1152,13 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1032
1152
  const mayNeedWait = WAIT_MARKER_RE.test(stepPrompt) || requiresWait || conditionalWait
1033
1153
 
1034
1154
  console.log(`\n### 完成后执行`)
1155
+ // P0-3(noai-ir-roadmap §3):gate 预检前置——gate 失败 → rollback → 重做一轮是最贵的循环,
1156
+ // 多数失败是机械的(占位符未替换/文件未建/行号失效)。gate 是只读聚合预检,--done 前跑一次
1157
+ // 把「重做一轮」变「本地预检」。主阶段且有 change 才提示(quick 边界审计在 --done 内联、
1158
+ // 辅助阶段无 gate 语义)。
1159
+ if (['brainstorm', 'plan', 'execute', 'verify', 'archive'].includes(stageName) && changeName) {
1160
+ console.log(`💡 预检(只读,省一轮 gate 失败重跑):sillyspec gate ${stageName} --change ${changeName} [--json]`)
1161
+ }
1035
1162
  // requiresConfirm 步骤(如 archive「确认归档」)的完成命令必须带 --confirm——坑
1036
1163
  // archive-batch-31-tool-notes ②:通用 --done 模板不带该 flag,agent 照抄执行撞
1037
1164
  // 「请添加 --confirm」确认门,误以为参数没带。
@@ -1055,15 +1182,24 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1055
1182
  console.log(`如果不需要用户决策,正常完成:`)
1056
1183
  }
1057
1184
  console.log(doneCommand + (autoMeta ? '' : ' --input "用户原始需求/反馈"'))
1185
+ // P0-2(noai-ir-roadmap §3):--output 可省略提示(auto 模式除外——auto driver 的
1186
+ // SS-META doneCommand 仍带 --output,照抄路径零变化;省略路径由 command.js 同源合成)。
1187
+ if (!autoMeta) {
1188
+ console.log(`(--output 可省略:省略时 CLI 按门禁与 diff 事实合成事实性摘要;方案取舍/用户反馈等语义说明才需要手写)`)
1189
+ }
1058
1190
 
1059
1191
  // ── SS-META 机器可读元数据块(change: 2026-09-08-auto-driver,D-002@v1,FR-01)──
1060
- // 仅 auto 模式(autoMeta 非空)渲染:单行 HTML 注释内 JSON,agent 低噪可读、脚本一行正则
1061
- // 可提取(<!--SS-META:(.*)-->)。requiresUser 四源见上方纯函数;doneCommand 与正文同源。
1062
- if (autoMeta) {
1192
+ // 单行 HTML 注释内 JSON,agent 低噪可读、脚本一行正则可提取(<!--SS-META:(.*)-->)。
1193
+ // requiresUser 四源见上方纯函数;doneCommand 与正文同源。
1194
+ // P1-3(noai-ir-roadmap §4):SS-META 从 auto 专属毕业——常规模式 `run <stage> --meta`
1195
+ // 同样渲染(command.js 置 SILLYSPEC_RUN_META=1,CLI 短进程生命周期内有效,不跨调用泄漏);
1196
+ // meta 增补 change 字段(宿主脚本免再从正文 header 行解析变更名,向后兼容追加)。
1197
+ if (autoMeta || process.env.SILLYSPEC_RUN_META === '1') {
1063
1198
  const meta = {
1064
1199
  stage: stageName,
1065
1200
  stepIndex: stepIndex + 1,
1066
1201
  stepName: step.name,
1202
+ change: changeName || null,
1067
1203
  requiresUser: requiresWait || mayNeedWait || step.requiresConfirm === true,
1068
1204
  doneCommand,
1069
1205
  waitHint: (requiresWait || mayNeedWait)