sillyspec 3.33.3 → 3.33.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 (55) hide show
  1. package/.claude/skills/sillyspec-config/SKILL.md +48 -0
  2. package/.claude/skills/sillyspec-flow/SKILL.md +4 -0
  3. package/AGENTS.md +1 -1
  4. package/package.json +1 -1
  5. package/src/config-known-failures.js +258 -0
  6. package/src/config-schema.js +28 -0
  7. package/src/config-validate.js +180 -0
  8. package/src/decision-distill.js +26 -5
  9. package/src/decision-redomain.js +5 -4
  10. package/src/docs-check.js +8 -4
  11. package/src/doctor-diagnostics.js +89 -6
  12. package/src/flow-parity.js +22 -2
  13. package/src/flow-preflight.js +67 -0
  14. package/src/flow.js +252 -55
  15. package/src/fr-index.js +7 -3
  16. package/src/git-helper.js +82 -0
  17. package/src/handoff.js +4 -0
  18. package/src/hooks/worktree-guard.js +12 -3
  19. package/src/hot-cache.js +60 -0
  20. package/src/index.js +111 -7
  21. package/src/knowledge-graph.js +82 -2
  22. package/src/knowledge-match.js +18 -2
  23. package/src/knowledge-quicklog.js +17 -2
  24. package/src/knowledge-vector.js +5 -3
  25. package/src/machine-interface.js +34 -13
  26. package/src/progress/change-registry.js +15 -0
  27. package/src/progress/shared.js +4 -0
  28. package/src/progress/stage-machine.js +15 -2
  29. package/src/progress.js +5 -0
  30. package/src/redomain-unmapped.js +204 -0
  31. package/src/redomain.js +5 -3
  32. package/src/review-dispatch.js +208 -47
  33. package/src/run/bg-sync.js +4 -0
  34. package/src/run/code-face-key.js +4 -1
  35. package/src/run/command.js +20 -5
  36. package/src/run/complete-handlers.js +38 -4
  37. package/src/run/complete.js +185 -14
  38. package/src/run/gates.js +30 -1
  39. package/src/run/prompt.js +9 -1
  40. package/src/run/quick-audit.js +4 -2
  41. package/src/run/shared.js +18 -1
  42. package/src/run/stage.js +22 -19
  43. package/src/run/verify-quality-scan.js +51 -0
  44. package/src/runtime-paths.js +3 -0
  45. package/src/sillyhub-mcp/client.js +11 -6
  46. package/src/spawn-settle.js +47 -0
  47. package/src/spec-sync.js +89 -3
  48. package/src/stages/knowledge.js +25 -3
  49. package/src/timing.js +55 -0
  50. package/src/verify-postcheck.js +153 -11
  51. package/src/watcher.js +4 -0
  52. package/src/worktree-apply.js +67 -4
  53. package/src/worktree.js +80 -11
  54. package/src/yaml-scalar-unquote.js +43 -0
  55. package/templates/agents-instruction.md +1 -1
package/src/flow.js CHANGED
@@ -83,14 +83,22 @@ export function readFlowConfig(specBase) {
83
83
  // module_gate_ignore(2026-10-12-thin-module-gate-and-feed):未登记目录软门的固定忽略清单,
84
84
  // 单行逗号串(目录前缀,posix)——列表块形态不支持,readFlowConfig 同款逐键正则防线
85
85
  const ig = raw.match(/^\s*module_gate_ignore\s*:\s*(.+)$/m)
86
+ // turbo docs-auto 常设授权档(2026-10-23-turbo-docs-auto):仓主一行声明纯文档面自动降档;
87
+ // 逐键正则同款防线,未配置/坏值(turbo: off 等)fail-soft 恒 false
88
+ const turboDocsAuto = /^\s*turbo\s*:\s*docs-auto\s*$/m.test(raw)
89
+ // review_inline_wait(2026-10-23-review-inline-dispatch):薄道评审平台单调用通道的等待
90
+ // 预算秒数(1-600 正整数有效;缺省/坏值 0=off——回退现状任务书面零回归)
91
+ const riw = raw.match(/^\s*review_inline_wait\s*:\s*([0-9]+)\s*$/m)
86
92
  return {
87
93
  mode,
88
94
  editRatioThreshold: th ? Number(th[1]) : 0.5,
89
95
  editRatioEnforcement: en ? en[1] : 'advisory',
90
96
  moduleGateIgnore: ig ? ig[1].split(',').map((s) => s.trim()).filter(Boolean) : [],
97
+ turboDocsAuto,
98
+ reviewInlineWaitSeconds: riw ? Math.min(600, Math.max(0, Number(riw[1]))) : 0,
91
99
  }
92
100
  } catch {
93
- return { mode: 'thin', editRatioThreshold: 0.5, editRatioEnforcement: 'advisory', moduleGateIgnore: [] }
101
+ return { mode: 'thin', editRatioThreshold: 0.5, editRatioEnforcement: 'advisory', moduleGateIgnore: [], turboDocsAuto: false, reviewInlineWaitSeconds: 0 }
94
102
  }
95
103
  }
96
104
 
@@ -630,7 +638,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
630
638
  const _localYaml = join(specBase, 'local.yaml')
631
639
  if (!existsSync(_localYaml) && !existsSync(join(specBase, 'changes', change))) {
632
640
  console.error('❌ local.yaml 不存在——先跑 sillyspec init(mode/lint/known_failures 等 init 级配置的落点)。')
633
- console.error(' 测试面无需在此配置(2026-09-26 起:测试门按变更动态推断——本变更测试 ∪ FR 关联回归 ∪ import 依赖,')
641
+ console.error(' 测试面无需在此配置(测试门按变更动态推断——本变更测试 ∪ import 依赖;FR 关联回归层缺省不跑归 pre-push/CI 门(2026-10-23-fr-regress-default-off),fr_regress_cap: N|all 可开启,')
634
642
  console.error(' runner 自项目结构 pyproject/package.json 推断;modules.*.test/commands.test 已退役,后者仅显式 test_strategy: full 生效)。')
635
643
  process.exit(2)
636
644
  }
@@ -1158,6 +1166,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
1158
1166
  ``,
1159
1167
  `🛑 三断点纪律(顺序纪律:①未过不动码;②③为汇报点——用户没说「全跑完」就必须在每个断点向用户汇报并等确认):`,
1160
1168
  ` ① spec 断点【机器门】:填完 FR 区和 design 四节后,把摘要给用户看(FR 条目+盲维作答+方案概述),`,
1169
+ ` 起草完先自查:sillyspec flow verify-drafts --change ${change}(与批准门同判定——自查通过再请批准,用户一次过);`,
1161
1170
  ` 用户确认后请其运行 sillyspec flow approve --change ${change}(批准留痕)再动手写代码——`,
1162
1171
  ` agent 代跑时如实声明通道:--user-confirmed(转述用户已确认)/ --self-approved(agent 自批),`,
1163
1172
  ` 无 flag 按 unattested 留痕(审计面无法区分用户确认与自批)——2026-10-12-approve-channel-attest。`,
@@ -1268,7 +1277,35 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1268
1277
  }
