sillyspec 3.31.0 → 3.32.1

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 (74) 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 +10 -6
  5. package/.claude/skills/sillyspec-quick/SKILL.md +105 -7
  6. package/.zcodeignore +66 -0
  7. package/AGENTS.md +16 -16
  8. package/assets/command-cards/flow.md +9 -3
  9. package/assets/command-cards/run-quick.md +4 -1
  10. package/package.json +2 -2
  11. package/src/agent-session-log.js +27 -5
  12. package/src/config-schema.js +21 -0
  13. package/src/datetime.js +51 -0
  14. package/src/db.js +10 -3
  15. package/src/decision-distill.js +21 -12
  16. package/src/design-facts.js +46 -6
  17. package/src/flow-draft.js +468 -95
  18. package/src/flow-parity.js +199 -5
  19. package/src/flow-review.js +136 -6
  20. package/src/flow.js +786 -145
  21. package/src/foreign-declared.js +75 -3
  22. package/src/fr-index.js +148 -162
  23. package/src/git-helper.js +16 -3
  24. package/src/hooks/worktree-guard.js +16 -5
  25. package/src/hunk-attribution.js +28 -5
  26. package/src/index.js +186 -19
  27. package/src/knowledge-classify.js +3 -1
  28. package/src/knowledge-digest.js +55 -26
  29. package/src/knowledge-match.js +135 -7
  30. package/src/knowledge-stats.js +105 -4
  31. package/src/knowledge-vector.js +160 -0
  32. package/src/machine-interface.js +90 -2
  33. package/src/module-impact.js +22 -1
  34. package/src/modules.js +9 -0
  35. package/src/progress/change-registry.js +20 -0
  36. package/src/progress/stage-machine.js +4 -7
  37. package/src/progress.js +8 -1
  38. package/src/quicklog.js +20 -0
  39. package/src/redomain.js +23 -7
  40. package/src/route-hindsight.js +276 -0
  41. package/src/run/archive-distill.js +3 -1
  42. package/src/run/command.js +65 -12
  43. package/src/run/complete-handlers.js +219 -5
  44. package/src/run/complete.js +99 -22
  45. package/src/run/gate-snapshot.js +15 -0
  46. package/src/run/gates.js +60 -4
  47. package/src/run/prompt.js +159 -31
  48. package/src/run/quick-audit.js +66 -7
  49. package/src/run/shared.js +6 -11
  50. package/src/run/stage.js +5 -1
  51. package/src/run/verify-quality-scan.js +84 -0
  52. package/src/scan-facts.js +2 -1
  53. package/src/scope-audit.js +138 -5
  54. package/src/sentinel-assertions.js +67 -16
  55. package/src/stage-contract-engine.js +4 -1
  56. package/src/stage-contract-spec.js +103 -4
  57. package/src/stage-contract.js +55 -7
  58. package/src/stages/brainstorm.js +27 -14
  59. package/src/stages/knowledge.js +4 -3
  60. package/src/stages/plan-postcheck.js +98 -3
  61. package/src/stages/plan.js +22 -2
  62. package/src/stages/quick.js +1 -1
  63. package/src/stages/verify.js +2 -1
  64. package/src/task-review.js +1 -1
  65. package/src/task-tick.js +121 -0
  66. package/src/taskcard-frontmatter.js +31 -0
  67. package/src/ui-visual.js +43 -8
  68. package/src/verify-postcheck.js +518 -38
  69. package/src/verify-probes.js +216 -5
  70. package/src/watcher.js +1067 -1031
  71. package/src/worktree-apply.js +9 -1
  72. package/src/worktree.js +21 -6
  73. package/templates/agents-instruction.md +15 -15
  74. package/templates/prompts/taskcard-rules.md +1 -0
package/src/run/gates.js CHANGED
@@ -34,6 +34,7 @@ import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, unlinkSync,
34
34
  import { writeAtomicSync } from '../fs-atomic.js'
35
35
  import { triggerSync, resolveChangeDir, resolveRuntimeRoot } from './shared.js'
36
36
  import { runValidators } from '../stage-contract.js'
37
+ import { computeGateFingerprint, lookupGreenCache, storeGreenCache, greenCacheNotice } from './green-cache.js'
37
38
  import { handleScanStageCompleted, handleExecuteWorktreeCleanup } from './complete-handlers.js'
38
39
 
39
40
  /**
@@ -956,6 +957,19 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
956
957
  if (consult.reuse) ledgerReuse = consult
957
958
  } catch { /* 账本咨询异常 → 真跑 */ }
958
959
  }
960
+ // ── R8 green-cache 第二层复用(2026-09-30-verify-done-green-reuse):--done 收敛循环防重复
961
+ // 真跑。同指纹(HEAD+代码脏面+local.yaml,文档面剔除——green-cache.js 单点口径)30min 内
962
+ // 真跑绿 → 免重跑。实证:multi-agent-platform 2026-09-30 tool-report-activation 收敛循环
963
+ // 码态恒定(12:59-14:15 零 commit)下 test 实测 10 轮 ×~290s 全真跑——P2 账本 miss 即裸奔,
964
+ // 本层兜住。指纹取主仓 cwd/specBase(非快照根:快照目录每轮不同,入指纹永不命中;快照
965
+ // 内容 = HEAD+变更文件 overlay,主仓指纹等价描述)。优先级:P2 账本(三键精确)>
966
+ // P0-1 质量扫描复用 > 本层 > 真跑;fail-open:指纹/缓存任何异常一律回退真跑。──
967
+ let greenHit = null
968
+ let gateFingerprint = null
969
+ try {
970
+ gateFingerprint = computeGateFingerprint({ cwd, specBase })
971
+ if (gateFingerprint) greenHit = lookupGreenCache({ runtimeRoot: resolveRuntimeRoot(platformOpts, specBase), scope: changeName, kind: 'test', fingerprint: gateFingerprint })
972
+ } catch { /* fail-open:缓存链异常回退真跑 */ }
959
973
  if (ledgerReuse) {
960
974
  testCheck = {
961
975
  status: 'passed',
@@ -970,6 +984,16 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
970
984
  testCheck = reusableScan.testResult
971
985
  console.log(`\n♻️ Verify 测试对账:复用 noAI 质量扫描步的实测结果(代码指纹匹配,免重跑;实测于 ${reusableScan.ranAt || '本变更 verify 期间'}${testCheck.resultPath ? `,台账 ${testCheck.resultPath}` : ''})`)
972
986
  printVerifyTestCheck(testCheck)
987
+ } else if (greenHit) {
988
+ testCheck = {
989
+ status: 'passed',
990
+ reason: greenCacheNotice(greenHit, 'verify-test'),
991
+ command: '(green-cache 同指纹复用)',
992
+ exitCode: 0, durationMs: 0, outputTail: null,
993
+ resultPath: null, strategy: null, cached: true,
994
+ }
995
+ console.log(`\n♻️ Verify 测试对账:green-cache 同指纹复用(HEAD+代码脏面+local.yaml 全等,近期真跑绿——P2 账本未命中时的第二层免重跑;SILLYSPEC_GREEN_CACHE_OFF=1 可关闭)`)
996
+ printVerifyTestCheck(testCheck)
973
997
  } else {
974
998
  // 测试实测是同步 execSync,长套件可跑 2~10min 且中途无输出——先预告避免 agent 误判卡死
975
999
  console.log(`\n⏳ Verify 测试对账:CLI 亲自执行 local.yaml 的 commands.test(同步,耗时可能较长,请等待…)`)
@@ -1039,6 +1063,12 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1039
1063
  }
1040
1064
  } catch { /* 记账异常不影响门禁(下次仍真跑) */ }
