sillyspec 3.29.5 → 3.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +1 -1
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
  3. package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
  4. package/.claude/skills/sillyspec-flow/SKILL.md +57 -0
  5. package/.claude/skills/sillyspec-quick/SKILL.md +7 -105
  6. package/AGENTS.md +38 -20
  7. package/SKILL.md +9 -3
  8. package/assets/command-cards/flow.md +32 -0
  9. package/assets/command-cards/run-brainstorm.md +1 -1
  10. package/assets/command-cards/run-quick.md +8 -25
  11. package/package.json +2 -1
  12. package/packages/dashboard/server/executor.js +2 -1
  13. package/packages/dashboard/server/parser.js +7 -7
  14. package/r7-test-env/README-R7-TEST.md +93 -0
  15. package/r7-test-env/demo-project/lib/calc.js +6 -0
  16. package/r7-test-env/demo-project/package.json +1 -0
  17. package/r7-test-env/demo-project/test.js +10 -0
  18. package/r7-test-env/prompt-R7-SMOKE.md +41 -0
  19. package/r7-test-env/sillyspec-r7.cmd +3 -0
  20. package/round4/account.mjs +18 -5
  21. package/round4/b1-analyze-task08.mjs +40 -0
  22. package/round4/b1-b1-target-validation.md +32 -0
  23. package/round4/b3a-prompt-audit-result.md +66 -0
  24. package/round4/b3a-prompt-audit.mjs +89 -0
  25. package/round4/optimization-plan.md +165 -0
  26. package/round4/r4-final-report.html +347 -0
  27. package/round4/r5-scenario-matrix.md +46 -0
  28. package/src/change-list.js +9 -0
  29. package/src/classify-change.js +6 -0
  30. package/src/command-cards.js +2 -1
  31. package/src/commit-guard.js +2 -0
  32. package/src/config-schema.js +64 -12
  33. package/src/db.js +35 -4
  34. package/src/decision-distill.js +9 -3
  35. package/src/docs-check.js +1 -1
  36. package/src/doctor-diagnostics.js +44 -2
  37. package/src/flow-draft.js +748 -0
  38. package/src/flow-parity.js +236 -0
  39. package/src/flow-review.js +184 -0
  40. package/src/flow.js +1492 -0
  41. package/src/foreign-declared.js +17 -1
  42. package/src/fr-index.js +396 -22
  43. package/src/fs-atomic.js +5 -1
  44. package/src/git-helper.js +2 -2
  45. package/src/greenfield-bootstrap.js +98 -0
  46. package/src/handoff.js +120 -7
  47. package/src/hunk-attribution.js +191 -0
  48. package/src/index.js +570 -16
  49. package/src/knowledge-classify.js +47 -1
  50. package/src/knowledge-digest.js +162 -0
  51. package/src/knowledge-quicklog.js +246 -0
  52. package/src/machine-draft.js +128 -0
  53. package/src/machine-interface.js +107 -5
  54. package/src/mcp-server.js +1 -0
  55. package/src/module-resolve.js +3 -1
  56. package/src/preview-progress.js +160 -0
  57. package/src/progress/change-registry.js +11 -3
  58. package/src/progress/shared.js +3 -2
  59. package/src/progress.js +56 -13
  60. package/src/quicklog.js +44 -6
  61. package/src/redomain.js +147 -0
  62. package/src/review-material-pack.js +204 -1
  63. package/src/run/archive-distill.js +4 -2
  64. package/src/run/bg-sync.js +67 -0
  65. package/src/run/command.js +123 -10
  66. package/src/run/complete-handlers.js +327 -72
  67. package/src/run/complete.js +350 -71
  68. package/src/run/gate-snapshot-ledger.js +213 -0
  69. package/src/run/gate-snapshot.js +250 -34
  70. package/src/run/gates.js +370 -233
  71. package/src/run/green-cache.js +123 -0
  72. package/src/run/junction-rm.js +70 -0
  73. package/src/run/next.js +14 -0
  74. package/src/run/prompt.js +185 -55
  75. package/src/run/quick-audit.js +185 -12
  76. package/src/run/shared.js +120 -3
  77. package/src/run/stage.js +182 -216
  78. package/src/run/test-ledger.js +264 -0
  79. package/src/run/verify-quality-scan.js +84 -7
  80. package/src/scope-audit.js +6 -1
  81. package/src/sentinel-assertions.js +114 -0
  82. package/src/setup.js +3 -3
  83. package/src/spec-sync.js +41 -1
  84. package/src/stage-contract.js +26 -8
  85. package/src/stage-review-checklist.js +2 -0
  86. package/src/stage-review.js +7 -2
  87. package/src/stage-templates.js +157 -0
  88. package/src/stages/archive.js +2 -0
  89. package/src/stages/brainstorm.js +24 -106
  90. package/src/stages/execute.js +221 -84
  91. package/src/stages/knowledge.js +74 -1
  92. package/src/stages/plan-postcheck.js +352 -1
  93. package/src/stages/plan.js +23 -1
  94. package/src/stages/quick.js +10 -2
  95. package/src/stages/verify.js +4 -2
  96. package/src/sync.js +81 -23
  97. package/src/task-done.js +181 -0
  98. package/src/task-review.js +8 -55
  99. package/src/test-bindings.js +549 -0
  100. package/src/timeline.js +218 -0
  101. package/src/ui-visual.js +175 -0
  102. package/src/verify-draft.js +214 -0
  103. package/src/verify-postcheck.js +860 -158
  104. package/src/verify-probes.js +59 -3
  105. package/src/watcher.js +1031 -0
  106. package/src/worktree-apply.js +44 -4
  107. package/src/worktree-deps.js +22 -8
  108. package/src/worktree-policy.js +42 -0
  109. package/src/worktree.js +102 -15
  110. package/templates/agents-instruction.md +37 -18
@@ -52,6 +52,8 @@ import { resolveSessionIdentity } from '../progress.js'
52
52
  // checkDbScriptDeclarationGate(task-04 / FR-05)同排静态引入:门函数自身纯文件集 ×
53
53
  // 文本声明对账(verify-probes 文法经 worktree-apply 顶层动态绑定),同样无环约束。
54
54
  import { chunkPaths, checkDbScriptDeclarationGate } from '../worktree-apply.js'
55
+ import { parseModulePathsSubset } from '../decision-distill.js'
56
+ import { parseUncategorizedEntries } from '../knowledge-classify.js'
55
57
 