1269
1278
  // 极速模式读态(2026-10-14-thin-turbo-mode FR-02/D-007):turbo && tier!=='thick' 双条件——
1270
1279
  // 升厚(distill 异态/verify 失败/--thick 起步)后 turbo 残留不短路厚档仪式,重入即恢复全量校验
1271
- const turbo = st.turbo === true && st.tier !== 'thick'
1280
+ // turbo 双源(2026-10-23-turbo-docs-auto):旗标档(--turbo 用户显式)原样;未开且仓主常设
1281
+ // 授权 turbo: docs-auto 时按面自动降档——面=changedFilesSinceBaseline(提交∪脏面)经
1282
+ // dirty_gate_ignore 前缀剔除(与 dirty 门同一残渣口径)后非空且无文件命中代码分类器
1283
+ // (quick-audit 单一源)。fail-closed:面空/含代码/读取或判定异常一律走全量(不静默降档);
1284
+ // 升厚即失效对双源同效(tier=thick 关)。授权权在仓主(local.yaml 一行),agent 仍无法自选。
1285
+ const turboFlag = st.turbo === true && st.tier !== 'thick'
1286
+ let turbo = turboFlag
1287
+ let turboSource = turboFlag ? 'flag' : null
1288
+ if (!turbo && st.tier !== 'thick') {
1289
+ try {
1290
+ if (readFlowConfig(specBase).turboDocsAuto === true) {
1291
+ const { readDirtyGateIgnore } = await import('./flow-parity.js')
1292
+ const { CODE_PATH_SEGMENT_RE, CODE_EXTENSION_RE } = await import('./run/quick-audit.js')
1293
+ const ignorePfx = readDirtyGateIgnore(specBase)
1294
+ const face = (changedFilesSinceBaseline(cwd, st.baseline_commit) || [])
1295
+ .filter((f) => !ignorePfx.some((p) => f.startsWith(p)))
1296
+ const docsOnly = face.length > 0 && !face.some((f) => CODE_PATH_SEGMENT_RE.test(f) || CODE_EXTENSION_RE.test(f))
1297
+ if (docsOnly) {
1298
+ turbo = true
1299
+ turboSource = 'docs-auto'
1300
+ console.log(`⚡ turbo(docs-auto):提交/冻结面纯文档(${face.length} 文件)——实测/评审/UI 探针按极速档跳过(授权源 local.yaml turbo: docs-auto;升厚即失效)`)
1301
+ }
1302
+ }
1303
+ } catch { /* 面判定异常 fail-closed 走全量 */ }
1304
+ }
1305
+ // 实测门 reason 的授权源标签(防文案漂移:docs-auto 档不得显示「用户显式触发」)
1306
+ const turboLabel = turboSource === 'docs-auto'
1307
+ ? 'local.yaml turbo: docs-auto 常设授权(提交/冻结面纯文档)'
1308
+ : `用户显式触发 ${st.turbo_since || '时点未记'}`
1272
1309
  const runtimeRoot = resolveRuntimeRoot(runtimeRootOpt ? { runtimeRoot: runtimeRootOpt } : {}, specBase)
1273
1310
  // 归属收窄清单(2026-09-25-thin-dogfood-fixes 修复②):baseline..HEAD 不分作者——并行会话在
1274
1311
  // start..done 之间的提交会混入本变更实测面与 FR 域路由。复用 verify 对账同款切分器:他侧显式
@@ -1377,7 +1414,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1377
1414
  if (dr.applicable && dr.emptySlots.length > 0) {
1378
1415
  console.error(`❌ 设计记录未作答:design.md 有 ${dr.emptySlots.length} 个空 AGENT 槽(${dr.emptySlots.join('、')})`)
1379
1416
  console.error(` 每节至少写一行(小改动可写「不适用:<理由>」)——设计承诺是评审与 FR 对账的锚点,空槽=承诺未落盘`)
1380
- console.error(` 恢复指引:槽标记(<!--AGENT:槽N)被删时,从 .runtime/step-guides/ 的指纹缓存可找骨架原文;或删 design.md 后重入 flow start 补生成(criteria 从 proposal 回提)`)
1417
+ console.error(` 恢复指引:槽标记(<!--AGENT:槽N)被删时,骨架原文永存于 .runtime/state/draft-ledger-${change}.json 的 files["design.md"].text(draft-ledger 机器稿首版永存——2026-10-23 实证修正:step-guides/ 只有阶段指引,从未缓存 flow design 骨架,旧指引指向不存在的缓存);从 ledger 取槽标记与问题行原样补回(作答放问题行之后——槽在问题行前会被当成答案);或删 design.md 后重入 flow start 补生成(criteria 从 proposal 回提,注意会重置为骨架态)`)
1381
1418
  reportMidFail('artifacts')
1382
1419
  process.exit(1)
1383
1420
  }
@@ -1578,13 +1615,49 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1578
1615
  }
1579
1616
  }
1580
1617
  } catch { /* 节奏门 best-effort:读流失败静默(fail-open——观测缺席不阻断) */ }
1618
+ // ── 收口预检(fail-together 一次报全,2026-10-23-flow-done-preflight):实测门前只读收集
1619
+ // 便宜阻断条件(dirty-gap/orphan 预判)——56% 触发率的 dirty 门不再每轮先付完整实测;
1620
+ // 命中即拦在付费实测之前,全部条件+处置出路一次报全(b1ab65b2「两条修复一次给全」
1621
+ // 模式升格)。全过静默零行为变化;判定与权威门同源同旗标镜像(collectCloseoutPreflight
1622
+ // fail-safe:异常静默放行,权威门仍在原子步原序)。
1623
+ {
1624
+ const { collectCloseoutPreflight } = await import('./flow-preflight.js')
1625
+ const pre = await collectCloseoutPreflight({ cwd, specBase, change, st, freezeDirty, acceptDirtyGap, acceptOrphanFace, turbo })
1626
+ if (pre.findings.length > 0) {
1627
+ console.error(`⛔ 收口预检未过——${pre.findings.length} 项阻断条件(fail-together 一次报全;实测未启动,处置后重跑同一条命令从断点续):`)
1628
+ for (const f of pre.findings) {
1629
+ if (f.id === 'dirty-gap') {
1630
+ // 逐字复用 patch 子步权威门文案(防两路口径漂移——文案即契约面)
1631
+ console.error(` ▸ ${f.count} 个未提交交付文件未入冻结面(共享主仓无法归属)——三选一:`)
1632
+ console.error(`⛔ 未提交交付缺口未显式处置——收口阻断(不归档;处置后重跑同一条命令从断点续):`)
1633
+ console.error(` ① git commit 这些文件(信息尾缀带 (${change}) 或 (${change})),重跑本命令——冻结面自动并入`)
1634
+ console.error(` ② 全部归属本变更:flow done --change ${change} --freeze-dirty`)
1635
+ console.error(` ③ 接受审计缺口:flow done --change ${change} --accept-dirty-gap`)
1636
+ console.error(` 💡 快速通道:脏文件全属并行会话在途/运行残渣且不在本变更交付面时,直接重跑带 --accept-dirty-gap(显式 pathspec 提交面已隔离归属;属本变更的文件必须走①提交)`)
1637
+ console.error(` ④ 已知残渣固定忽略:.sillyspec/local.yaml 加 dirty_gate_ignore: <目录前缀>(单行逗号分隔,同 module_gate_ignore)`)
1638
+ console.error(` ${f.files.slice(0, 3).join('、')}${f.files.length > 3 ? ' 等' : ''}`)
1639
+ } else if (f.id === 'orphan-face') {
1640
+ // 逐字复用 patch 子步权威门文案(防两路口径漂移——文案即契约面)
1641
+ console.error(` ▸ 会话专属 worktree 冻结面外交付改动——三选一:`)
1642
+ console.error(`⛔ 会话专属 worktree 存在冻结面外交付改动:${f.commits.length} 个提交不在 HEAD 祖先链(patch-id 不等价)触及 ${f.count} 个交付文件——收口阻断(不归档;处置后重跑同一条命令从断点续):`)
1643
+ console.error(` 提交:${f.commits.slice(0, 5).map((c) => c.slice(0, 10)).join('、')}${f.commits.length > 5 ? ' 等' : ''}`)
1644
+ console.error(` 文件:${f.files.slice(0, 5).join('、')}${f.files.length > 5 ? ' 等' : ''}`)
1645
+ console.error(` ① git rebase/cherry-pick 这些提交到 HEAD(补齐冻结面)后重跑`)
1646
+ console.error(` ② 确认放弃这些提交:清理对应分支/reflog 源后重跑`)
1647
+ console.error(` ③ 接受缺口留痕:flow done --change ${change} --accept-orphan-face`)
1648
+ }
1649
+ }
1650
+ reportMidFail('patch')
1651
+ process.exit(1)
1652
+ }
1653
+ }
1581
1654
  // 极速模式实测门跳过(2026-10-14-thin-turbo-mode FR-02/D-002):turbo 在场不调