1041
1065
  }
1066
+ // green-cache 记绿(R8 接线,与 P2 同口径:只有通过结果入缓存——失败永不缓存)
1067
+ if (testCheck && testCheck.status === 'passed' && gateFingerprint) {
1068
+ try {
1069
+ storeGreenCache({ runtimeRoot: resolveRuntimeRoot(platformOpts, specBase), scope: changeName, kind: 'test', fingerprint: gateFingerprint, result: { status: 'passed', exitCode: 0 } })
1070
+ } catch { /* 写缓存失败不影响门禁(下次真跑) */ }
1071
+ }
1042
1072
  }
1043
1073
  // ── trace 晋升(2026-09-24-fr-test-bindings task-04,fr-test-binding §3.2/D-003@v1):
1044
1074
  // verify 门通过点按矩阵判定列晋升 candidate→active(covered/covered-service→active+
@@ -1112,8 +1142,16 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1112
1142
  const { runVerifyLintCheck, printVerifyLintCheck } = await import('../verify-postcheck.js')
1113
1143
  // P0-1 指纹复用:noAI 质量扫描记录同指纹时 lint 一并复用(lint tally 只记真实执行次数)
1114
1144
  let lintCheck = (reusableScan && reusableScan.lintResult) || null
1145
+ let lintGreenHit = null
1146
+ try {
1147
+ lintGreenHit = gateFingerprint ? lookupGreenCache({ runtimeRoot: resolveRuntimeRoot(platformOpts, specBase), scope: changeName, kind: 'lint', fingerprint: gateFingerprint }) : null
1148
+ } catch { /* fail-open:缓存链异常回退真跑 */ }
1115
1149
  if (lintCheck) {
1116
1150
  console.log(`\n♻️ Verify lint 对账:复用 noAI 质量扫描步的实测结果(代码指纹匹配,免重跑)`)
1151
+ } else if (lintGreenHit) {
1152
+ // R8 green-cache 第二层(同 test 段口径):近期同指纹真跑绿免重跑;tally 只记真实执行
1153
+ lintCheck = { status: 'passed', exitCode: 0, durationMs: 0, failureFiles: [], cached: true, reason: greenCacheNotice(lintGreenHit, 'verify-lint') }
1154
+ console.log(`\n♻️ Verify lint 对账:green-cache 同指纹复用(HEAD+代码脏面+local.yaml 全等,近期真跑绿免重跑;SILLYSPEC_GREEN_CACHE_OFF=1 可关闭)`)
1117
1155
  } else {
1118
1156
  lintCheck = runVerifyLintCheck({ cwd: gateCwd, specBase: gateSpecBase, timeoutMs: verifyGateSnap ? 5 * 60 * 1000 : undefined })
1119
1157
  // 快照 lint 超时回退主仓(2026-09-12 dogfood 两连实证:junction I/O 病态慢,3min/5min 均被
@@ -1123,6 +1161,12 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1123
1161
  console.warn('⚠️ 快照 lint 超时(node_modules junction I/O 慢)→ 主仓复跑 lint(并行噪声可能混入——失败先做归属鉴定)')
1124
1162
  lintCheck = runVerifyLintCheck({ cwd, specBase })
1125
1163
  }
1164
+ // green-cache 记绿(R8 接线:passed 才缓存——失败永不缓存;快照超时回退的主仓复跑同记)
1165
+ if (lintCheck.status === 'passed' && gateFingerprint) {
1166
+ try {
1167
+ storeGreenCache({ runtimeRoot: resolveRuntimeRoot(platformOpts, specBase), scope: changeName, kind: 'lint', fingerprint: gateFingerprint, result: { status: 'passed', exitCode: 0 } })
1168
+ } catch { /* 写缓存失败不影响门禁(下次真跑) */ }
1169
+ }
1126
1170
  if (lintCheck.status !== 'skipped') {
1127
1171
  console.log(`\n⏳ Verify lint 对账:CLI 亲自执行 local.yaml 的 commands.lint…`)
1128
1172
  }
@@ -1527,9 +1571,20 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1527
1571
  console.warn(`\n⚠️ module-impact.md「更新结果」表有 ${execPendingRows.length} 个 pending/待办项(文档同步债)——verify 阶段会硬拦:`)
1528
1572
  for (const row of execPendingRows) console.warn(` - ${row}`)
1529
1573
  console.warn(' 建议 execute 收尾就完成模块文档同步并回填 done/skipped(说明原因),别把债拖到 verify 末尾。')
1574
+ console.warn(' 批量跳过不同步行可一键回填:sillyspec module-impact --change <变更名> --fill-skipped --reason "<真实原因>"')
1530
1575
  }
1531
1576
  }
1532
1577
  } catch { /* 提示失败不阻断 execute */ }
1578
+ // UI 视觉证据前置 advisory(2026-10-07-flow-friction-batch3 task-05):证据应执行期随手落盘、
1579
+ // verify 收口才执法(postmortem 实证发现时机太晚)。warn 级不阻断 execute;fail-soft。
1580
+ try {
1581
+ const uiChangeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
1582
+ if (uiChangeDir) {
1583
+ const { uiEvidenceExecuteAdvisory } = await import('../ui-visual.js')
1584
+ const uiAdv = uiEvidenceExecuteAdvisory({ changeDir: uiChangeDir, specBase })
1585
+ if (uiAdv) console.warn(`\n⚠️ ${uiAdv}`)
1586
+ }
1587
+ } catch { /* advisory 失败不阻断 execute */ }
1533
1588
  }
1534
1589
 
1535
1590
  // ── Execute Task Review Gate 已退役(2026-09-26-task-review-retire)──