56
58
  /**
57
59
  * 清洗项目名:只保留 ASCII 字母/数字/横线/下划线/点,过滤中文和特殊字符。
@@ -527,6 +529,25 @@ export function archiveNarrowedGitAdd({ cwd, specBase, destDir, destName }) {
527
529
  // changes 侧:本变更归档目录精确收窄(quick 轻量归档 closeSingleQuickLinkedChange 同款)
528
530
  safeGit(cwd, ['add', '--', `.sillyspec/changes/archive/${destName}/`])
529
531
 
532
+ // knowledge 侧(cli-protocol-trust / R17 臂3 实证 distill 产物 untracked 漏提交):本归档链
533
+ // distill 刚写的 fr 域文件/decisions/INDEX 必在 status 面——按 status 窄化逐文件精确 add
534
+ // (非目录级:避免扫入他侧并行会话的同目录 WIP;追加型共享面整文件提交与既有惯例一致)。
535
+ try {
536
+ const _knSt = gitQuiet(cwd, ['status', '--porcelain'])
537
+ if (_knSt) {
538
+ const knFiles = String(_knSt).split('\n').map((l) => {
539
+ const p = l.slice(3).trim().replace(/^"|"$/g, '')
540
+ const arrow = p.indexOf(' -> ')
541
+ return arrow !== -1 ? p.slice(arrow + 4) : p // rename 形态取新路径(评审 P3 清偿)
542
+ }).filter((p) => p.replace(/\\/g, '/').startsWith('.sillyspec/knowledge/'))
543
+ for (const batch of chunkPaths(knFiles)) safeGit(cwd, ['add', '--', ...batch])
544
+ if (knFiles.length > 0) console.log(` 📚 knowledge 蒸馏产物入暂存:${knFiles.length} 个文件(fr 域/decisions/INDEX)`)
545
+ else console.log(' ℹ️ knowledge 面无待入暂存文件(本归档无蒸馏产物变更)')
546
+ }
547
+ } catch (e) {
548
+ console.warn(`⚠️ knowledge 侧 add 失败(fail-soft,不阻断归档链):${e && e.message ? e.message : e}`)
549
+ }
550
+
530
551
  // docs 侧:module-impact「更新结果」done 行 → 精确文件集。
531
552
  // 三态(对齐修复④要求):
532
553
  // - module-impact.md 无「## 更新结果」段 / 文件缺失 / 读取异常 → docPaths=null → 回退目录级
@@ -570,56 +591,161 @@ export function archiveNarrowedGitAdd({ cwd, specBase, destDir, destName }) {
570
591
  return { docsAdded: null, fallbackDocs: true }
571
592
  }
572
593
 
573
- export async function archiveChangeDirectory(pm, cwd, progress, specBase, platformOpts = {}, gateOpts = {}) {
574
- const archiveChangeName = progress.currentChange
575
- if (!archiveChangeName) {
576
- console.error('❌ 归档失败:未找到当前变更名(currentChange)')
577
- process.exit(1)
594
+ /**
595
+ * flow 变更交付面推导(资产三小件③)——flow.js changedFilesSinceBaseline 同口径本仓实现
596
+ * (flow.js 为并行会话在改,不 cross-import;两处语义等价:diff 基线→HEAD + porcelain dirty,
597
+ * 排除 .sillyspec/ 内部产物)。docsIncrement 单列 .sillyspec/docs/**(文档认领增量信号)。
598
+ * @returns {{ deliverableFiles: string[], docsIncrement: string[] }|null}
599
+ * 非 flow 变更(无 flow-state.yaml / 无 baseline_commit / 读取异常)→ null(调用方跳过 advisory)
600
+ */
601
+ export function deriveFlowDeliverableFace({ cwd, srcDir }) {
602
+ let baseline = null
603
+ try {
604
+ const raw = readFileSync(join(srcDir, 'flow-state.yaml'), 'utf8')
605
+ const m = raw.match(/^baseline_commit:\s*'?([0-9a-f]{7,40})'?\s*$/m)
606
+ baseline = m ? m[1] : null
607
+ } catch { return null }
608
+ if (!baseline) return null
609
+ const all = new Set()
610
+ const addAll = (out) => {
611
+ if (!out) return
612
+ for (const l of String(out).split('\n')) {
613
+ const p = l.trim().replace(/\\/g, '/')
614
+ if (p) all.add(p)
615
+ }
578
616
  }
579
- const changesDir = join(specBase, 'changes')
580
- const archiveDir = join(changesDir, 'archive')
581
- const srcDir = join(changesDir, archiveChangeName)
582
- const date = new Date().toISOString().slice(0, 10)
583
- const destName = archiveDestDirName(date, archiveChangeName)
584
- const destDir = join(archiveDir, destName)
585
-
586
- if (!existsSync(srcDir)) {
587
- // 幂等自愈(issue archive-stage-physical-tracking-desync):源目录已不存在,但变更可能已被
588
- // 手动 / 部分流程移到 changes/archive/ 并 commit,而 --done --confirm 从未正式跑完 → 进度 DB
589
- // 卡 archive 阶段、active 列表仍列此 change。检测到已归档则回填进度(unregisterChange)并
590
- // 成功返回,让收尾流程把 archive 阶段标完成,而非 exit(1) 死路(source 已移走无法重跑 move)。
591
- const alreadyArchivedDir = findAlreadyArchivedDir(archiveDir, archiveChangeName)
592
- if (alreadyArchivedDir) {
593
- console.log(`ℹ️ 源目录不存在但变更已在 archive/(${basename(alreadyArchivedDir)}),判定已归档,自愈进度 DB`)
594
- // 终态一致化(坑 manual-archive-desync-status-only):手动搬目录绕过标准归档后,本自愈路径
595
- // 不能只翻 status——同时收尾 archive 阶段(steps/stages/current_stage),否则推送平台的终态
596
- // 是「已归档 + 归档 0/5 + 停在 execute」的矛盾体,详情页渲染成「进度丢失」
597
- pm.unregisterChange(cwd, archiveChangeName, { archiveStepNames: typeof pm.archiveStepNamesForArchive === 'function' ? pm.archiveStepNamesForArchive() : null })
598
- await archiveWorktreeCleanup(cwd, archiveChangeName, specBase, platformOpts)
599
- console.log(`📦 已自愈归档:${archiveChangeName} → archive/${basename(alreadyArchivedDir)}/`)
600
- return alreadyArchivedDir
617
+ addAll(gitQuiet(cwd, ['diff', '--name-only', `${baseline}..HEAD`]))
618
+ const s = gitQuiet(cwd, ['status', '--porcelain'])
619
+ if (s) {
620
+ for (const line of String(s).split('\n')) {
621
+ if (!line || line.length < 4) continue
622
+ const p = line.slice(3).trim().replace(/^"|"$/g, '')
623
+ const arrow = p.indexOf(' -> ')
624
+ all.add((arrow !== -1 ? p.slice(arrow + 4) : p).replace(/\\/g, '/'))
601
625
  }
602
- console.error(`❌ 归档失败:源目录不存在 ${srcDir}`)
603
- console.error(` 且 changes/archive/ 下未找到该变更的归档目录。若已手动归档请核对目录名;否则先补全变更产物。`)
604
- process.exit(1)
605
626
  }
606
- // ── 所有权护栏(2026-09-14-change-ownership-guards task-03 / FR-01 / D-001@v1 接线点)──
607
- // 归档=接管类操作(注销 change + 移目录 + 清 worktree),他人活跃 change 硬拒。判定在
608
- // withMainRepoLock 锁内(handleArchiveConfirmStep 调用点),判定-执行无 TOCTOU;放行分支
609
- // no-owner/takeover-stale 重写 owner=本会话(setChangeOwner 同事务刷新 last_active,心跳从
610
- // 接管时刻起算)。会话标识三级解析:--session > env SILLYSPEC_SESSION_ID > anon@host(降级
611
- // 时 resolveSessionIdentity 自带教学 warning)。消费 task-02 契约(assertChangeOwnership
612
- // 纯读不写库),不重复实现判定语义。
613
- {
614
- const { session } = resolveSessionIdentity({ flagSession: gateOpts.sessionFlag || null, cwd })
615
- const check = pm.assertChangeOwnership(cwd, archiveChangeName, { selfSession: session, nowMs: Date.now() })
616
- if (!check.allowed) {
617
- console.error(`🚫 归档失败:变更 ${archiveChangeName} 正被其他会话持有(owner: ${check.owner},最后活跃: ${check.lastActive || '未知'}),活跃窗口 ${Math.round(check.heartbeatMs / 60000)} 分钟内拒绝归档`)
618
- console.error(` 等对方会话收尾后重试(窗口过后自动接管);确认对方已放弃且本会话应接管:先经 sillyspec worktree apply/cleanup ${archiveChangeName} --takeover 显式接管(重写 owner 留痕)再归档`)
619
- process.exit(1)
627
+ return {
628
+ deliverableFiles: [...all].filter((p) => !p.startsWith('.sillyspec/')),
629
+ docsIncrement: [...all].filter((p) => p.startsWith('.sillyspec/docs/')),
630
+ }
631
+ }
632
+
633
+ /**
634
+ * 模块文档认领 advisory(资产三小件③,advisory 不阻断归档链)。
635
+ *
636
+ * 背景:flow done/burst 收口不产生模块文档增量(旧流程的文档同步挂在 execute/verify 步骤里)
637
+ * ——R7-L 重放实证零模块文档的资产缺口。判定:deliverableFiles 交付面 × module-map
638
+ * (discoverModuleIndex 同源解析 parseModulePathsSubset,按项目扫描保卡片路径归属)交集
639
+ * ≥1 且本次变更无 .sillyspec/docs/** 增量 → 一行警告列出未认领模块卡路径。
640
+ * --no-docs 豁免仅存在于 quick 出口(run/command.js L2 docClaim),archive 链无该 flag——
641
+ * 出现豁免语义时在 runArchiveChain 接线处尊重。
642
+ * @returns {string|null} 命中 → 警告文案;负例(纯 spec 变更/有文档增量/无交集/无 map)→ null
643
+ */
644
+ export function buildArchiveModuleDocAdvisory({ specBase, deliverableFiles, docsIncrement }) {
645
+ if (!Array.isArray(deliverableFiles) || deliverableFiles.length === 0) return null
646
+ if (Array.isArray(docsIncrement) && docsIncrement.length > 0) return null
647
+ const posix = deliverableFiles.map((f) => String(f).replace(/\\/g, '/')).filter((f) => f && !f.startsWith('.sillyspec/'))
648
+ if (posix.length === 0) return null
649
+ const docsDir = join(specBase, 'docs')
650
+ if (!existsSync(docsDir)) return null
651
+ const cards = []
652
+ for (const d of readdirSync(docsDir, { withFileTypes: true })) {
653
+ if (!d.isDirectory()) continue
654
+ const mapPath = join(docsDir, d.name, 'modules', '_module-map.yaml')
655
+ if (!existsSync(mapPath)) continue
656
+ let mods
657
+ try { mods = parseModulePathsSubset(readFileSync(mapPath, 'utf8')) } catch { continue }
658
+ for (const [id, m] of Object.entries(mods || {})) {
659
+ const prefixes = [...(m && m.paths ? m.paths : []), ...(m && m.core_files ? m.core_files : [])]
660
+ .map((p) => String(p).replace(/\\/g, '/').replace(/\/+$/, '')).filter(Boolean)
661
+ const hit = prefixes.some((p) => posix.some((f) => f === p || f.startsWith(p + '/')))
662
+ if (hit && m && typeof m.doc === 'string' && m.doc.trim() !== '') {
663
+ // doc 值相对 docs/<项目>/(如 modules/core-engine.md),不再叠 modules/ 段
664
+ const card = `.sillyspec/docs/${d.name}/${m.doc.replace(/^\.\//, '').replace(/\\/g, '/')}`
665
+ if (!cards.some((c) => c.card === card)) cards.push({ id, card })
666
+ }
620
667
  }
621
- if (check.action !== 'self') pm.setChangeOwner(cwd, archiveChangeName, session)
622
668
  }
669
+ if (cards.length === 0) return null
670
+ return `⚠️ 模块文档认领 advisory:交付面命中 ${cards.map((c) => c.id).join('、')} 模块但本次变更无 .sillyspec/docs/** 增量——未认领模块卡:${cards.map((c) => c.card).join('、')}(advisory 不阻断归档;确认无需文档增量可忽略,需落盘则先补模块卡再归档)`
671
+ }
672
+
673
+ // ── 归档时间线烤制(2026-09-28-archive-timeline-bake)──
674
+ // 事件流(.runtime/watcher-events-<change>.jsonl)机本位:gitignore + 平台同步排除区,归档后
675
+ // 本机清理/跨机 clone 均无从合成时间线。本函数在归档链 rename 后调用:用与 `watcher timeline`
676
+ // CLI 同一合成面(readWatcherEvents × parseTaskLines/readBirthTs/flow-state tier ×
677
+ // resolveCommitAnchors(gitLook))把渲染文本写 destDir/timeline.md、原始事件字节副本写
678
+ // destDir/watcher-events.jsonl(尺寸帽防巨型事件流污染 git,超帽只烤渲染面)。整体 fail-open:
679
+ // 异常返回 {ok:false} 由调用方一行警告,不阻断归档。runtimeRoot 解析与 spawnWatcher 写侧同链
680
+ // (resolvePlatformOpts > resolveRuntimeRoot)——平台模式下事件流在平台 runtime 目录也能读对。
681
+ // gitLook/写读函数/尺寸帽/nowIso 为测试注入面(缺省 gitQuiet 包装与 index.js timeline 接线同形)。
682
+ export const BAKE_EVENTS_COPY_MAX_BYTES = 2 * 1024 * 1024
683
+
684
+ export async function bakeArchiveTimeline({ cwd, specBase, changeName, destDir, gitLook = null, writeImpl = writeFileSync, readRawImpl = readFileSync, eventsCopyMaxBytes = BAKE_EVENTS_COPY_MAX_BYTES, nowIso = null }) {
685
+ try {
686
+ const { resolvePlatformOpts } = await import('../progress.js')
687
+ const runtimeRoot = resolveRuntimeRoot(resolvePlatformOpts(cwd), specBase)
688
+ const { readWatcherEvents, watcherEventsPath } = await import('../watcher.js')
689
+ const res = readWatcherEvents({ runtimeRoot, change: changeName })
690
+ if (!res.exists) return { ok: true, skipped: true, reason: `无事件流(${watcherEventsPath(runtimeRoot, changeName)})` }
691
+ const { parseTaskLines, renderBakedTimeline, resolveCommitAnchors, readBirthTs } = await import('../timeline.js')
692
+ let tasks = null
693
+ try { tasks = parseTaskLines(readRawImpl(join(destDir, 'tasks.md'), 'utf8')) } catch { /* 任务面缺失照烤(渲染层有降级段) */ }
694
+ let tier = null
695
+ try { const m = /^tier:\s*(\S+)/m.exec(readRawImpl(join(destDir, 'flow-state.yaml'), 'utf8')); if (m) tier = m[1] } catch { /* 头脑风暴预段等无 flow-state 按未知 */ }
696
+ const look = gitLook || ((hash) => {
697
+ const out = gitQuiet(cwd, ['log', '-1', '--format=%h%x1f%aI%x1f%B', hash])
698
+ if (typeof out !== 'string' || !out.trim()) return null
699
+ const sep = out.indexOf('\x1f')
700
+ const rest = out.slice(sep + 1)
701
+ const sep2 = rest.indexOf('\x1f')
702
+ return { hash: out.slice(0, sep), dateISO: rest.slice(0, sep2), message: rest.slice(sep2 + 1) }
703
+ })
704
+ const anchors = resolveCommitAnchors(res.events, look)
705
+ let eventsRaw = null
706
+ try { eventsRaw = readRawImpl(watcherEventsPath(runtimeRoot, changeName), 'utf8') } catch { /* 副本缺源只烤渲染面 */ }
707
+ const oversize = eventsRaw != null && Buffer.byteLength(eventsRaw, 'utf8') > eventsCopyMaxBytes
708
+ // 评审 P3-1 清偿:副本未落盘(读源失败或超帽)时头注记必须显式「未随包」,不虚报在场
709
+ const timelineMd = renderBakedTimeline({ change: changeName, events: res.events, tasks, anchors, birthTs: readBirthTs(destDir), tier, bakedAtIso: nowIso || new Date().toISOString(), eventsCopySkipped: eventsRaw == null || oversize })
710
+ writeImpl(join(destDir, 'timeline.md'), timelineMd)
711
+ const files = ['timeline.md']
712
+ if (eventsRaw != null && !oversize) {
713
+ writeImpl(join(destDir, 'watcher-events.jsonl'), eventsRaw)
714
+ files.push('watcher-events.jsonl')
715
+ }
716
+ return { ok: true, skipped: false, files, eventsCopySkipped: oversize }
717
+ } catch (e) {
718
+ return { ok: false, skipped: false, error: e && e.message ? e.message : String(e) }
719
+ }
720
+ }
721
+
722
+ /**
723
+ * runArchiveChain —— 归档执行链(R7 切片二 task-02 从 archiveChangeDirectory 抽出,纯搬运)。
724
+ *
725
+ * 链面:未 apply 交付面检查(--skip-apply 留痕旁路)→ plan.md 硬校验(skipPlanCheck 旁路:
726
+ * flow thin 轻量工件面无 plan.md,task-03 flow done 归档子步传 true;既有 archive 流程缺省
727
+ * false 行为零变化)→ module-impact pending 死信校验 → 目标目录检查 → 目录搬移(rename 重试)
728
+ * → 时间线烤制(fail-open,2026-09-28-archive-timeline-bake)→ unregisterChange 终态一致化
729
+ * → archiveNarrowedGitAdd 窄化暂存 → 他者半归档残留探测。
730
+ * 消费方:archiveChangeDirectory(既有 archive 流程)与 flow done 归档子步(R7)。
731
+ *
732
+ * @returns {Promise<void>} 失败面沿链内既有 process.exit 语义(纯搬运不改)。
733
+ */
734
+ export async function runArchiveChain({ pm, cwd, specBase, changeName, srcDir, destDir, skipApply = false, skipPlanCheck = false, deliverableFiles = null, docsIncrement = null } = {}) {
735
+ const archiveDir = dirname(destDir) // 抽取补:原闭包变量(changes/archive),从 destDir 推导等价
736
+ // ── 模块文档认领 advisory(资产三小件③,2026-09-23)──
737
+ // flow done/burst 收口不产生模块文档增量(旧流程挂在 execute/verify 步骤)——交付面 ×
738
+ // module-map 交集 ≥1 且无 .sillyspec/docs/** 增量 → 一行警告列未认领模块卡。advisory 不
739
+ // 阻断;fail-open(异常零输出)。交付面来源:调用方显式清单优先(burst 等未来调用方),
740
+ // 缺省从 srcDir/flow-state.yaml 的 baseline_commit 自算(deriveFlowDeliverableFace);非 flow
741
+ // 变更(无 flow-state.yaml)跳过——legacy archive 已有 module-impact「更新结果」死信硬门。
742
+ try {
743
+ const face = (deliverableFiles !== null && docsIncrement !== null)
744
+ ? { deliverableFiles, docsIncrement }
745
+ : deriveFlowDeliverableFace({ cwd, srcDir })
746
+ const docMsg = face ? buildArchiveModuleDocAdvisory({ specBase, ...face }) : null
747
+ if (docMsg) console.warn(docMsg)
748
+ } catch { /* advisory fail-open:任何异常不碰归档链 */ }
623
749
  // ── 归档收口(2026-09-14-change-ownership-guards task-03 / FR-02 / D-002@v1)──
624
750
  // worktree 有未 apply 交付面 → 归档阻断:归档会注销 change + 清理 worktree(目录/分支/meta),
625
751
  // 交付物失去进主仓通道,形成「归档完成但交付物未进主仓」悬空态(troubleshooting §65 事件①
@@ -630,28 +756,28 @@ export async function archiveChangeDirectory(pm, cwd, progress, specBase, platfo
630
756
  try {
631
757
  const { WorktreeManager } = await import('../worktree.js')
632
758
  const wmGate = new WorktreeManager({ cwd })
633
- const gateMeta = wmGate.getMeta(archiveChangeName)
759
+ const gateMeta = wmGate.getMeta(changeName)
634
760
  if (gateMeta
635
761
  && gateMeta.mode !== 'in-place-fallback'
636
762
  && gateMeta.mode !== 'native-worktree'
637
763
  && gateMeta.worktreePath
638
764
  && existsSync(gateMeta.worktreePath)) {
639
765
  const { applyWorktree } = await import('../worktree-apply.js')
640
- const checkApply = applyWorktree(archiveChangeName, { cwd, checkOnly: true })
766
+ const checkApply = applyWorktree(changeName, { cwd, checkOnly: true })
641
767
  const face = (checkApply && Array.isArray(checkApply.changedFiles)) ? checkApply.changedFiles : []
642
768
  if (face.length > 0) {
643
769
  const facePreview = `${face.slice(0, 5).join('、')}${face.length > 5 ? ' 等' : ''}`
644
- if (!gateOpts.skipApply) {
770
+ if (!skipApply) {
645
771
  console.error(`❌ 归档失败:worktree 存在未 apply 的交付面(${face.length} 个文件:${facePreview})`)
646
772
  console.error(` 归档会清理 worktree(目录+分支+meta),交付物将失去进主仓的通道,形成「归档完成但交付物未进主仓」悬空态。`)
647
- console.error(` 先 apply 再归档: sillyspec worktree apply ${archiveChangeName}`)
773
+ console.error(` 先 apply 再归档: sillyspec worktree apply ${changeName}`)
648
774
  console.error(` 确认无需 apply(交付面已由其他会话落地/纯探索性变更等): sillyspec run archive --done --confirm --skip-apply(显式跳过并留痕归档记录)`)
649
775
  process.exit(1)
650
776
  }
651
777
  console.warn(`⚠️ 已按 --skip-apply 跳过未 apply 交付面检查(${face.length} 个文件未确认落地主仓:${facePreview})——留痕归档记录`)
652
778
  writeFileSync(join(srcDir, 'skip-apply.record.json'), JSON.stringify({
653
779
  schemaVersion: 1,
654
- change: archiveChangeName,
780
+ change: changeName,
655
781
  flag: '--skip-apply',
656
782
  skippedAt: new Date().toISOString(),
657
783
  deliverableCount: face.length,
@@ -667,7 +793,9 @@ export async function archiveChangeDirectory(pm, cwd, progress, specBase, platfo
667
793
  }
668
794
  // 移动前硬校验:变更包必须含 plan.md,否则不该归档。
669
795
  // 在移动前阻断(而非移动后),目录尚未动,用户可直接修复后重试。
670
- if (!existsSync(join(srcDir, 'plan.md'))) {
796
+ // skipPlanCheck(R7 切片二 / task-03 flow 轻量工件面):flow-state 在场的 thin change 无
797
+ // plan.md(轻量工件面四件套),flow done 归档子步传 true 旁路;既有 archive 流程缺省 false 零变化。
798
+ if (!skipPlanCheck && !existsSync(join(srcDir, 'plan.md'))) {
671
799
  console.error(`❌ 归档失败:变更目录缺少 plan.md(${srcDir})`)
672
800
  console.error(` plan.md 是归档的必需产物。请先补全 plan 阶段产出再归档。`)
673
801
  process.exit(1)
@@ -705,9 +833,22 @@ export async function archiveChangeDirectory(pm, cwd, progress, specBase, platfo
705
833
  process.exit(1)
706
834
  }
707
835
 
836
+ // ── 时间线烤制(2026-09-28-archive-timeline-bake)──事件流机本位(.runtime gitignore),
837
+ // 归档时把合成时间线 + 原始事件副本烤进归档目录(跨机进 git);fail-open:跳过/失败一行
838
+ // 注记不阻断归档。位于 rename 之后(目录归本链独占、destName 文件面完整)、窄化 git add
839
+ // 之前(烤制文件随 archiveNarrowedGitAdd 确定性进暂存)。
840
+ try {
841
+ const baked = await bakeArchiveTimeline({ cwd, specBase, changeName, destDir })
842
+ if (baked.skipped) console.log(`⏭️ 时间线烤制跳过:${baked.reason}`)
843
+ else if (baked.ok) console.log(`📦 时间线烤制完成:${baked.files.join('、')}(随归档包进 git)`)
844
+ else console.warn(`⚠️ 时间线烤制失败(不阻断归档): ${baked.error}`)
845
+ } catch (e) {
846
+ console.warn(`⚠️ 时间线烤制异常(不阻断归档): ${e && e.message ? e.message : e}`)
847
+ }
848
+
708
849
  // 正常路径同样走终态一致化(坑 manual-archive-desync-status-only):标准 --done --confirm 流程
709
850
  // 中 completeStep 随后也会逐项完成,此处先收尾是幂等写同值——保证任何走出本函数的归档终态一致
710
- pm.unregisterChange(cwd, archiveChangeName, { archiveStepNames: typeof pm.archiveStepNamesForArchive === 'function' ? pm.archiveStepNamesForArchive() : null })
851
+ pm.unregisterChange(cwd, changeName, { archiveStepNames: typeof pm.archiveStepNamesForArchive === 'function' ? pm.archiveStepNamesForArchive() : null })
711
852
 
712
853
  // CLI 下沉 git add(坑4,FR-04):确定性暂存归档目录 + 模块文档,不靠 step5 prompt 驱动。
713
854
  // step5 prompt 的 git add 保留作幂等兜底;POSIX 路径跨平台(git 接受正斜杠)。
@@ -745,12 +886,12 @@ export async function archiveChangeDirectory(pm, cwd, progress, specBase, platfo
745
886
  // 已暂存 rename → 半归档残留;按目标目录名归属他者变更
746
887
  const m = /^\.sillyspec\/changes\/archive\/([^/]+)\//.exec(dst)
747
888
  const owner = m ? m[1] : '?'
748
- if (owner !== archiveChangeName) othersResidual.push(owner)
889
+ if (owner !== changeName) othersResidual.push(owner)
749
890
  } else if (x === ' ' && (y === 'D' || y === 'A' || y === 'M')) {
750
891
  // 未暂存的源侧移动:源目录 D / 新位置 A——指向本变更的补暂存
751
892
  const p = dst || src
752
- if (p && (p.startsWith(changesPrefix + archiveChangeName + '/')
753
- || p.startsWith(changesPrefix + 'archive/' + archiveChangeName + '/'))) {
893
+ if (p && (p.startsWith(changesPrefix + changeName + '/')
894
+ || p.startsWith(changesPrefix + 'archive/' + changeName + '/'))) {
754
895
  minePaths.push(p)
755
896
  }
756
897
  }
@@ -773,6 +914,60 @@ export async function archiveChangeDirectory(pm, cwd, progress, specBase, platfo
773
914
  }
774
915
  }
775
916
  } catch { /* 探测失败不阻断归档(advisory) */ }
917
+ }
918
+
919
+ export async function archiveChangeDirectory(pm, cwd, progress, specBase, platformOpts = {}, gateOpts = {}) {
920
+ const archiveChangeName = progress.currentChange
921
+ if (!archiveChangeName) {
922
+ console.error('❌ 归档失败:未找到当前变更名(currentChange)')
923
+ process.exit(1)
924
+ }
925
+ const changesDir = join(specBase, 'changes')
926
+ const archiveDir = join(changesDir, 'archive')
927
+ const srcDir = join(changesDir, archiveChangeName)
928
+ const date = new Date().toISOString().slice(0, 10)
929
+ const destName = archiveDestDirName(date, archiveChangeName)
930
+ const destDir = join(archiveDir, destName)
931
+
932
+ if (!existsSync(srcDir)) {
933
+ // 幂等自愈(issue archive-stage-physical-tracking-desync):源目录已不存在,但变更可能已被
934
+ // 手动 / 部分流程移到 changes/archive/ 并 commit,而 --done --confirm 从未正式跑完 → 进度 DB
935
+ // 卡 archive 阶段、active 列表仍列此 change。检测到已归档则回填进度(unregisterChange)并
936
+ // 成功返回,让收尾流程把 archive 阶段标完成,而非 exit(1) 死路(source 已移走无法重跑 move)。
937
+ const alreadyArchivedDir = findAlreadyArchivedDir(archiveDir, archiveChangeName)
938
+ if (alreadyArchivedDir) {
939
+ console.log(`ℹ️ 源目录不存在但变更已在 archive/(${basename(alreadyArchivedDir)}),判定已归档,自愈进度 DB`)
940
+ // 终态一致化(坑 manual-archive-desync-status-only):手动搬目录绕过标准归档后,本自愈路径
941
+ // 不能只翻 status——同时收尾 archive 阶段(steps/stages/current_stage),否则推送平台的终态
942
+ // 是「已归档 + 归档 0/5 + 停在 execute」的矛盾体,详情页渲染成「进度丢失」
943
+ pm.unregisterChange(cwd, archiveChangeName, { archiveStepNames: typeof pm.archiveStepNamesForArchive === 'function' ? pm.archiveStepNamesForArchive() : null })
944
+ await archiveWorktreeCleanup(cwd, archiveChangeName, specBase, platformOpts)
945
+ console.log(`📦 已自愈归档:${archiveChangeName} → archive/${basename(alreadyArchivedDir)}/`)
946
+ return alreadyArchivedDir
947
+ }
948
+ console.error(`❌ 归档失败:源目录不存在 ${srcDir}`)
949
+ console.error(` 且 changes/archive/ 下未找到该变更的归档目录。若已手动归档请核对目录名;否则先补全变更产物。`)
950
+ process.exit(1)
951
+ }
952
+ // ── 所有权护栏(2026-09-14-change-ownership-guards task-03 / FR-01 / D-001@v1 接线点)──
953
+ // 归档=接管类操作(注销 change + 移目录 + 清 worktree),他人活跃 change 硬拒。判定在
954
+ // withMainRepoLock 锁内(handleArchiveConfirmStep 调用点),判定-执行无 TOCTOU;放行分支
955
+ // no-owner/takeover-stale 重写 owner=本会话(setChangeOwner 同事务刷新 last_active,心跳从
956
+ // 接管时刻起算)。会话标识三级解析:--session > env SILLYSPEC_SESSION_ID > anon@host(降级
957
+ // 时 resolveSessionIdentity 自带教学 warning)。消费 task-02 契约(assertChangeOwnership
958
+ // 纯读不写库),不重复实现判定语义。
959
+ {
960
+ const { session } = resolveSessionIdentity({ flagSession: gateOpts.sessionFlag || null, cwd })
961
+ const check = pm.assertChangeOwnership(cwd, archiveChangeName, { selfSession: session, nowMs: Date.now() })
962
+ if (!check.allowed) {
963
+ console.error(`🚫 归档失败:变更 ${archiveChangeName} 正被其他会话持有(owner: ${check.owner},最后活跃: ${check.lastActive || '未知'}),活跃窗口 ${Math.round(check.heartbeatMs / 60000)} 分钟内拒绝归档`)
964
+ console.error(` 等对方会话收尾后重试(窗口过后自动接管);确认对方已放弃且本会话应接管:先经 sillyspec worktree apply/cleanup ${archiveChangeName} --takeover 显式接管(重写 owner 留痕)再归档`)
965
+ process.exit(1)
966
+ }
967
+ if (check.action !== 'self') pm.setChangeOwner(cwd, archiveChangeName, session)
968
+ }
969
+ // ── 归档执行链(task-02 抽取:链体在 runArchiveChain,行为等价纯搬运)──
970
+ await runArchiveChain({ pm, cwd, specBase, changeName: archiveChangeName, srcDir, destDir, skipApply: Boolean(gateOpts.skipApply), skipPlanCheck: false })
776
971
 
777
972
  // 归档时清理可能残留的 worktree(自愈路径也复用,见上方 srcDir 缺失分支)。
778
973
  await archiveWorktreeCleanup(cwd, archiveChangeName, specBase, platformOpts)
@@ -1436,8 +1631,8 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
1436
1631
 
1437
1632
  // ── quick 资产尾(2026-09-20-quick-asset-tail,D-001/D-002/D-005):门禁过后三件机械
1438
1633
  // 事,agent 零新增命令/零写作义务,fail-open 全链(任何异常 warn 不拦 quick 完成)。
1439
- // ① 薄通道蒸馏尾:linked 真变更(非 quick-<hex>)→ decision-distill + fr-index(幂等)
1440
- // + lite 归档(治「薄通道零资产 + 僵尸 brainstorm 态」——autocompact 对撞实证)。
1634
+ // ① 轻量通道蒸馏尾:linked 真变更(非 quick-<hex>)→ decision-distill + fr-index(幂等)
1635
+ // + lite 归档(治「轻量通道零资产 + 僵尸 brainstorm 态」——autocompact 对撞实证)。
1441
1636
  // 挂点在门禁后(D-002 时序:防未实现设计进索引)。
1442
1637
  // ② 模块 changelog 机械追加:changedFiles×moduleIndex join → 边车存在才 append 一行
1443
1638
  // (纯 quick 的检索面——QUICKLOG 是时间流水账,模块边车是空间索引)。
@@ -1785,6 +1980,27 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
1785
1980
  softFiles,
1786
1981
  })
1787
1982
  console.log(`📝 QUICKLOG 条目 ${qlId} 已标记完成`)
1983
+ // ── ql 绑定行落盘(2026-09-24-fr-test-bindings task-05,fr-test-binding §3.2/D-004@v1):
1984
+ // 窗口测试文件(命名三信号)机械落 candidate(discovery=machine/confirmed_by=null——
1985
+ // gate 实测证明「跑过且过」,不证明绑定关系正确,诚实恒 candidate;确认走 tests --bind)。
1986
+ // fail-open:落行异常不拦收尾。──
1987
+ try {
1988
+ const stripAnno = (f) => String(f).replace(/([^)]*)$/,'').trim()
1989
+ const isTestFile = (p) => /(^|\/)(test|tests|__tests__)(\/|$)/i.test(p) || /^test_/i.test(p.slice(p.lastIndexOf('/')+1)) || /[._](test|spec)\.[^.]+$/i.test(p)
1990
+ const testFiles = [...new Set((annotatedRealFiles || []).map(stripAnno).filter(isTestFile))]
1991
+ if (testFiles.length > 0) {
1992
+ const { upsertQlBindings } = await import('../test-bindings.js')
1993
+ upsertQlBindings({
1994
+ specBase, qlId,
1995
+ rows: testFiles.map((f, i) => ({
1996
+ anchor: qlId, row_id: `${changeName}:win-${i}:${f}`, tests: [f],
1997
+ reason: 'regression', state: 'candidate', discovery: 'machine', confirmed_by: null,
1998
+ source_change: changeName,
1999
+ })),
2000
+ })
2001
+ console.log(`🔗 ql 绑定候选落盘:${testFiles.length} 行(${qlId},candidate——确认走 sillyspec tests --bind)`)
2002
+ }
2003
+ } catch { /* ql 绑定落行 fail-open */ }
1788
2004
  // 范围快照/patch 审计级落盘(quick-359a48f1):quicklog/patches/<qlId>.json+.patch——按
1789
2005
  // ql-ID 对齐 QUICKLOG 条目(平台按条目抓取),json 冗余 sessionId 供 guard 清理后记录态
1790
2006
  // 反查(computeQuickAudit → findQuickPatchRecord 同链)。patch 为 --done 时点全量冻结
@@ -2659,19 +2875,56 @@ export function checkKnowledgeBaselineRatchet(specBase) {
2659
2875
  }
2660
2876
 
2661
2877
  /**
2662
- * 棘轮对比 + console 渲染(quick --done 与 archive 收尾共用):over → ⚠️ 软警告(条数 +
2663
- * 建议 knowledge classify 清单,不阻断不抛错);tightened → 📉 单行留痕;disabled/steady
2664
- * 零输出。自身 try/catch fail-open(R-05:棘轮误伤的兜底就是什么都不做)。
2878
+ * 知识收件箱横幅纯函数(2026-09-23 知识可见性 quick):待审条数 > 0 即渲染(不再基线内静默——
2879
+ * 用户反馈「聊天过程中完全不知道有待审知识」),标题直出聊天面(前 3 条 + 余量指引),超基线
2880
+ * 升级 ⚠️ 前缀。零条目零输出(降噪:清空即静默)。已 export 供 test/knowledge-inbox.test.mjs。
2881
+ */
2882
+ export function buildKnowledgeInboxLines({ count = 0, baseline = null, over = false, titles = [] } = {}) {
2883
+ if (!Number.isFinite(count) || count <= 0) return []
2884
+ const head = over
2885
+ ? `\n⚠️ 知识收件箱:uncategorized 待审 ${count} 条,超 knowledge-baseline 基线 ${baseline}(软警告不阻断)——请转达用户审阅,别只留在文件里`
2886
+ : `\n📚 知识收件箱:uncategorized 待审 ${count} 条——请转达用户审阅,别只留在文件里`
2887
+ const lines = [head]
2888
+ const shown = titles.filter(Boolean).slice(0, 3)
2889
+ shown.forEach((t, i) => lines.push(` ${i + 1}. ${t}`))
2890
+ if (count > shown.length) lines.push(` …(另 ${count - shown.length} 条,sillyspec knowledge inbox 看全文)`)
2891
+ lines.push(` 归类:sillyspec knowledge classify --title "…" --file conventions|patterns|known-issues.md`)
2892
+ return lines
2893
+ }
2894
+
2895
+ /** uncategorized 待审标题清单(前 limit+1 条,供横幅与余量计算)。fail-open:读失败空集。 */
2896
+ function listUncategorizedTitles(specBase, limit = 3) {
2897
+ try {
2898
+ const p = join(specBase, 'knowledge', 'uncategorized.md')
2899
+ if (!existsSync(p)) return []
2900
+ return parseUncategorizedEntries(readFileSync(p, 'utf8').replace(/\r\n/g, '\n'))
2901
+ .slice(0, limit + 1).map((e) => e.title)
2902
+ } catch { return [] }
2903
+ }
2904
+
2905
+ /**
2906
+ * 棘轮对比 + console 渲染(quick --done 与 archive 收尾共用):over → 收件箱横幅 ⚠️ 升级形态;
2907
+ * 基线内但有存量 → 📚 横幅(可见性修复:不再静默);tightened → 📉 单行留痕;零条目零输出。
2908
+ * 自身 try/catch fail-open(R-05:棘轮误伤的兜底就是什么都不做)。
2665
2909
  */
2666
2910
  function renderKnowledgeBaselineRatchet(specBase) {
2667
2911
  try {
2668
2912
  const r = checkKnowledgeBaselineRatchet(specBase)
2669
2913
  if (r && r.status === 'over') {
2670
- console.warn(`\n⚠️ knowledge/uncategorized.md 待归类 ${r.count} 条,超 knowledge-baseline 基线 ${r.baseline}(软警告不阻断)`)
2671
- console.warn(` 建议跑 sillyspec knowledge classify 逐条归类(待归类清单见 knowledge/uncategorized.md),条数降后基线自动收紧。`)
2672
- } else if (r && r.status === 'tightened') {
2914
+ for (const l of buildKnowledgeInboxLines({ count: r.count, baseline: r.baseline, over: true, titles: listUncategorizedTitles(specBase) })) {
2915
+ console.warn(l)
2916
+ }
2917
+ return
2918
+ }
2919
+ if (r && r.status === 'tightened') {
2673
2920
  console.log(`📉 knowledge-baseline 已自动收紧:${r.baseline} → ${r.count}(uncategorized 待归类条数下降)`)
2674
2921
  }
2922
+ // 可见性修复(2026-09-23 用户反馈):基线内存量不再静默——有待审即出横幅,清空才静默
2923
+ if (r && Number.isFinite(r.count) && r.count > 0) {
2924
+ for (const l of buildKnowledgeInboxLines({ count: r.count, baseline: r.baseline, over: false, titles: listUncategorizedTitles(specBase) })) {
2925
+ console.log(l)
2926
+ }
2927
+ }
2675
2928
  } catch { /* 棘轮渲染失败零副作用(fail-open,R-05) */ }
2676
2929
  }
2677
2930
 
@@ -2870,7 +3123,7 @@ async function runArchiveCeremonyDualRunExit({ cwd, specBase, changeName, platfo
2870
3123
  // ── quick 资产尾(2026-09-20-quick-asset-tail,D-001/D-002/D-004)─────────────────
2871
3124
 
2872
3125
  /**
2873
- * lite 归档(D-004):薄通道(brainstorm→linked quick)变更的轻量收口——所有权 assert →
3126
+ * lite 归档(D-004):轻量通道(brainstorm→linked quick)变更的轻量收口——所有权 assert →
2874
3127
  * 命名 → rename → unregisterChange。不复用 archiveChangeDirectory 的 9 处重门(未 apply
2875
3128
  * 交付面等 worktree 导向,linked quick 变更无 worktree 语义会被误拦);重件
2876
3129
  * (module-impact/ROADMAP/delta)明确豁免。自愈:源目录缺失但已在 archive/ 时补
@@ -2910,19 +3163,19 @@ export async function liteArchiveChange({ pm, cwd, specBase, changeName, platfor
2910
3163
  mkdirSync(archiveDir, { recursive: true })
2911
3164
  renameSync(srcDir, destDir) // 先 rename 后 unregister:失败时 change 留 active 可重试(D-004 顺序保证)
2912
3165
  pm.unregisterChange(cwd, changeName, { archiveStepNames: typeof pm.archiveStepNamesForArchive === 'function' ? pm.archiveStepNamesForArchive() : null })
2913
- console.log(`📦 lite 归档:${changeName} → archive/${destName}/(薄通道轻收口:蒸馏已先行,module-impact/ROADMAP 豁免)`)
3166
+ console.log(`📦 lite 归档:${changeName} → archive/${destName}/(轻量通道轻收口:蒸馏已先行,module-impact/ROADMAP 豁免)`)
2914
3167
  return { archivedTo: destDir }
2915
3168
  }
2916
3169
 
2917
3170
  /**
2918
- * 薄通道蒸馏尾(D-001/D-002):quick --done 门禁过后对 linked 真变更(非 quick-<hex>)跑
3171
+ * 轻量通道蒸馏尾(D-001/D-002):quick --done 门禁过后对 linked 真变更(非 quick-<hex>)跑
2919
3172
  * decision-distill + fr-index(均幂等)→ lite 归档。挂点在质量闸后(D-002:防未实现设计进
2920
3173
  * 索引——quick 中途废弃永不 --done 永不蒸馏);两文件均缺失零打扰跳过;fail-open 全链
2921
3174
  * (任何异常 warn 留痕不拦 quick 完成)。
2922
3175
  * @returns {Promise<{distilled: boolean, frCount: number, decisionCount: number, archived: boolean, warnings: string[]}>}
2923
3176
  */
2924
3177
  export async function distillLinkedChangeAssets({ pm, cwd, specBase, changeName, linkedChanges = [], platformOpts = {} }) {
2925
- const out = { distilled: false, frCount: 0, decisionCount: 0, archived: false, warnings: [] }
3178
+ const out = { distilled: false, frCount: 0, decisionCount: 0, archived: false, warnings: [], frFiles: [] }
2926
3179
  const realChanges = (linkedChanges || []).filter((c) => c && !QUICK_SID_RE.test(c))
2927
3180
  if (realChanges.length === 0) return out // 纯 quick:无关联真变更,零打扰
2928
3181
  try {
@@ -2946,14 +3199,15 @@ export async function distillLinkedChangeAssets({ pm, cwd, specBase, changeName,
2946
3199
  // FR 索引(requirements.md → knowledge/fr/,幂等:同变更名 no-op)
2947
3200
  if (hasReqs) {
2948
3201
  try {
2949
- const { indexRequirements } = await import('../fr-index.js')
2950
- const r = indexRequirements({ changeDir, knowledgeRoot, headHash: '' })
2951
- if (r && Array.isArray(r.written) && r.written.length > 0) { out.frCount += r.written.length; anyDistilled = true }
3202
+ const { indexRequirements, archiveDeliverableFiles } = await import('../fr-index.js')
3203
+ // greenfield-bootstrap:供 deliverableFiles(同 archive-distill 口径——防域路由退化 unmapped)
3204
+ const r = indexRequirements({ changeDir, knowledgeRoot, headHash: '', deliverableFiles: archiveDeliverableFiles(changeDir) })
3205
+ if (r && Array.isArray(r.written) && r.written.length > 0) { out.frCount += r.written.length; r.written.forEach((w) => { if (w && w.file && !out.frFiles.includes(w.file)) out.frFiles.push(w.file) }); anyDistilled = true }
2952
3206
  } catch (e) { out.warnings.push(`fr-index ${linked} 异常跳过:${e && e.message ? e.message : e}`) }
2953
3207
  }
2954
3208
  }
2955
3209
  out.distilled = anyDistilled
2956
- // lite 归档(蒸馏后;仅四件套容器变更——requirements.md 在场=brainstorm 承接的薄通道形态;
3210
+ // lite 归档(蒸馏后;仅四件套容器变更——requirements.md 在场=brainstorm 承接的轻量通道形态;
2957
3211
  // 纯信号自动关联的 proposal-only 变更不归档〔quick-single-change-auto-link §3 契约钉死〕)
2958
3212
  for (const linked of realChanges) {
2959
3213
  if (!existsSync(join(specBase, 'changes', linked, 'requirements.md'))) continue
@@ -2964,7 +3218,8 @@ export async function distillLinkedChangeAssets({ pm, cwd, specBase, changeName,
2964
3218
  } catch (e) { out.warnings.push(`lite 归档 ${linked} 异常跳过:${e && e.message ? e.message : e}`) }
2965
3219
  }
2966
3220
  if (anyDistilled || out.archived) {
2967
- console.log(`📚 quick 资产尾:${out.frCount} 条 FR 入索引、${out.decisionCount} 条决策入 knowledge${out.archived ? '、linked 变更 lite 归档' : ''}(门禁后自动,agent 零新增命令)`)
3221
+ const frDetail = out.frFiles.length > 0 ? `(${out.frFiles.join('、')})` : ''
3222
+ console.log(`📚 quick 资产尾:${out.frCount} 条 FR 入索引${frDetail}、${out.decisionCount} 条决策入 knowledge${out.archived ? '、linked 变更 lite 归档' : ''}(门禁后自动,agent 零新增命令)`)
2968
3223
  }
2969
3224
  } catch (e) {
2970
3225
  out.warnings.push(`蒸馏尾异常(fail-open 不拦 quick 完成):${e && e.message ? e.message : e}`)