1582
1655
  // runQuickTestLintGate,合成 gate 同形状 skipped 对象(带 status:'skipped' 完整形态——
1583
1656
  // 下方 fmt 对 null 显示 '—',带形态才输出「skipped — 跳过原因」,审计可见;
1584
1657
  // 对齐 quick-audit env 逃生门 skip 形态先例 src/run/quick-audit.js:482)。
1585
1658
  // 失败升厚分支(下方 action==='fail')turbo 下天然不触达——升厚语义不变。
1586
1659
  const gate = turbo
1587
- ? { action: 'skip', failed: [], reason: `⚡ 极速模式(用户显式触发 ${st.turbo_since || '时点未记'})——test+lint 实测跳过(审计留痕;本次收口不含测试客观核验)`, test: { status: 'skipped', command: null, exitCode: null, durationMs: null, outputTail: null, reason: `⚡ 极速模式(用户显式触发 ${st.turbo_since || '时点未记'})——本次收口不含测试客观核验(审计留痕)`, resultPath: null }, lint: { status: 'skipped', command: null, exitCode: null, durationMs: null, outputTail: null, reason: '⚡ 极速模式——lint 实测跳过(审计留痕)', resultPath: null } }
1660
+ ? { action: 'skip', failed: [], reason: `⚡ 极速模式(${turboLabel})——test+lint 实测跳过(审计留痕;本次收口不含测试客观核验)`, test: { status: 'skipped', command: null, exitCode: null, durationMs: null, outputTail: null, reason: `⚡ 极速模式(${turboLabel})——本次收口不含测试客观核验(审计留痕)`, resultPath: null }, lint: { status: 'skipped', command: null, exitCode: null, durationMs: null, outputTail: null, reason: '⚡ 极速模式——lint 实测跳过(审计留痕)', resultPath: null } }
1588
1661
  : await runQuickTestLintGate({ cwd, specBase, changedFiles, changeName: change, skipSentinel: true /* flow 侧哨兵带 baseline 区间,quick 侧区间不可靠——单判不双判 */ })
1589
1662
  if (gate && gate.action === 'fail') {
1590
1663
  console.error(`❌ 测试门 FAIL(整单 FAIL——实测失败/超时=失败,不继续 distill/归档):`)
@@ -1902,6 +1975,11 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1902
1975
  if (freeze.dirtyAdded.length > 0) {
1903
1976
  console.log(`🔒 ${exclusiveFrom === 'flag' ? '--freeze-dirty 显式声明' : '会话专属 worktree'}:${freeze.dirtyAdded.length} 个未提交交付文件一并入冻结面`)
1904
1977
  }
1978
+ // 已知残渣剔除留痕(2026-10-23-dirty-gate-ignore):collectFreezeFiles 单一源剔除
1979
+ // dirty_gate_ignore 命中面——每轮一行可审计(配了什么、剔了几个),预检同源继承。
1980
+ if (Array.isArray(freeze.dirtyIgnored) && freeze.dirtyIgnored.length > 0) {
1981
+ console.log(`ℹ️ ${freeze.dirtyIgnored.length} 个 dirty 文件按 local.yaml dirty_gate_ignore 剔除(已知残渣面,不入冻结缺口):${freeze.dirtyIgnored.slice(0, 3).join('、')}${freeze.dirtyIgnored.length > 3 ? ' 等' : ''}`)
1982
+ }
1905
1983
  if (freeze.dirtyWarned.length > 0) {
1906
1984
  console.warn(`⚠️ ${freeze.dirtyWarned.length} 个未提交交付文件未入冻结面(共享主仓无法归属)——三选一:`)
1907
1985
  console.warn(` ① 先提交这些文件(信息尾缀带变更名)后重跑 ② 确认全部归属本变更:重跑 flow done --freeze-dirty 并入冻结 ③ 确认接受审计缺口:重跑 flow done --accept-dirty-gap(缺口数随 change-patch.json 留痕)。Git 中间提交可归档后压扁(见收口指引)`)
@@ -1922,6 +2000,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1922
2000
  console.error(` ② 全部归属本变更:flow done --change ${change} --freeze-dirty`)
1923
2001
  console.error(` ③ 接受审计缺口:flow done --change ${change} --accept-dirty-gap`)
1924
2002
  console.error(` 💡 快速通道:脏文件全属并行会话在途/运行残渣且不在本变更交付面时,直接重跑带 --accept-dirty-gap(显式 pathspec 提交面已隔离归属;属本变更的文件必须走①提交)`)
2003
+ console.error(` ④ 已知残渣固定忽略:.sillyspec/local.yaml 加 dirty_gate_ignore: <目录前缀>(单行逗号分隔,同 module_gate_ignore)`)
1925
2004
  reportMidFail('patch')
1926
2005
  process.exit(1)
1927
2006
  }
@@ -2351,6 +2430,41 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2351
2430
  } catch { /* 无/坏 JSON → 保持 null(既有判定) */ }
2352
2431
  }
2353
2432
  const tier = classifyReviewNeed({ changeDir, input: null, patchText, editRatio: st.edit_ratio ?? null, reviewForce: st.review_force ?? null, change })