@@ -1697,10 +1752,11 @@ export function validateFileLocations(cwd, stageName, progress, changeName, spec
1697
1752
  if (!existsSync(changeDir)) return
1698
1753
 
1699
1754
  // 每个阶段完成后预期存在的文件
1700
- // brainstorm:scale=small(小变更)只必产 design.md;large/未标 scale → 四件套全。
1701
- // 与 validateBrainstormOutputs 的 BRAINSTORM_RULES condition(scale≠small)同源,避免对合法 small 变更
1702
- // 误报"⬜ proposal/requirements/tasks 未找到"(本检查仅 advisory 打印不 gate,但误导输出仍要消除)。
1703
- const brainstormExpected = readDesignScale(specBase, effectiveChange) === 'small'
1755
+ // brainstorm:scale=small/未标(收编轻量道,2026-09-29-brainstorm-exit-thin-default 起未标默认 small)只必产
1756
+ // design.md;显式 large → 四件套全。避免对合法收编变更误报"⬜ proposal/requirements/tasks 未找到"
1757
+ // (本检查仅 advisory 打印不 gate,但误导输出仍要消除)。注:stage-contract 硬门条件(ne:small)在并行
1758
+ // 变更在途面,未标仍轻推补值——忘写 scale 的 agent 被门推向显式落值,v1 可接受。
1759
+ const brainstormExpected = readDesignScale(specBase, effectiveChange) !== 'large'
1704
1760
  ? ['design.md']
1705
1761
  : ['design.md', 'proposal.md', 'requirements.md', 'tasks.md']
1706
1762
  const expectedFiles = {
package/src/run/prompt.js CHANGED
@@ -19,12 +19,45 @@
19
19
  * - loadModuleContextIndex/buildModuleContextInjection 内 require('fs'/'path') 改顶部静态 import
20
20
  */
21
21
  import { basename, dirname, join } from 'node:path'
22
- import { existsSync, readFileSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs'
22
+ import { existsSync, readFileSync, mkdirSync, readdirSync, writeFileSync, rmSync } from 'node:fs'
23
23
  import { createHash } from 'node:crypto'
24
24
  import jsYaml from 'js-yaml'
25
25
  import { writeAtomicSync } from '../fs-atomic.js'
26
26
  import { stageRegistry } from '../stages/index.js'
27
27
  import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, parsePorcelainPath, WAIT_MARKER_RE, QUICK_SID_RE, triggerStepStartSync } from './shared.js'
28
+
29
+ /**
30
+ * step-guide 旧指纹清理(2026-10-06-verify-docs-prefill task-05):guide 文件名 =
31
+ * `${stage}-step${idx}-${指纹8位}.md` 跨变更共享、旧指纹永不清理——同步骤多版本共存会让自行
32
+ * ls/glob 目录的 agent 读到旧版本指引。白名单 = keepAbsPaths(本次新写的 guide)∪ 仍被任一
33
+ * 变更 state 文件(`${stage}-step${idx}-*.json`)引用的 guidePath;白名单外的同前缀文件删除。
34
+ * 纯目录操作 fail-soft:目录缺失/坏 state 均不抛,返回清理计数。
35
+ * @param {{ guideRoot: string, stateRoot: string, stageName: string, stepIndex: number, keepAbsPaths: string[] }} opts
36
+ * @returns {number} 实际删除的文件数
37
+ */
38
+ export function pruneStaleStepGuides({ guideRoot, stateRoot, stageName, stepIndex, keepAbsPaths = [] }) {
39
+ const prefix = `${stageName}-step${stepIndex}-`
40
+ const keep = new Set((keepAbsPaths || []).map(String))
41
+ try {
42
+ for (const sf of readdirSync(stateRoot)) {
43
+ if (!sf.startsWith(prefix)) continue
44
+ try {
45
+ const st = JSON.parse(readFileSync(join(stateRoot, sf), 'utf8'))
46
+ if (st && typeof st.guidePath === 'string') keep.add(st.guidePath)
47
+ } catch { /* 坏 state 不参与白名单(其指向的文件按无引用清理——复入时 existsSync 回退全量重印,fail-soft) */ }
48
+ }
49
+ } catch { /* stateRoot 缺失 → 空白名单 */ }
50
+ let pruned = 0
51
+ try {
52
+ for (const gf of readdirSync(guideRoot)) {
53
+ if (!gf.startsWith(prefix) || !gf.endsWith('.md')) continue
54
+ const abs = join(guideRoot, gf)
55
+ if (keep.has(abs)) continue
56
+ try { rmSync(abs, { force: true }); pruned++ } catch { /* 单文件删除失败继续 */ }
57
+ }
58
+ } catch { /* guideRoot 缺失 → 0 */ }
59
+ return pruned
60
+ }
28
61
  import { materializeStageTemplates } from '../stage-templates.js'
29
62
  import { renderSemanticGuardBlock, readSemanticGuardEnabled } from '../semantic-guard.js'
30
63
 
@@ -33,7 +66,7 @@ import { renderSemanticGuardBlock, readSemanticGuardEnabled } from '../semantic-
33
66
  // (漏登会令指纹误变——退化后果只是多一次全量重印,不产生错误指引;多登同理只多印)。
34
67
  const VOLATILE_PLACEHOLDER_RES = [
35
68
  /<now-datetime>/g, /<now-timestamp>/g, /<now-date>/g, /<now-iso-datetime>/g, /<git-head-short>/g, /<quicklog-id>/g,
36
- /{SCAN_STALENESS}/g, /{SCAN_FACTS}/g, /{QUICK_CONTEXT_DIGEST}/g, /{DECISION_HITS}/g,
69
+ /{SCAN_STALENESS}/g, /{SCAN_FACTS}/g, /{QUICK_CONTEXT_DIGEST}/g, /{DECISION_HITS}/g, /{UI_VISUAL_GUIDANCE}/g,
37
70
  /{KNOWLEDGE_HIT_REPORT}/g, /{DOCS_DEBT}/g, /{MODULE_RESOLVE_TABLE}/g, /{REVIEW_MATERIALS}/g,
38
71
  /{EXECUTE_RUN_ID}/g, /{STAGE_REVIEW_RUN_ID}/g, /{PROGRESS_SNAPSHOT}/g, /{TASK_COMPLETION_REPORT}/g,
39
72
  /{WORKTREE_META}/g, /{WORKTREE_BASELINE_INFO}/g, /{TASKS_CHECKBOX}/g, /{GIT_DIRTY}/g,
@@ -392,6 +425,88 @@ export function buildQuickSemanticGuardInjection({ specBase, cwd, changeName } =
392
425
  export const KNOWLEDGE_INJECT_MAX_FILES = 3
393
426
  export const KNOWLEDGE_INJECT_MAX_LINES = 40
394
427
 
428
+ // {FR_INDEX_DIGEST} 注入截断上限(2026-09-29-rot-retire-inject-cap):brainstorm step8 写作期
429
+ // 现行 FR 清单 top-N + 指针行——与 flow start 注入口径(flow.js slice(0,8))同数。大域/unmapped
430
+ // 整池倾倒实证:digest 源遥测 11/11 次 count=723@unmapped。
431
+ export const FR_INDEX_DIGEST_MAX_ENTRIES = 8
432
+
433
+ /**
434
+ * {FR_INDEX_DIGEST} 段构建(brainstorm step8 写作期现行 FR 清单)——2026-09-29-rot-retire-inject-cap
435
+ * 从注入装配内联块抽出为可单测函数,并修两缺口:① 滤 unmapped(停车场非行为域,原不滤致整池倾倒
436
+ * ——遥测 11/11 次 count=723@unmapped 实证);② top-8 截断 + 指针行(原无上限,与 flow start /
437
+ * module-inject 两个注入口径对齐)。截断先截条目再追加尾部承接指引 blockquote(naive slice 会把
438
+ * 指引一起截掉)。遥测 count 保持全量口径(L3 指标①),另加 rendered/truncated/unmappedFiltered。
439
+ * @param {object} opts
440
+ * @param {string} opts.frSpecBase - spec 根(.sillyspec)
441
+ * @param {string} opts.changeName - 变更名(域路由读其 design.md 交付清单;遥测 change 字段)
442
+ * @returns {Promise<{text: string, telemetry: object}>} text=替换值;telemetry=fr-inject 事件负载(异常时 null)
443
+ */
444
+ export async function buildFrIndexDigestSection({ frSpecBase, changeName }) {
445
+ try {
446
+ const { discoverModuleIndex } = await import('../decision-distill.js')
447
+ const { resolveTouchedDomains, readActiveFrDigest, rankFrDigestForInjection, deliverableFilesFromDesignText } = await import('../fr-index.js')
448
+ const frChangeDir = join(frSpecBase, 'changes', String(changeName || ''))
449
+ const knowledgeRoot = join(frSpecBase, 'knowledge')
450
+ const domainsAll = resolveTouchedDomains(frChangeDir, discoverModuleIndex(knowledgeRoot))
451
+ // unmapped 是未归类停车场非行为域,不进写作 prompt——与 flowKnowledgeDigest / frDupGateFlow 同口径
452
+ const domains = domainsAll.filter((d) => d !== 'unmapped')
453
+ const entries = domains.length > 0 ? readActiveFrDigest(knowledgeRoot, domains) : []
454
+ // 注入排序(2026-10-03-fr-inject-relevance-rank):TierA 覆盖命中(🎯)置前、TierB 日期新→旧
455
+ // ——取代文件序前 N(域增长后注入面恒为最老 N 条,新立规格永不可见)。触碰文件面与域路由
456
+ // 同源(design 交付清单);fail-soft:清单缺席=纯 TierB。
457
+ let tierAIds = new Set()
458
+ let ranked = entries
459
+ if (entries.length > 0) {
460
+ try {
461
+ const touched = deliverableFilesFromDesignText(readFileSync(join(frChangeDir, 'design.md'), 'utf8'))
462
+ if (touched.length > 0) {
463
+ ({ ranked, tierAIds } = rankFrDigestForInjection({ archiveRoot: join(frSpecBase, 'changes', 'archive'), frs: entries, changed: touched }))
464
+ }
465
+ } catch { /* design 缺席/不可读=无触碰面,保持文件序 */ }
466
+ }
467
+ // 骨架门(2026-10-03-fr-skeleton-gate):纯骨架条目不占注入席位(TierA 命中例外);指针行
468
+ // 披露骨架数。查重/rot/绑定面不滤。
469
+ const injectable = ranked.filter((e) => !e.skeleton || tierAIds.has(e.id))
470
+ const skeletonHidden = entries.filter((e) => e.skeleton && !tierAIds.has(e.id)).length
471
+ const truncated = Math.max(0, injectable.length - FR_INDEX_DIGEST_MAX_ENTRIES)
472
+ let text
473
+ if (domains.length === 0) {
474
+ // 零路由命中两态:有交付清单但零命中(unmapped 兜底被滤)→ 指停车场;无清单 → 通用空态。
475
+ // 注意 unmapped 是 resolveTouchedDomains 的零命中兜底,真域命中时不与之并存。
476
+ let hasDeliverableRows = false
477
+ try {
478
+ hasDeliverableRows = deliverableFilesFromDesignText(
479
+ readFileSync(join(frChangeDir, 'design.md'), 'utf8')).length > 0
480
+ } catch { /* design 缺席/不可读=无清单态 */ }
481
+ text = domainsAll.length > 0 && hasDeliverableRows
482
+ ? '(触达域仅剩 unmapped(未归类停车场,不注入)——存量条目需要时自查 knowledge/fr/unmapped.md;本变更交付文件建议登记进模块卡 paths,让后续变更路由到真域)'
483
+ : '(触达域暂无 active FR 索引条目——本变更大概率是这些域的首批需求,照常写作)'
484
+ } else if (entries.length === 0) {
485
+ text = '(触达域暂无 active FR 索引条目——本变更大概率是这些域的首批需求,照常写作)'
486
+ } else {
487
+ const renderedEntries = injectable.slice(0, FR_INDEX_DIGEST_MAX_ENTRIES)
488
+ const hiddenCount = entries.length - renderedEntries.length
489
+ const lines = renderedEntries.map((e) => `- ${e.id}${tierAIds.has(e.id) ? ' 🎯' : ''} ${e.title}(来源 ${e.change}${e.scenarios.length ? ';场景:' + e.scenarios.slice(0, 3).join(',') : ''}${(e.decisions || []).length ? ';依据:' + e.decisions.slice(0, 3).join('、') : ''})`)
490
+ if (hiddenCount > 0) lines.push(`(+${hiddenCount} 条见 knowledge/fr/ 对应域文件——未尽条目按需 Read${skeletonHidden > 0 ? `;纯骨架 ${skeletonHidden} 条不注入` : ''})`)
491
+ lines.push('')
492
+ lines.push('> 域解析自本变更 design.md 文件清单(漏域先核对清单)。改写/取代已有行为 → 对应 FR 块加承接行;新行为 → 新 FR 块。superseded 条目默认不列(历史回溯自行读 knowledge/fr/)。依据决策(L2)= 当年取舍锚——翻案须先读 knowledge/decisions/<域>.md 的否决理由,满足复潮条件走 D-xxx@vN+1,不得静默改行为。承接行可带退役理由:`承接: FR-<域>-NNN(退役理由:一句话)`——归档时写进被取代条目(理由内禁逗号);条目正文是截断摘要,全文锚(全文:<归档路径>#FR-NN)由 CLI 自动落。' + (tierAIds.size > 0 ? '🎯=与本次触碰文件有覆盖交集(TierA 优先注入)。' : ''))
493
+ text = lines.join('\n')
494
+ }
495
+ return {
496
+ text,
497
+ telemetry: {
498
+ type: 'fr-inject', change: changeName, domains, count: entries.length, source: 'digest',
499
+ rendered: Math.min(injectable.length, FR_INDEX_DIGEST_MAX_ENTRIES), truncated,
500
+ unmappedFiltered: domainsAll.length !== domains.length,
501
+ tierA: tierAIds.size,
502
+ skeletonHidden,
503
+ },
504
+ }
505
+ } catch (e) {
506
+ return { text: `(FR 索引注入失败:${e && e.message ? e.message : e}——可自行读 {SPEC_ROOT}/knowledge/fr/)`, telemetry: null }
507
+ }
508
+ }
509
+
395
510
  /** top-N 选取:按 entries 出现序(INDEX 行序)取前 N 个不同 file(matchKnowledge 布尔 filter
396
511
  * 无相关度排序,X-009——出现序即唯一稳定序),每 file 取首个命中 entry 作代表。 */
397
512
  function pickTopKnowledgeEntries(entries, maxFiles) {
@@ -1215,32 +1330,16 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1215
1330
 
1216
1331
  // ①c {FR_INDEX_DIGEST}(2026-09-18-fr-index-l1 L1,D-004):brainstorm step8 的触达域现行 FR 注入
1217
1332
  // ——写作期防重复 FR 的确定性清单(superseded 默认藏);无触达域索引/索引空 → 段消隐。
1218
- // fr-inject 遥测(L3 证据发生器指标①)fail-soft;token 本体在 stages/brainstorm.js step8 模板。
1333
+ // 段构建与收敛口径(滤 unmapped + top-8)在 buildFrIndexDigestSection(可单测);遥测 fail-soft。
1219
1334
  if (stageName === 'brainstorm' && promptText.includes('{FR_INDEX_DIGEST}')) {
1220
- try {
1221
- const frSpecBase = resolvePromptSpecBase(platformOpts, cwd)
1222
- const { discoverModuleIndex } = await import('../decision-distill.js')
1223
- const { resolveTouchedDomains, readActiveFrDigest } = await import('../fr-index.js')
1224
- const frChangeDir = join(frSpecBase, 'changes', String(changeName || ''))
1225
- const knowledgeRoot = join(frSpecBase, 'knowledge')
1226
- const domains = resolveTouchedDomains(frChangeDir, discoverModuleIndex(knowledgeRoot))
1227
- const entries = readActiveFrDigest(knowledgeRoot, domains)
1228
- if (entries.length === 0) {
1229
- substitute(/\{FR_INDEX_DIGEST\}/g, '(触达域暂无 active FR 索引条目——本变更大概率是这些域的首批需求,照常写作)')
1230
- } else {
1231
- const lines = entries.map((e) => `- ${e.id} ${e.title}(来源 ${e.change}${e.scenarios.length ? ';场景:' + e.scenarios.slice(0, 3).join(',') : ''}${(e.decisions || []).length ? ';依据:' + e.decisions.slice(0, 3).join('、') : ''})${e.needsReview ? ` ⚠️ 待复核(${e.needsReview})——该 FR 覆盖的代码近期被 quick 触达,行为可能已变;本变更若触及同域先核对现状再决定承接/新写` : ''}`)
1232
- lines.push('')
1233
- lines.push('> 域解析自本变更 design.md 文件清单(漏域先核对清单)。改写/取代已有行为 → 对应 FR 块加承接行;新行为 → 新 FR 块。superseded 条目默认不列(历史回溯自行读 knowledge/fr/)。依据决策(L2)= 当年取舍锚——翻案须先读 knowledge/decisions/<域>.md 的否决理由,满足复潮条件走 D-xxx@vN+1,不得静默改行为。承接行可带退役理由:`承接: FR-<域>-NNN(退役理由:一句话)`——归档时写进被取代条目(理由内禁逗号);条目正文是截断摘要,全文锚(全文:<归档路径>#FR-NN)由 CLI 自动落。')
1234
- substitute(/\{FR_INDEX_DIGEST\}/g, lines.join('\n'))
1235
- }
1335
+ const frSpecBase = resolvePromptSpecBase(platformOpts, cwd)
1336
+ const sec = await buildFrIndexDigestSection({ frSpecBase, changeName })
1337
+ substitute(/\{FR_INDEX_DIGEST\}/g, sec.text)
1338
+ if (sec.telemetry) {
1236
1339
  try {
1237
1340
  const { appendKnowledgeHit } = await import('../knowledge-hits.js')
1238
- appendKnowledgeHit(join(frSpecBase, '.runtime'), {
1239
- type: 'fr-inject', change: changeName, domains, count: entries.length, source: 'digest',
1240
- })
1341
+ appendKnowledgeHit(join(frSpecBase, '.runtime'), sec.telemetry)
1241
1342
  } catch { /* 遥测 fail-soft */ }
1242
- } catch (e) {
1243
- substitute(/\{FR_INDEX_DIGEST\}/g, `(FR 索引注入失败:${e && e.message ? e.message : e}——可自行读 {SPEC_ROOT}/knowledge/fr/)`)
1244
1343
  }
1245
1344
  }
1246
1345
 
@@ -1290,21 +1389,24 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1290
1389
  // 全降级不抛(异常 → 单行说明)。
1291
1390
  if (stageName === 'brainstorm' && promptText.includes('{DECISION_HITS}')) {
1292
1391
  try {
1293
- const { matchKnowledge } = await import('../knowledge-match.js')
1392
+ const { matchKnowledgeHybrid } = await import('../knowledge-vector.js')
1393
+ const { deathPathNote } = await import('../knowledge-match.js')
1294
1394
  const decSpecBase = resolvePromptSpecBase(platformOpts, cwd)
1295
1395
  const decKnowledgeDir = join(decSpecBase, 'knowledge')
1296
1396
  // taskContext:changeName(brainstorm Step2 时 tasks.md 尚未生成,变更名是唯一稳定任务
1297
1397
  // 语料;与 execute 分支的 changeName 基底同口径)
1298
- const decResult = matchKnowledge(decKnowledgeDir, changeName || '')
1299
- const rejectedHits = (decResult.decisionHits || []).filter(h => h.status === 'rejected')
1398
+ const decResult = await matchKnowledgeHybrid(decKnowledgeDir, changeName || '', { cwd })
1399
+ // 防复潮面 = 死路注记 ∪ 有主题重叠的 rejected(与 flow 注入段/knowledge-gate 同口径;
1400
+ // score 零 rejected 不弹——2026-09-28-knowledge-gate-denoise)
1401
+ const rejectedHits = (decResult.decisionHits || []).filter(h => h.deathPath || (h.status === 'rejected' && h.score > 0))
1300
1402
  let decInjected = ''
1301
1403
  if (rejectedHits.length > 0) {
1302
1404
  const lines = [
1303
- '⚠️ 否决决策提示(历史已否决,防复潮)——下列决策此前已被否决。看到否决理由后,除非复潮条件明确满足,不要在本次方案中重新提出;若认为复潮条件已满足,须在本变更 decisions.md 记录新版本条目(D-xxx@vN+1)说明依据:'
1405
+ '⚠️ 否决决策/死路注记提示(历史已否决或已记死路,防复潮)——下列决策此前已被否决或已记死路。看到理由后,除非复潮条件明确满足,不要在本次方案中重新提出;若认为复潮条件已满足,须在本变更 decisions.md 记录新版本条目(D-xxx@vN+1)说明依据:'
1304
1406
  ]
1305
- for (const h of rejectedHits) {
1306
- lines.push(`- ${h.id} ${h.title}(${h.file})`)
1307
- lines.push(` - 否决理由:${h.reason || '(未记录)'}`)
1407
+ for (const h of rejectedHits.slice(0, 5)) {
1408
+ lines.push(`- ${h.id} ${h.title}(${h.file})${h.status === 'rejected' ? '' : ' ⚰️死路注记'}`)
1409
+ lines.push(` - ${h.status === 'rejected' ? '否决理由' : '死路'}:${h.status === 'rejected' ? (h.reason || '(未记录)') : deathPathNote(h.reason)}`)
1308
1410
  lines.push(` - 复潮条件:${h.revisitWhen || '(未记录)'}`)
1309
1411
  }
1310
1412
  decInjected = '\n\n' + lines.join('\n')
@@ -1319,6 +1421,26 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1319
1421
  }
1320
1422
  }
1321
1423
 
1424
+ // {UI_VISUAL_GUIDANCE} → UI 触达检测命中时注入「UI 变更执行须知」(2026-09-28-guidance-principles:
1425
+ // 原型诞生地在头脑风暴的方案对比步——frontend-apple-style 会话实证手绘定稿原型因缺开工引导;
1426
+ // 与 flow start 注入同源同文案,检测语料=变更目录 proposal 与 requirements + 变更名)。无命中
1427
+ // 替换空串零输出;fail-soft 异常单行说明,不留残留占位符。
1428
+ if (stageName === 'brainstorm' && promptText.includes('{UI_VISUAL_GUIDANCE}')) {
1429
+ try {
1430
+ const { detectUiTouch, buildUiGuidanceLines } = await import('../ui-visual.js')
1431
+ const uiSpecBase = resolvePromptSpecBase(platformOpts, cwd)
1432
+ let corpus = changeName || ''
1433
+ for (const name of ['proposal.md', 'requirements.md']) {
1434
+ try {
1435
+ corpus += '\n' + readFileSync(join(uiSpecBase, 'changes', changeName, name), 'utf8')
1436
+ } catch { /* 缺席跳过 */ }
1437
+ }
1438
+ substitute(/\{UI_VISUAL_GUIDANCE\}/g, detectUiTouch(corpus) ? '\n' + buildUiGuidanceLines().join('\n') + '\n' : '')
1439
+ } catch (e) {
1440
+ substitute(/\{UI_VISUAL_GUIDANCE\}/g, `[ui-guidance] UI 须知注入异常(${e.message}),跳过`)
1441
+ }
1442
+ }
1443
+
1322
1444
  // Knowledge hit report: execute 阶段注入匹配结果
1323
1445
  // 2026-09-14-knowledge-loop-close task-04(FR-04,X-004 升级既有机制):命中清单报告 → 命中
1324
1446
  // 正文注入——命中时占位符替换为「📚 命中知识」段(top-3 不同 file + 首 40 行截断,段头保留
@@ -2001,6 +2123,12 @@ ${maskVolatileForGuide(guideTemplate)}
2001
2123
  `, 'utf8')
2002
2124
  mkdirSync(stateRoot, { recursive: true })
2003
2125
  writeFileSync(stateFile, JSON.stringify({ fingerprint, guidePath: guideFile, changeName: changeName || null, stage: stageName, stepIndex, at: new Date().toISOString() }, null, 1) + '\n', 'utf8')
2126
+ // 旧指纹清理(2026-10-06-verify-docs-prefill task-05):同步骤旧指纹 guide 永久共存会让
2127
+ // 自行 ls/glob 目录的 agent 读到旧版本指引(厚流程 step5/step6 错位疑云)。白名单 =
2128
+ // 新写的 guide ∪ 仍被任一变更 state 引用的 guide(他变更复入短输出依赖 existsSync 回退,
2129
+ // 被引用文件误删只退化全量重印但无谓);清理 fail-soft 且静默——步骤输出是字节中立面
2130
+ // (knowledge-inject A/B 对比、guidance-output-neutrality),运维日志不进 prompt 流。
2131
+ try { pruneStaleStepGuides({ guideRoot, stateRoot, stageName, stepIndex, keepAbsPaths: [guideFile] }) } catch { /* 清理 best-effort */ }
2004
2132
  }
2005
2133
  } catch { /* guide/state 落盘 best-effort:失败仅退化回全量重印,不阻断步骤输出 */ }
2006
2134
  }
@@ -12,7 +12,7 @@
12
12
  * 启动路径(实测冷加载 100-150ms),而 checkbox 仅"≥2 活跃变更 + TTY"分支用到
13
13
  */
14
14
  import { join, dirname } from 'node:path'
15
- import { existsSync, readFileSync } from 'node:fs'
15
+ import { existsSync, readFileSync, readdirSync, cpSync, rmSync, renameSync } from 'node:fs'
16
16
  import { parsePorcelainPath, safeGit } from './shared.js'
17
17
  import { collectRecentForeignDelivery, detectAssertionRewrites, readSemanticGuardEnabled } from '../semantic-guard.js'
18
18
 
@@ -555,6 +555,14 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
555
555
  } catch { /* 快照链路异常 → 主仓现行为 */ }
556
556
  }
557
557
  }
558
+ // failed/test/lint 提升到 try 外(坑 flowdone-lint-fail-no-output 排障顺带发现 + 收编评审 P1):
559
+ // 此前三者均声明在 try 块内(块级作用域),finally 的快照回拷读 failed.length、resultPath
560
+ // 重映射读 [test, lint] 各抛 ReferenceError 被空 catch 吞掉——P6b 回拷 + 重映射自 2026-09-28
561
+ // 落地即死代码(快照 FAIL 时结果文件随临时目录蒸发、FAIL 三件套的结果文件路径恒死链)。
562
+ // 收编首版只提升了 failed(重映射引用的 test/lint 仍死)——评审 P1 抓出后三变量齐提升。
563
+ const failed = []
564
+ let test = null
565
+ let lint = null
558
566
  try {
559
567
  // ── P2 三键账本(D-001@v1,batch3 task-01):同码同环境免重跑(fail-closed——无记录/
560
568
  // 键不等/分量不可得=真跑,行为不变)。quick 会话名 per-change 天然隔离账本文件。──
@@ -577,7 +585,7 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
577
585
  // 「零信息误判」主因)。
578
586
  console.log(`⏳ 开始 test+lint 实测${snapshot ? '(隔离快照内)' : ''}——大仓全量可达 15-20 分钟,期间无输出属正常;后台跑 + 长容忍(≥20 分钟),勿按超时杀进程(杀掉不丢进度,但每轮重建沙箱重跑实测耗时成倍)。`)
579
587
  }
580
- let test = testLedgerReuse
588
+ test = testLedgerReuse
581
589
  ? { status: 'passed', reason: `♻️ P2 三键账本复用(代码×测试面×环境全等;实测于 ${testLedgerReuse.result.ranAt})`, command: `${testLedgerReuse.runPlan?.[0]?.runner || 'npm test'} (ledger-reuse)`, exitCode: 0, durationMs: 0, outputTail: null }
582
590
  // restrictFiles = 本会话声明文件(坑 quick-gate-并行全流程变更脏文件误伤):模块选择
583
591
  // 与 deps(auto) 只取「实际变更 ∩ 本会话声明」,并行全流程变更 WIP 不再挡死本会话
@@ -603,7 +611,7 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
603
611
  })
604
612
  } catch { /* 记账异常不影响门禁(下次仍真跑) */ }
605
613
  }
606
- let lint = runVerifyLintCheck({ cwd: gateCwd, specBase: gateSpecBase, timeoutMs: snapshot ? 5 * 60 * 1000 : undefined })
614
+ lint = runVerifyLintCheck({ cwd: gateCwd, specBase: gateSpecBase, timeoutMs: snapshot ? 5 * 60 * 1000 : undefined })
607
615
  // 快照 lint 超时回退主仓(2026-09-12 dogfood 两连实证:junction I/O 病态慢,3min/5min 均被
608
616
  // 超时杀——主仓 60~110s 正常。按用户建议「自动回退」而非假败/advisory:主仓复跑保硬门,
609
617
  // 并行噪声混入时失败输出带归属鉴定提示)
@@ -611,7 +619,6 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
611
619
  console.warn('⚠️ 快照 lint 超时(node_modules junction I/O 慢)→ 主仓复跑 lint(并行噪声可能混入——失败先做归属鉴定)')
612
620
  lint = runVerifyLintCheck({ cwd, specBase })
613
621
  }
614
- const failed = []
615
622
  // 哨兵断言(2026-09-25-sentinel-wiring,与 flow done 同判):tasks.md 全勾但零完成
616
623
  // 证据(区间提交消息标题与正文均无 task-NN token 且无对应 review.json)→ 计入 failed 拒收。
617
624
  // 证据面=整条提交消息(2026-09-25-thin-done-gate-calibration 坑2,同 flow 侧 %B%x1e 口径)。
@@ -621,8 +628,11 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
621
628
  const tasksDir = changeName ? join(specBase, 'changes', changeName, 'tasks.md') : null
622
629
  if (tasksDir && existsSync(tasksDir)) {
623
630
  const _log = safeGit ? safeGit(cwd, ['log', '--format=%B%x1e', 'HEAD~10..HEAD']) : null
624
- const commitMsgs = String((_log && !_log.error) ? _log.value : '') || ''
625
- if (commitMsgs) {
631
+ const _logOk = !!(_log && !_log.error)
632
+ const commitMsgs = _logOk ? String(_log.value) || '' : ''
633
+ if (_logOk) { // 空串(区间零提交)照跑——零提交不豁免,哨兵从严
634
+ // 证据判据统一(2026-10-07-thin-tasks-v3):已勾任务一律按 per-task 证据判(提交 token /
635
+ // review.json),镜像豁免与基线读取退役(quick 道本就无基线快照面)。
626
636
  const sent = detectFakeCheckCompletion({ changeDir: dirname(tasksDir), tasksMd: readFileSync(tasksDir, 'utf8'), commits: commitMsgs.split('\x1e').map((x) => x.trim()).filter(Boolean) })
627
637
  if (sent.status === 'fake') {
628
638
  console.error(`\n🚫 哨兵断言拒收:tasks.md 全勾(${sent.checked}/${sent.claimTotal})但 ${sent.missing.length} 个任务零完成证据(区间提交标题与正文均无 token):${sent.missing.join('、')}`)
@@ -678,6 +688,16 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
678
688
  printSnapshotFailureHint({ snapshotRoot: snapshot.snapshotRoot, changeFileCount: snapshot.overlaid, sourceRoot: snapshot.sourceRoot || null }, { offEnv: 'SILLYSPEC_QUICK_GATE_SNAPSHOT_OFF' })
679
689
  } catch { /* 提示失败不影响门禁语义 */ }
680
690
  }
691
+ // lint 结果持久化(坑 flowdone-lint-fail-no-output,2026-10-03):runVerifyLintCheck 全程
692
+ // 静默、tally 只记一句退出码——lint 结果对象并入 test 的 test-result.json(modules 并列
693
+ // 一节);test 无结果文件(skipped)而 lint 实跑时独立落盘(kind: 'lint')。快照模式下
694
+ // 并入写在快照路径、随 P6b FAIL 回拷回主仓(失败恰是排障需要它的时点)。
695
+ try {
696
+ if (lint && (lint.status === 'passed' || lint.status === 'failed')) {
697
+ const { persistLintResult } = await import('../verify-postcheck.js')
698
+ lint.resultPath = persistLintResult({ specBase, changeName: typeof changeName === 'string' ? changeName : null, testResultPath: test && test.resultPath, lint }) || lint.resultPath || null
699
+ }
700
+ } catch { /* 持久化 best-effort,不影响门禁语义 */ }
681
701
  return {
682
702
  action: failed.length > 0 ? 'fail' : 'pass',
683
703
  failed,
@@ -690,8 +710,47 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
690
710
  semanticGuard,
691
711
  }
692
712
  } finally {
693
- if (snapshot) { try { snapshot.cleanup() } catch { /* 清理失败不连坐门禁结论 */ } }
713
+ if (snapshot) {
714
+ // 快照证据回拷(2026-09-28-split-guard-and-gate-report,P6b):快照内 verify-runs 结果
715
+ // 落快照临时目录、收尾即清——测试/lint FAIL 时先回拷主仓 .runtime 再清理,排障证据不蒸发。
716
+ try {
717
+ if (failed.length > 0) {
718
+ const copied = copySnapshotVerifyRuns(gateSpecBase, specBase)
719
+ // 结果路径重映射(评审 P1 清偿):快照内写下的 resultPath 指向快照临时目录——清理后
720
+ // existsSync 恒 false,flow done 的 FAIL 三件套会静默失效。回拷后把 test/lint 的
721
+ // resultPath 改写为主仓路径(对象即返回值,finally 在 return 落地前变异生效)。
722
+ for (const r of [test, lint]) {
723
+ if (r && typeof r.resultPath === 'string' && r.resultPath.startsWith(gateSpecBase)) {
724
+ r.resultPath = specBase + r.resultPath.slice(gateSpecBase.length)
725
+ }
726
+ }
727
+ }
728
+ } catch { /* 回拷 best-effort,不连坐门禁结论 */ }
729
+ try { snapshot.cleanup() } catch { /* 清理失败不连坐门禁结论 */ }
730
+ }
731
+ }
732
+ }
733
+
734
+ /** 快照 .runtime/verify-runs 全量回拷主仓(同名目录不覆盖——主仓自己的结果优先;临时名+原子改名,崩溃残端可续拷)。 */
735
+ function copySnapshotVerifyRuns(fromSpecBase, toSpecBase) {
736
+ const fromDir = join(fromSpecBase, '.runtime', 'verify-runs')
737
+ const toDir = join(toSpecBase, '.runtime', 'verify-runs')
738
+ if (!existsSync(fromDir)) return 0
739
+ let copied = 0
740
+ for (const d of readdirSync(fromDir)) {
741
+ const src = join(fromDir, d)
742
+ const dst = join(toDir, d)
743
+ if (existsSync(dst)) continue
744
+ // 评审 P3 清偿:cpSync 非原子——中途失败留下残缺 dst 且同名跳过阻断续拷自愈。改为
745
+ // 临时名整拷 + renameSync 原子落位;崩溃只留 .part 残端,重跑时先清再拷(可续拷)。
746
+ const part = dst + '.part'
747
+ rmSync(part, { recursive: true, force: true })
748
+ cpSync(src, part, { recursive: true })
749
+ renameSync(part, dst)
750
+ copied++
694
751
  }
752
+ if (copied > 0) console.warn(`📎 快照实测证据已回拷主仓:${copied} 个 verify-runs 目录(排障读主仓 .runtime 即可)`)
753
+ return copied
695
754
  }
696
755
 
697
756
  /**
package/src/run/shared.js CHANGED
@@ -81,7 +81,10 @@ export function assertSafeChangeName(name, label = '变更名') {
81
81
  // 历史 DB/目录已存在的旧变更不追诉(归档里 auto-flow-optimization 等 10 个无前缀名照常可读)。
82
82
  // 豁免:quick-<8hex> 会话 key 与 default 兜底 key 是系统生成名,非 agent 自拟描述名。
83
83
  // 日期只校验形状(月 01-12 日 01-31),不校验「当天」——跨天续跑/次日重命名不应被拦。
84
- const DATED_CHANGE_NAME_RE = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])-[a-zA-Z0-9][\w.\-]*$/
84
+ // slug 首字符放宽含 CJK(一-鿿,与 flow 族 validateChangeName 同字符集):flow start 2026-09-28
85
+ // 起复用本校验器,中文名(平台键 2026-09-25-中文名-a1b2c3 形态)是 flow 族既有测试特性;
86
+ // run 族不受影响——其上游 assertSafeChangeName 仅放行 ASCII [\w.\-],中文在到达本门之前已被拦。
87
+ const DATED_CHANGE_NAME_RE = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])-[a-zA-Z0-9一-鿿][\w.\-一-鿿]*$/
85
88
  export function assertDatedChangeName(name, label = '变更名') {
86
89
  if (name == null) return
87
90
  const s = String(name)
@@ -1872,16 +1875,8 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
1872
1875
  type: 'fr-rot-suspect', change: guard.changeName || null,
1873
1876
  domains: [...touchedDomains], count: frs.length, source: 'quick-done',
1874
1877
  })
1875
- // 地图诚实升级(2026-09-20-quick-asset-tail FR-02/D-003):遥测之外给条目打
1876
- // 待复核标记——下一次 brainstorm 注入行带 ⚠️,承接翻链时清除。fail-open 同块。
1877
- const markRef = guard.changeName || 'recent-quick'
1878
- try {
1879
- const { markFrNeedsReview } = await import('../fr-index.js')
1880
- const mr = markFrNeedsReview(join(specBase, 'knowledge'), frs.map(f => f.id), markRef)
1881
- if (mr.marked > 0) {
1882
- console.warn(`⚠️ [FR 待复核标记] ${mr.marked} 条 active FR 已标「待复核:${markRef}」——下次 brainstorm 注入可见,承接翻链时自动清除`)
1883
- }
1884
- } catch { /* 标记失败不影响遥测/审计 */ }
1878
+ // 持久打标已拆(2026-09-29-rot-retire-inject-cap:371 条零消费实证)——
1879
+ // 遥测 + 当场 warn 即止,不留账。fail-open 同块。
1885
1880
  console.warn(`⚠️ [FR 腐烂 suspect·advisory] 本次 quick 触达 ${[...touchedDomains].join('、')} 域的 ${frs.length} 条 active FR——quick 不走归档,若改动影响这些行为,索引不会自动 supersede(L3 声明义务;盲区:unmapped 文件/map 漂移不报警)`)
1886
1881
  }
1887
1882
  }
package/src/run/stage.js CHANGED
@@ -416,7 +416,11 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
416
416
  // 注:CLI 进程入口的新会话已被 index.js refuseRetiredQuickFreshStart 预门拦下,本门兜底
417
417
  // 进程内直调 runCommand 的入口(测试/编程调用)与 --change 指向不存在会话的形态。
418
418
  console.error(`❌ quick 通道已退役,无法启动 quick 会话(若带 --change 则该会话不存在或已清理)。`)
419
- console.error(` 新工作请走轻量变更:sillyspec flow start --change <名> --input "<动机与背景;随后独立一行『成功标准:』;再每行一条『- <可验证标准>』>"`)
419
+ console.error(` 新工作请走轻量变更(--input 引号内换行合法,可照抄):`)
420
+ console.error(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
421
+ console.error('')
422
+ console.error(' 成功标准:')
423
+ console.error(' - <可验证标准>"')
420
424
  console.error(` 升级前进行中的 quick 会话仍可收尾:sillyspec run quick --change <会话ID> 续跑 / --done 收口 / --cancel 取消。`)
421
425
  // 进程内路径幻影残留清理(best-effort):CLI 入口已被 index.js 预门拦下;进程内直调
422
426
  // runCommand 的新会话形态在 command.js 已生成 sid + owner.json + current-quick-run-id +