2433
+ // 登记块单一源(2026-10-23-review-inline-dispatch):子代理重跑路径与平台 inline 回收
2434
+ // 路径同源消费——validateReviewJson+FAIL/P1 拦截+P2/P3 留债+PASS mark,无第二套判定。
2435
+ const registerReviewOutcome = async () => {
2436
+ const v = validateReviewJson(join(changeDir, 'review.json'))
2437
+ if (!v.ok) {
2438
+ console.error(`❌ review.json 校验失败(${v.errors.join(';')})——按任务书 schema 修正后重跑`)
2439
+ reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'invalid' }
2440
+ appendTelemetry(reviewOutcome) // 修评审 P2①
2441
+ reportMidFail('review')
2442
+ process.exit(1)
2443
+ }
2444
+ const p1s = (v.review.findings || []).filter((f) => /^P1$/i.test(String(f.severity || '')))
2445
+ const others = (v.review.findings || []).filter((f) => !/^P1$/i.test(String(f.severity || '')))
2446
+ if (v.review.verdict === 'FAIL' || p1s.length > 0) {
2447
+ console.error(`❌ 独立评审未过(verdict=${v.review.verdict},P1 发现 ${p1s.length} 项)——承诺与实现不一致,不许归档:`)
2448
+ for (const f of [...p1s, ...others]) console.error(` [${f.severity}] ${f.title} — ${f.evidence || ''}${f.location ? `(${f.location})` : ''}`)
2449
+ console.error(` 修复后删除 review.json 并重新执行评审任务书(起子代理重评),再重跑 flow done(断点续)`)
2450
+ reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'FAIL', findingsP1: p1s.length }
2451
+ appendTelemetry(reviewOutcome) // 修评审 P2①:sampled+FAIL 恰是误豁免率校准的最关键信号
2452
+ reportMidFail('review')
2453
+ process.exit(1)
2454
+ }
2455
+ if (others.length > 0) {
2456
+ console.warn(`⚠️ 评审非阻断发现 ${others.length} 项(P2/P3,随归档留档):`)
2457
+ for (const f of others) console.warn(` [${f.severity}] ${f.title} — ${f.evidence || ''}`)
2458
+ // 处置提示(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop):
2459
+ // P2/P3 非阻断可留债归档;要修则修复面决定是否重评——只碰治理文件不动本变更
2460
+ // requirements/design 的后缀提交触发治理面等价保留(评审保留+自动重冻结,零重评);
2461
+ // 触及交付面或承诺面才隔离重评(任务书带前轮 findings,复审只验修复+新增)。
2462
+ console.warn(` 💡 处置参考:P2/P3 非阻断,可留债归档;要修——只碰治理文件(.sillyspec/**,不动本变更 requirements/design)的提交不触发重评;触及交付面或 requirements/design 需重评(复审只验修复+新增,非全量首评)`)
2463
+ }
2464
+ console.log(`✅ 独立评审通过(reviewer=${v.review.reviewer}${tier.sampled ? ',豁免抽查采样命中' : ''})`)
2465
+ reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'PASS', findingsP1: 0 }
2466
+ mark('review')
2467
+ }
2354
2468
  if (!tier.required) {
2355
2469
  console.log(`⚖️ 评审豁免(低风险证据齐全):${tier.exemptEvidence.join(';')}`)
2356
2470
  reviewOutcome = { required: false, sampled: false, verdict: 'exempt' }
@@ -2361,9 +2475,17 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2361
2475
  // 不再被漂移重冻结误隔离(实测竞态修复)。rev-parse 失败传 null(任务书退引导形态)。
2362
2476
  let taskbookHead = null
2363
2477
  try { const h = gitQuiet(cwd, ['rev-parse', 'HEAD']); if (h && /^[0-9a-f]{7,40}$/i.test(String(h).trim())) taskbookHead = String(h).trim() } catch { /* best-effort */ }
2364
- // 派发纪律(2026-10-19-thin-review-dispatch-verbatim):横幅是主会话派发时最先读到的通道,
2365
- // 明确「原样复制」——实测手写缩编会丢材料包(厚道 stage-review.js:803 同款指令的薄道对齐)。
2366
- console.log(`⚖️ 本变更需要独立评审(${tier.reasons.join(';')})——评审任务书如下,任务书原样复制给子代理(勿自行改写增删)执行后重跑本命令:\n`)
2478
+ // 平台单调用通道(2026-10-23-review-inline-dispatch):review_inline_wait 键门控——键有效
2479
+ // 先试 inline(派发→有界等待→回收→同调用登记,missing→PASS 往返消灭);任何失败回退
2480
+ // 现状任务书面(零治理损失)。键 off 时本分支与改动前逐字一致。
2481
+ const inlineWaitSec = readFlowConfig(specBase).reviewInlineWaitSeconds
2482
+ if (inlineWaitSec > 0) {
2483
+ console.log(`⚖️ 本变更需要独立评审(${tier.reasons.join(';')})——平台单调用通道已启用(review_inline_wait=${inlineWaitSec}s),先试 inline 派发:`)
2484
+ } else {
2485
+ // 派发纪律(2026-10-19-thin-review-dispatch-verbatim):横幅是主会话派发时最先读到的通道,
2486
+ // 明确「原样复制」——实测手写缩编会丢材料包(厚道 stage-review.js:803 同款指令的薄道对齐)。
2487
+ console.log(`⚖️ 本变更需要独立评审(${tier.reasons.join(';')})——评审任务书如下,任务书原样复制给子代理(勿自行改写增删)执行后重跑本命令:\n`)
2488
+ }
2367
2489
  // 项目评审探针装配(2026-10-12-review-probe-injection):local.yaml 数据驱动的项目特有
2368
2490
  // 必查面注入任务书(未配置零注入;装载告警/帽留痕 best-effort 提示,不阻断渲染)。
2369
2491
  let _probeLines = null
@@ -2403,43 +2525,39 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2403
2525
  const _pack = assembleReviewPack({ changeDir, patchText, attribution: _attribution, testReceipt: _receipt })
2404
2526
  if (_pack && Array.isArray(_pack.lines) && _pack.lines.length > 0) _packLines = _pack.lines
2405
2527
  } catch { /* 材料包面 fail-soft:装配失败零注入 */ }
2406
- console.log(renderReviewerTaskbook({ change, changeDir, head: taskbookHead, probeLines: _probeLines, packLines: _packLines }))
2407
- reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'missing' }
2408
- appendTelemetry(reviewOutcome) // 修评审 P2①:失败面先落账再 exit(校准信号不丢)
2409
- reportMidFail('review')
2410
- process.exit(1)
2411
- } else {
2412
- const v = validateReviewJson(join(changeDir, 'review.json'))
2413
- if (!v.ok) {
2414
- console.error(`❌ review.json 校验失败(${v.errors.join(';')})——按任务书 schema 修正后重跑`)
2415
- reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'invalid' }
2416
- appendTelemetry(reviewOutcome) // 修评审 P2①
2417
- reportMidFail('review')
2418
- process.exit(1)
2528
+ const taskbookText = renderReviewerTaskbook({ change, changeDir, head: taskbookHead, probeLines: _probeLines, packLines: _packLines })
2529
+ let inlineWritten = false
2530
+ if (inlineWaitSec > 0) {
2531
+ try {
2532
+ const { runThinReviewInline } = await import('./review-dispatch.js')
2533
+ const inline = await runThinReviewInline({
2534
+ cwd, specBase, change, changeDir, taskbookText,
2535
+ waitMs: Math.min(300_000, inlineWaitSec * 1000),
2536
+ log: (m) => console.log(m),
2537
+ })
2538
+ if (inline.ok) {
2539
+ console.log(`✅ 平台评审单调用回收(mission ${String(inline.missionId).slice(0, 8)}/worker ${String(inline.workerId).slice(0, 8)},reviewer=platform-inline)——同调用登记结论(missing→PASS 往返消灭)`)
2540
+ inlineWritten = true
2541
+ } else {
2542
+ console.log(`ℹ️ 平台评审单调用未回收:${inline.reason}——回退评审任务书面`)
2543
+ }
2544
+ } catch (e) {
2545
+ console.log(`ℹ️ 平台评审单调用异常(${(e && e.message) || e})——回退评审任务书面`)
2546
+ }
2419
2547
  }
2420
- const p1s = (v.review.findings || []).filter((f) => /^P1$/i.test(String(f.severity || '')))
2421
- const others = (v.review.findings || []).filter((f) => !/^P1$/i.test(String(f.severity || '')))
2422
- if (v.review.verdict === 'FAIL' || p1s.length > 0) {
2423
- console.error(`❌ 独立评审未过(verdict=${v.review.verdict},P1 发现 ${p1s.length} 项)——承诺与实现不一致,不许归档:`)
2424
- for (const f of [...p1s, ...others]) console.error(` [${f.severity}] ${f.title} — ${f.evidence || ''}${f.location ? `(${f.location})` : ''}`)
2425
- console.error(` 修复后删除 review.json 并重新执行评审任务书(起子代理重评),再重跑 flow done(断点续)`)
2426
- reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'FAIL', findingsP1: p1s.length }
2427
- appendTelemetry(reviewOutcome) // 修评审 P2①:sampled+FAIL 恰是误豁免率校准的最关键信号
2548
+ if (!inlineWritten) {
2549
+ if (inlineWaitSec > 0) {
2550
+ console.log(`⚖️ 回退评审任务书面——任务书原样复制给子代理(勿自行改写增删)执行后重跑本命令:\n`)
2551
+ }
2552
+ console.log(taskbookText)
2553
+ reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'missing' }
2554
+ appendTelemetry(reviewOutcome) // 修评审 P2①:失败面先落账再 exit(校准信号不丢)
2428
2555
  reportMidFail('review')
2429
2556
  process.exit(1)
2430
2557
  }
2431
- if (others.length > 0) {
2432
- console.warn(`⚠️ 评审非阻断发现 ${others.length} 项(P2/P3,随归档留档):`)
2433
- for (const f of others) console.warn(` [${f.severity}] ${f.title} — ${f.evidence || ''}`)
2434
- // 处置提示(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop):
2435
- // P2/P3 非阻断可留债归档;要修则修复面决定是否重评——只碰治理文件不动本变更
2436
- // requirements/design 的后缀提交触发治理面等价保留(评审保留+自动重冻结,零重评);
2437
- // 触及交付面或承诺面才隔离重评(任务书带前轮 findings,复审只验修复+新增)。
2438
- console.warn(` 💡 处置参考:P2/P3 非阻断,可留债归档;要修——只碰治理文件(.sillyspec/**,不动本变更 requirements/design)的提交不触发重评;触及交付面或 requirements/design 需重评(复审只验修复+新增,非全量首评)`)
2439
- }
2440
- console.log(`✅ 独立评审通过(reviewer=${v.review.reviewer}${tier.sampled ? ',豁免抽查采样命中' : ''})`)
2441
- reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'PASS', findingsP1: 0 }
2442
- mark('review')
2558
+ await registerReviewOutcome()
2559
+ } else {
2560
+ await registerReviewOutcome()
2443
2561
  }
2444
2562
  }
2445
2563
 
@@ -2712,6 +2830,82 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2712
2830
  }
2713
2831
 
2714
2832
  /** flow 命令族入口(index.js case 'flow' 接线):flow start|done|amend-draft。 */
2833
+ /**
2834
+ * spec 面完整性只读校验单一源(2026-10-23-flow-verify-drafts):approve 批准门与
2835
+ * verify-drafts 自查命令同判定同源——verifyFlowDrafts repair:false 只读轨(不做 design 问题行
2836
+ * 自动回填,写盘修复属 done 面)+ ledger 机本位缺失降级(仅 v1 AGENT 槽形态跑空槽兜底)。
2837
+ * 纯计算零打印:调用方各自渲染(approve 文案钉死既有形态,verify-drafts 按新面渲染)。
2838
+ * 返回 { applicable, schema, violations, advisories, skipDesign, degraded, v1EmptySlots }。
2839
+ */
2840
+ async function computeSpecFaceChecks({ changeDir, change, specBase, runtimeRootOpt = null }) {
2841
+ const runtimeRoot = resolveRuntimeRoot(runtimeRootOpt ? { runtimeRoot: runtimeRootOpt } : {}, specBase)
2842
+ const { verifyFlowDrafts, verifyDesignRecordFilled } = await import('./flow-draft.js')
2843
+ const st = readFlowState(changeDir) || {}
2844
+ const adoptedDesign = st.adopted_from === 'brainstorm' && existsSync(join(changeDir, 'design.md'))
2845
+ const r = verifyFlowDrafts({ changeDir, change, runtimeRoot, skipDesign: adoptedDesign, repair: false })
2846
+ const out = {
2847
+ applicable: r.applicable === true, schema: r.schema || null,
2848
+ violations: r.violations || [], advisories: r.advisories || [],
2849
+ skipDesign: adoptedDesign, degraded: false, v1EmptySlots: [],
2850
+ }
2851
+ if (!r.applicable) {
2852
+ // draft-ledger 机本位缺失(跨机/存量变更):fail-open 降级——仅 v1(AGENT 槽形态)跑空槽
2853
+ // 兜底;v2 纯 markdown 无 ledger 无法对锚校验,如实降级(不假装能查)。
2854
+ out.degraded = true
2855
+ let v1Text = null
2856
+ try { v1Text = readFileSync(join(changeDir, 'design.md'), 'utf8') } catch { /* 无 design */ }
2857
+ if (v1Text && /<!--AGENT:槽\d+/.test(v1Text)) {
2858
+ const dr = verifyDesignRecordFilled({ changeDir })
2859
+ if (dr.applicable) out.v1EmptySlots = dr.emptySlots
2860
+ }
2861
+ }
2862
+ return out
2863
+ }
2864
+
2865
+ /**
2866
+ * flow verify-drafts(2026-10-23-flow-verify-drafts):spec 面起草自查只读命令——起草完
2867
+ * 先自查(与批准门同判定同源 computeSpecFaceChecks)、通过再请用户批准,把「用户跑 approve
2868
+ * 被 exit 1 硬拒」的往返消灭在 agent 侧。零写盘、零状态副作用(通过≠批准——批准仍走 approve
2869
+ * 留痕)。--json 与人类渲染同判定同退出码(对齐 flow status --json 双态同源先例)。
2870
+ */
2871
+ async function cmdFlowVerifyDrafts({ change, cwd, specBase, runtimeRootOpt = null, json = false }) {
2872
+ const changeDir = join(specBase, 'changes', change)
2873
+ if (!existsSync(changeDir)) {
2874
+ const msg = `❌ 变更不存在:${change}(flow verify-drafts 在 flow start 之后、approve 之前运行——起草面自查)`
2875
+ if (json) console.log(JSON.stringify({ change, status: 'missing' }))
2876
+ else console.error(msg)
2877
+ process.exit(2)
2878
+ }
2879
+ const r = await computeSpecFaceChecks({ changeDir, change, specBase, runtimeRootOpt })
2880
+ const hasViolations = r.applicable && r.violations.length > 0
2881
+ const hasEmptySlots = r.degraded && r.v1EmptySlots.length > 0
2882
+ const passed = !hasViolations && !hasEmptySlots
2883
+ if (json) {
2884
+ console.log(JSON.stringify({
2885
+ change, passed, applicable: r.applicable, schema: r.schema,
2886
+ violations: r.violations, advisories: r.advisories,
2887
+ skipDesign: r.skipDesign, degraded: r.degraded, v1EmptySlots: r.v1EmptySlots,
2888
+ }))
2889
+ if (!passed) process.exit(1) // 与人类渲染同退出码(json 不分叉)
2890
+ } else if (passed) {
2891
+ for (const a of r.advisories) console.warn(`⚠️ [门柱漂移 advisory] ${a}(不阻断批准)`)
2892
+ if (r.degraded) console.log(`ℹ️ 自查降级:draft-ledger 不在此机(.runtime 机本位/存量变更)——空槽兜底校验通过(v2 纯 markdown 无法对锚校验,如实降级)`)
2893
+ console.log(`✅ spec 面自查通过(0 violations)——通过≠批准:请把摘要给用户看,用户确认后 sillyspec flow approve --change ${change}(批准留痕后动码)`)
2894
+ } else {
2895
+ for (const a of r.advisories) console.warn(`⚠️ [门柱漂移 advisory] ${a}(不阻断批准)`)
2896
+ if (hasViolations) {
2897
+ console.error(`❌ spec 面自查未通过——${r.violations.length} 处违规(与 flow approve 批准门同判定;一次全列,修复后重跑本命令):`)
2898
+ for (const v of r.violations) console.error(` - ${v}`)
2899
+ }
2900
+ if (hasEmptySlots) {
2901
+ console.error(`❌ spec 面自查未通过——design.md 有 ${r.v1EmptySlots.length} 个空 AGENT 槽(${r.v1EmptySlots.join('、')}):每节至少一行(「不适用:<理由>」也算答)`)
2902
+ }
2903
+ console.error(` 修复后重跑:sillyspec flow verify-drafts --change ${change}——通过再请用户批准(flow approve),用户一次过`)
2904
+ process.exit(1)
2905
+ }
2906
+ return { change, passed }
2907
+ }
2908
+
2715
2909
  /**
2716
2910
  * flow approve(2026-10-04-thin-docs-v2 FR-06):spec 断点用户批准留痕——用户确认 FR+design
2717
2911
  * 方案后运行;写 flow-state {spec_approved,spec_approved_at,spec_approved_by}。flow done 的
@@ -2747,11 +2941,10 @@ async function cmdFlowApprove({ change, cwd, specBase, by = null, channelFlag =
2747
2941
  // 面只读,不做 design 问题行自动回填(写盘修复属 done 面)。诚实边界:本门证明「答在批前」,
2748
2942
  // 不证明「答在码前」——时序证据路线已实证不可行(批量收口下 git 时间戳说谎,thin-order-canon
2749
2943
  // 四路调查),不做假装能拦的检查。
2944
+ // 计算单一源 computeSpecFaceChecks(2026-10-23-flow-verify-drafts 抽取共享)——本块只保留
2945
+ // approve 面的渲染与退出,文案与退出码逐字保持(既有测试零改动作回归锚)。
2750
2946
  {
2751
- const runtimeRoot = resolveRuntimeRoot({}, specBase)
2752
- const { verifyFlowDrafts, verifyDesignRecordFilled } = await import('./flow-draft.js')
2753
- const adoptedDesign = st.adopted_from === 'brainstorm' && existsSync(join(changeDir, 'design.md'))
2754
- const r = verifyFlowDrafts({ changeDir, change, runtimeRoot, skipDesign: adoptedDesign, repair: false })
2947
+ const r = await computeSpecFaceChecks({ changeDir, change, specBase })
2755
2948
  for (const a of r.advisories || []) console.warn(`⚠️ [门柱漂移 advisory] ${a}(不阻断批准)`)
2756
2949
  if (r.applicable && r.violations.length > 0) {
2757
2950
  console.error(`❌ spec 断点未批准——文档未齐(批准是动码前置,先答 spec 面再批):`)
@@ -2762,15 +2955,10 @@ async function cmdFlowApprove({ change, cwd, specBase, by = null, channelFlag =
2762
2955
  if (!r.applicable) {
2763
2956
  // draft-ledger 机本位缺失(跨机批准/存量变更):fail-open 降级——仅 v1(AGENT 槽形态)跑
2764
2957
  // 空槽兜底;v2 纯 markdown 无 ledger 无法对锚校验,如实降级放行(不假装能查)。
2765
- let v1Text = null
2766
- try { v1Text = readFileSync(join(changeDir, 'design.md'), 'utf8') } catch { /* 无 design */ }
2767
- if (v1Text && /<!--AGENT:槽\d+/.test(v1Text)) {
2768
- const dr = verifyDesignRecordFilled({ changeDir })
2769
- if (dr.applicable && dr.emptySlots.length > 0) {
2770
- console.error(`❌ spec 断点未批准——design.md 有 ${dr.emptySlots.length} 个空 AGENT 槽(${dr.emptySlots.join('、')}):每节至少一行(「不适用:<理由>」也算答)`)
2771
- console.error(` 作答齐备后重跑:sillyspec flow approve --change ${change} [--user-confirmed|--self-approved]`)
2772
- process.exit(1)
2773
- }
2958
+ if (r.v1EmptySlots.length > 0) {
2959
+ console.error(`❌ spec 断点未批准——design.md 有 ${r.v1EmptySlots.length} 个空 AGENT 槽(${r.v1EmptySlots.join('、')}):每节至少一行(「不适用:<理由>」也算答)`)
2960
+ console.error(` 作答齐备后重跑:sillyspec flow approve --change ${change} [--user-confirmed|--self-approved]`)
2961
+ process.exit(1)
2774
2962
  }
2775
2963
  console.log(`ℹ️ 批准校验降级放行:draft-ledger 不在此机(.runtime 机本位/存量变更)——空槽兜底校验通过`)
2776
2964
  }
@@ -2962,6 +3150,15 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2962
3150
  await reportAgentLog(change, sub)
2963
3151
  return cmdFlowReject({ change, cwd, specBase, reason: getFlag('--reason') })
2964
3152
  }
3153
+ if (sub === 'verify-drafts') {
3154
+ // spec 面起草自查只读面(2026-10-23-flow-verify-drafts):与批准门同判定同源——起草完先
3155
+ // 自查、通过再请批准,用户被拒往返消灭在 agent 侧;零写盘零状态副作用。
3156
+ const change = getFlag('--change')
3157
+ if (!change) { console.error('❌ flow verify-drafts 需 --change <名>'); process.exit(2) }
3158
+ validateChangeName(change)
3159
+ await reportAgentLog(change, sub)
3160
+ return cmdFlowVerifyDrafts({ change, cwd, specBase, runtimeRootOpt, json: jsonGlobal || hasFlag('--json') })
3161
+ }
2965
3162
  if (sub === 'diff') {
2966
3163
  // 只读查询面(2026-10-13-flow-diff-cmd):change 粒度文件统计双态口——与 status 同级自愿面
2967
3164
  const change = getFlag('--change')
@@ -3152,7 +3349,7 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
3152
3349
  console.log(`✅ flow amend-draft 留痕重锚:${r.reanchored.join('、')}——ledger amendment 审计在案;editRatio=${maxRatio}(段级最大;基准=首版原文,AGENT 槽不计入)`)
3153
3350
  return r
3154
3351
  }
3155
- console.error('用法: sillyspec flow start --change <名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字)+动机与背景详述+独立一行『成功标准:』+每行一条『- 可验证标准』>" [--thick|--with-tasks] [--no-prestage(hindsight 标记在场时声明需求已含决策,留痕放行)] | sillyspec flow done --change <名> | sillyspec flow status --change <名> [--json] | sillyspec flow diff --change <名> [--json](只读文件统计)')
3352
+ console.error('用法: sillyspec flow start --change <名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字)+动机与背景详述+独立一行『成功标准:』+每行一条『- 可验证标准』>" [--thick|--with-tasks] [--no-prestage(hindsight 标记在场时声明需求已含决策,留痕放行)] | sillyspec flow done --change <名> | sillyspec flow status --change <名> [--json] | sillyspec flow verify-drafts --change <名> [--json](spec 面只读自查,通过再请批准) | sillyspec flow diff --change <名> [--json](只读文件统计)')
3156
3353
  console.error(' --input 可照抄形态(引号内换行合法):')
3157
3354
  console.error(' sillyspec flow start --change <名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字);随后动机与背景')
3158
3355
  console.error('')
package/src/fr-index.js CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  joinKnowledgeFile,
30
30
  syncIndexRoutingLines,
31
31
  discoverModuleIndex,
32
+ FR_ROUTE_STRIP_TOKENS,
32
33
  } from './decision-distill.js';
33
34
 
34
35
  /** epoch:此前归档不检查/不索引(存量 93 份不回填——不伪造历史,D-007)。 */
@@ -594,10 +595,13 @@ export function indexRequirements({ changeDir, knowledgeRoot, headHash = '', del
594
595
  section: 'FR 需求索引',
595
596
  subdir: FR_DIR,
596
597
  // 伪域路由行追加裸段关键词(auto-backend 行同时吃 'backend' 命中——域名字面作为
597
- // 唯一路由键对 auto- 域过窄,2026-09-21 unmapped 大池路由失效的教训)
598
+ // 唯一路由键对 auto- 域过窄,2026-09-21 unmapped 大池路由失效的教训)。
599
+ // 泛词退役(FR-02,2026-10-21-knowledge-tier-denoise):FR|需求|承接 不再全体域共享
600
+ // (查询含泛词命中全部 fr 行=注入席位行序抽签病根);存量行 stripTokens 幂等剥离。
601
+ stripTokens: FR_ROUTE_STRIP_TOKENS,
598
602
  makeLine: (d) => d.startsWith('auto-')
599
- ? `- ${d}|${d.slice(5)}|FR|需求|承接 → [${FR_DIR}/${d}.md](${FR_DIR}/${d}.md)`
600
- : `- ${d}|FR|需求|承接 → [${FR_DIR}/${d}.md](${FR_DIR}/${d}.md)`,
603
+ ? `- ${d}|${d.slice(5)} → [${FR_DIR}/${d}.md](${FR_DIR}/${d}.md)`
604
+ : `- ${d} → [${FR_DIR}/${d}.md](${FR_DIR}/${d}.md)`,
601
605
  });
602
606
  }
603
607
 
package/src/git-helper.js CHANGED
@@ -15,6 +15,7 @@
15
15
  import { execFileSync } from 'node:child_process'
16
16
  import { existsSync, readdirSync } from 'node:fs'
17
17
  import { join } from 'node:path'
18
+ import { createHash } from 'node:crypto'
18
19
 
19
20
  /**
20
21
  * git C 风格引号路径解码:`\\` `\"` `\n` `\t` 与 `\NNN` 八进制**原始字节**。
@@ -146,6 +147,87 @@ export function git(cwd, args, opts = {}) {
146
147
  return trim ? value.trim() : value
147
148
  }
148
149
 
150
+ /**
151
+ * 批量对象存在性判定(2026-10-21-protocol-tax task-07 / FR-07)。
152
+ *
153
+ * 单次 `git cat-file --batch-check`(SHA 列表走 stdin)替代逐 SHA `cat-file -e`——
154
+ * Windows 单次 git spawn ~100-150ms,归档锚点校验在 681 归档仓实测串行 25s+,批量化后
155
+ * 一次 spawn 判完全部。探针形态 `<sha>^{commit}` 与旧逐查同构(tag/tree 对象不算 commit)。
156
+ *
157
+ * @param {string} cwd
158
+ * @param {string[]} shas 40-hex SHA 列表(大小写不敏感,返回键统一小写)
159
+ * @returns {Map<string, boolean>} sha → 是否为可解析 commit;整批失败返回空 Map
160
+ * (调用方查表 miss = false = 不可解析 = 悬空——与逐查失败同向 fail,不静默洗白)
161
+ */
162
+ export function batchCatFileExists(cwd, shas) {
163
+ const map = new Map()
164
+ if (!Array.isArray(shas) || shas.length === 0) return map
165
+ const probes = shas.map((s) => `${s}^{commit}`)
166
+ try {
167
+ const out = execFileSync('git', buildFullArgs(cwd, ['cat-file', '--batch-check']), {
168
+ encoding: 'utf8', timeout: 30000, maxBuffer: GIT_MAX_BUFFER,
169
+ input: probes.join('\n') + '\n',
170
+ env: gitChildEnv(), windowsHide: true,
171
+ })
172
+ for (const line of String(out).split('\n')) {
173
+ // 行形态:可解析 → git 回显**解析后裸 SHA**(不回显输入串)`<sha> commit <size>`;
174
+ // 不可解析 → 回显输入串 `<sha>^{commit} missing`。统一剥后缀取裸 SHA 作键。
175
+ const m = line.match(/^([0-9a-f]{40})(?:\^\{commit\})?\s+(\S+)/i)
176
+ if (m) map.set(m[1].toLowerCase(), m[2] === 'commit')
177
+ }
178
+ } catch { /* 整批失败:空表全 miss=悬空(同逐查失败语义) */ }
179
+ return map
180
+ }
181
+
182
+ /**
183
+ * 批量暂存区 blob 内容哈希(2026-10-21-protocol-tax task-07 / FR-07 配套)。
184
+ *
185
+ * 单次 `git cat-file --batch`(stdin 逐行 `:path`)替代逐文件 `git show :path`——apply
186
+ * 交付指纹漂移维度对最近 5 份 manifest × 全部文件逐个起进程,Windows spawn 税可观。
187
+ * 响应按输入顺序回流:成功 `<oid> blob <size>\n<content>\n`、缺失 `<ref> missing\n`;
188
+ * 内容与写侧同基(staged blob 即 LF 规范态),sha256 直算 content 字节。
189
+ *
190
+ * @param {string} cwd
191
+ * @param {string[]} paths 仓库相对路径列表
192
+ * @returns {Map<string, string|null>} path → staged 内容 sha256(null=未暂存/不可解析);
193
+ * 整批失败返回空 Map(调用方查表 miss=null=未暂存,与逐查 gitQuiet null 同语义)
194
+ */
195
+ export function batchStageBlobSha256(cwd, paths) {
196
+ const map = new Map()
197
+ if (!Array.isArray(paths) || paths.length === 0) return map
198
+ let out
199
+ try {
200
+ out = execFileSync('git', buildFullArgs(cwd, ['cat-file', '--batch']), {
201
+ encoding: 'buffer', timeout: 30000, maxBuffer: GIT_MAX_BUFFER,
202
+ input: paths.map((p) => `:${p}`).join('\n') + '\n',
203
+ env: gitChildEnv(), windowsHide: true,
204
+ })
205
+ } catch { return map }
206
+ const buf = Buffer.isBuffer(out) ? out : Buffer.from(String(out), 'binary')
207
+ let pos = 0
208
+ let idx = 0
209
+ while (pos < buf.length && idx < paths.length) {
210
+ const nl = buf.indexOf(0x0a, pos)
211
+ if (nl === -1) break
212
+ const header = buf.toString('utf8', pos, nl).trim()
213
+ pos = nl + 1
214
+ const parts = header.split(/\s+/)
215
+ if (parts[parts.length - 1] === 'missing') {
216
+ map.set(paths[idx], null)
217
+ idx++
218
+ continue
219
+ }
220
+ const size = Number.parseInt(parts[parts.length - 1], 10)
221
+ if (!Number.isFinite(size) || pos + size > buf.length) break // 形态意外:截断防错读
222
+ map.set(paths[idx], createHash('sha256').update(buf.subarray(pos, pos + size)).digest('hex'))
223
+ pos += size + 1 // 内容 + 尾随换行
224
+ idx++
225
+ }
226
+ // 尾部未覆盖的路径(流截断等)按未暂存处理——与逐查失败同向
227
+ for (; idx < paths.length; idx++) map.set(paths[idx], null)
228
+ return map
229
+ }
230
+
149
231
  /**
150
232
  * 静默版 git:内部调 git,失败 catch 返回 null(对齐 worktree 本地 gitQuiet() 语义)。
151
233
  * ENOBUFS(输出超 GIT_MAX_BUFFER 上限)不是"无输出",静默成 null 会让调用方把大输出误当空清单——
package/src/handoff.js CHANGED
@@ -164,6 +164,10 @@ export async function buildHandoff({ cwd, specBase, changeName } = {}) {
164
164
  } else {
165
165
  lines.push('# (流程终态,无续跑命令)');
166
166
  }
167
+ // 所有权接缝预告(2026-10-21-p0-p1-hardening FR-05 / dsh z2 实证:新会话自拟新 SESSION_ID
168
+ // 而非沿用交接块导出的旧 ID 时,首个写操作必撞 15 分钟活跃窗被迫 --takeover):粘贴块内显式
169
+ // 给出出路,消「交接块没预告接管」的设计缺口。
170
+ lines.push('# ⚠️ 若换用新的 SILLYSPEC_SESSION_ID(不推荐,保持上方导出值即可):首个写操作(--done/--answer 等)会撞所有权活跃窗——等窗过期自动接管,或首写带 --takeover 显式接管留痕');
167
171
  lines.push('# ── 交接块结束 ──');
168
172
  lines.push('');
169
173
  lines.push(`📋 ${target}:当前 ${STAGE_LABELS[currentStage] || currentStage}${steps.length > 0 ? `(步骤 ${doneSteps}/${steps.length})` : ''}${stageCompleted ? ' ✅ 已完成' : ''}`);
@@ -67,11 +67,14 @@ const STAGE_HINTS = {
67
67
  ' - <可验证标准>"',
68
68
  ' 逻辑变更(skill sillyspec-brainstorm):sillyspec run brainstorm → plan → execute → verify → archive',
69
69
  ' 全自动模式(skill sillyspec-auto):sillyspec run auto "任务描述"',
70
+ '',
71
+ '若在头脑风暴阶段做真原型(变更 key 已出):sillyspec worktree create <变更名> 进隔离树写原型,',
72
+ '已在 worktree 内则复用当前目录不重复创建;文档照常主树(worktree 内运行 sillyspec 自动锚回主仓根)。',
70
73
  ],
71
74
  'brainstorm': [
72
- '当前在 brainstorm(需求分析)阶段,这个阶段只写文档,不写代码。',
73
- '完成 brainstorm 后,流程会自动推进到 plan → execute。',
74
- 'execute 阶段才允许写代码。',
75
+ '当前在 brainstorm(需求分析)阶段:spec 文档写主树;做真原型须先进隔离 worktree——变更 key 已出,运行 sillyspec worktree create <变更名>(已在 worktree 内则复用当前目录,不重复创建),原型代码写 worktree 内。',
76
+ '文档不受影响:worktree 内运行 sillyspec 会自动锚回主仓根,文档照常落主树。',
77
+ '完成 brainstorm 后,流程会自动推进到 plan → execute;execute 会复用已建的 worktree。',
75
78
  ],
76
79
  'plan': [
77
80
  '当前在 plan(计划制定)阶段,这个阶段只写计划文档和任务蓝图,不写代码。',
@@ -805,6 +808,12 @@ export function shouldBlockWrite(filePath, cwd) {
805
808
  // 2. 文件门禁:文档类/配置类始终放行,但 worktree 存储区内的源码必须继续走登记校验。
806
809
  if (!isInsideWorktreeStorage(absPath, projectRoot) && matchFileWhitelist(absPath)) return { blocked: false }
807
810
 
811
+ // 2.5 位置豁免(2026-10-21-brainstorm-proto-worktree):已注册 worktree(meta.json 登记)
812
+ // 是隔离区——路径落在其内即放行,不问阶段,与 Bash 侧 worktree-cwd 全放行对称。
813
+ // 头脑风暴期做真原型的协议出口:变更 key 已出 → sillyspec worktree create → 进树写原型。
814
+ // 未登记的 worktrees 存储区子目录不沾此豁免(下方阶段门照拦)。
815
+ if (isInsideRegisteredWorktree(absPath, projectRoot)) return { blocked: false }
816
+
808
817
  if (!['execute', 'quick'].includes(stage)) {
809
818
  return {
810
819
  blocked: true,