sillyspec 3.33.4 → 3.33.6

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.
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
 
@@ -289,6 +297,29 @@ export async function flowKnowledgeDigest({ specBase, change, changeDir, input,
289
297
  summary.domains = domains
290
298
  summary.frCount = frs.length
291
299
  lines.push(`🧠 知识注入(轻量道读取面——现行 FR/否决决策/已知坑,动手前扫一眼):`)
300
+ const scopedLines = []
301
+ // 作用域注入(2026-10-23-knowledge-scope-inject):①工具内置中立性行恒注入(规约 00——
302
+ // 不依赖仓侧配置:语言锁定类设计错误在决策时点被提醒,2026-10-23 实证驱动的内置面);
303
+ // ②always/repo 条目恒注入(帽 5 条+溢出计数);③module:<域> 条目仅触达域命中时注入;
304
+ // ④无标记条目走关键词面(零改动)。解析/读取 fail-soft 零输出。
305
+ try {
306
+ const { collectScopedKnowledge } = await import('./knowledge-scope.js')
307
+ const scoped = collectScopedKnowledge(knowledgeRoot)
308
+ const alwaysRepo = [...scoped.always, ...scoped.repo]
309
+ summary.scopedEntries = alwaysRepo.length
310
+ scopedLines.push(' 🧭 工具中立性(内置规约 00,每变更恒注入):不枚举定义开放世界、不锁死开发语言/框架——配置与机制用 CLI 自有分类(批类别/路径集合),新增语言只加探测适配。')
311
+ for (const e of alwaysRepo.slice(0, 5)) {
312
+ scopedLines.push(` 📌 [${e.scope}] ${e.id}:${e.summary}`)
313
+ }
314
+ if (alwaysRepo.length > 5) scopedLines.push(` (+${alwaysRepo.length - 5} 条恒注入条目见 knowledge/)`)
315
+ const moduleEntries = []
316
+ for (const d of domains) for (const e of (scoped.modules[d] || [])) moduleEntries.push({ ...e, domain: d })
317
+ for (const e of moduleEntries.slice(0, 3)) {
318
+ scopedLines.push(` 📌 [module:${e.domain}] ${e.id}:${e.summary}`)
319
+ }
320
+ if (moduleEntries.length > 3) scopedLines.push(` (+${moduleEntries.length - 3} 条触达域作用域条目见 knowledge/)`)
321
+ lines.push(...scopedLines)
322
+ } catch { /* 作用域注入 fail-soft:FR/关键词面不受影响 */ }
292
323
  if (domains.length === 0) {
293
324
  lines.push(` 触达域:起点无依据(--input 无路径语料、无 design 清单)——干活涉及 src 后 resume/收口会补查;`)
294
325
  lines.push(` 相关域现行 FR 可自行查 knowledge/fr/<域>.md(INDEX.md 有路由)。`)
@@ -359,6 +390,7 @@ export async function flowKnowledgeDigest({ specBase, change, changeDir, input,
359
390
  if (domains.length === 0 && rejected.length === 0 && !km.matched) {
360
391
  lines.length = 0
361
392
  lines.push(`🧠 知识注入(轻量道读取面):语料未命中知识库(无触达域依据、无否决命中、INDEX 无对齐条目)——可自行查 knowledge/INDEX.md。`)
393
+ if (scopedLines.length > 0) lines.push(...scopedLines) // 恒注入面不被空态折叠吞(2026-10-23-knowledge-scope-inject)
362
394
  }
363
395
  // 向量开关发现性提示(2026-10-15-vector-switch-discoverability):零命中=正需要语义召回
364
396
  // 而未获得的时刻——平台已连接但开关未开(缺省关)时给一行可执行提示;有命中/未连接/
@@ -630,7 +662,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
630
662
  const _localYaml = join(specBase, 'local.yaml')
631
663
  if (!existsSync(_localYaml) && !existsSync(join(specBase, 'changes', change))) {
632
664
  console.error('❌ local.yaml 不存在——先跑 sillyspec init(mode/lint/known_failures 等 init 级配置的落点)。')
633
- console.error(' 测试面无需在此配置(2026-09-26 起:测试门按变更动态推断——本变更测试 ∪ FR 关联回归 ∪ import 依赖,')
665
+ console.error(' 测试面无需在此配置(测试门按变更动态推断——本变更测试 ∪ import 依赖;FR 关联回归层缺省不跑归 pre-push/CI 门(2026-10-23-fr-regress-default-off),fr_regress_cap: N|all 可开启,')
634
666
  console.error(' runner 自项目结构 pyproject/package.json 推断;modules.*.test/commands.test 已退役,后者仅显式 test_strategy: full 生效)。')
635
667
  process.exit(2)
636
668
  }
@@ -1136,7 +1168,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
1136
1168
  `📜 requirements 每条 FR = 标题锚 + 行为句 + 按需场景块(#### 场景:名 + Given/When/Then);`,
1137
1169
  ` 测试绑定节每条 FR 一行 \`FR-NN: test/路径「用例」\`(无测试面写「不适用:理由」)。`,
1138
1170
  `📦 冻结面在 flow done 时点采集(baseline..HEAD 提交面):未提交交付文件——会话专属 worktree 自动并入;`,
1139
- ` 共享主仓可带 --freeze-dirty 显式声明并入;git 中间提交归档后可 reset --soft 压扁为单提交`,
1171
+ ` 共享主仓可带 --freeze-dirty 显式声明并入;git 中间提交归档后用 sillyspec push 压扁并推送`,
1140
1172
  ` (审计真相在 change.patch 冻结件 sha256 锚定,不依赖 git 历史形态)。`,
1141
1173
  `⚖️ 独立评审定档(flow done 按危险证据判,不看文件数):高危承诺词/盲维实质作答/diff 危险`,
1142
1174
  ` 原语/决策密度任一命中即需评审(届时会收到评审任务书,起子代理产出 review.json);豁免`,
@@ -1158,6 +1190,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
1158
1190
  ``,
1159
1191
  `🛑 三断点纪律(顺序纪律:①未过不动码;②③为汇报点——用户没说「全跑完」就必须在每个断点向用户汇报并等确认):`,
1160
1192
  ` ① spec 断点【机器门】:填完 FR 区和 design 四节后,把摘要给用户看(FR 条目+盲维作答+方案概述),`,
1193
+ ` 起草完先自查:sillyspec flow verify-drafts --change ${change}(与批准门同判定——自查通过再请批准,用户一次过);`,
1161
1194
  ` 用户确认后请其运行 sillyspec flow approve --change ${change}(批准留痕)再动手写代码——`,
1162
1195
  ` agent 代跑时如实声明通道:--user-confirmed(转述用户已确认)/ --self-approved(agent 自批),`,
1163
1196
  ` 无 flag 按 unattested 留痕(审计面无法区分用户确认与自批)——2026-10-12-approve-channel-attest。`,
@@ -1268,7 +1301,35 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1268
1301
  }
1269
1302
  // 极速模式读态(2026-10-14-thin-turbo-mode FR-02/D-007):turbo && tier!=='thick' 双条件——
1270
1303
  // 升厚(distill 异态/verify 失败/--thick 起步)后 turbo 残留不短路厚档仪式,重入即恢复全量校验
1271
- const turbo = st.turbo === true && st.tier !== 'thick'
1304
+ // turbo 双源(2026-10-23-turbo-docs-auto):旗标档(--turbo 用户显式)原样;未开且仓主常设
1305
+ // 授权 turbo: docs-auto 时按面自动降档——面=changedFilesSinceBaseline(提交∪脏面)经
1306
+ // dirty_gate_ignore 前缀剔除(与 dirty 门同一残渣口径)后非空且无文件命中代码分类器
1307
+ // (quick-audit 单一源)。fail-closed:面空/含代码/读取或判定异常一律走全量(不静默降档);
1308
+ // 升厚即失效对双源同效(tier=thick 关)。授权权在仓主(local.yaml 一行),agent 仍无法自选。
1309
+ const turboFlag = st.turbo === true && st.tier !== 'thick'
1310
+ let turbo = turboFlag
1311
+ let turboSource = turboFlag ? 'flag' : null
1312
+ if (!turbo && st.tier !== 'thick') {
1313
+ try {
1314
+ if (readFlowConfig(specBase).turboDocsAuto === true) {
1315
+ const { readDirtyGateIgnore } = await import('./flow-parity.js')
1316
+ const { CODE_PATH_SEGMENT_RE, CODE_EXTENSION_RE } = await import('./run/quick-audit.js')
1317
+ const ignorePfx = readDirtyGateIgnore(specBase)
1318
+ const face = (changedFilesSinceBaseline(cwd, st.baseline_commit) || [])
1319
+ .filter((f) => !ignorePfx.some((p) => f.startsWith(p)))
1320
+ const docsOnly = face.length > 0 && !face.some((f) => CODE_PATH_SEGMENT_RE.test(f) || CODE_EXTENSION_RE.test(f))
1321
+ if (docsOnly) {
1322
+ turbo = true
1323
+ turboSource = 'docs-auto'
1324
+ console.log(`⚡ turbo(docs-auto):提交/冻结面纯文档(${face.length} 文件)——实测/评审/UI 探针按极速档跳过(授权源 local.yaml turbo: docs-auto;升厚即失效)`)
1325
+ }
1326
+ }
1327
+ } catch { /* 面判定异常 fail-closed 走全量 */ }
1328
+ }
1329
+ // 实测门 reason 的授权源标签(防文案漂移:docs-auto 档不得显示「用户显式触发」)
1330
+ const turboLabel = turboSource === 'docs-auto'
1331
+ ? 'local.yaml turbo: docs-auto 常设授权(提交/冻结面纯文档)'
1332
+ : `用户显式触发 ${st.turbo_since || '时点未记'}`
1272
1333
  const runtimeRoot = resolveRuntimeRoot(runtimeRootOpt ? { runtimeRoot: runtimeRootOpt } : {}, specBase)
1273
1334
  // 归属收窄清单(2026-09-25-thin-dogfood-fixes 修复②):baseline..HEAD 不分作者——并行会话在
1274
1335
  // start..done 之间的提交会混入本变更实测面与 FR 域路由。复用 verify 对账同款切分器:他侧显式
@@ -1377,7 +1438,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1377
1438
  if (dr.applicable && dr.emptySlots.length > 0) {
1378
1439
  console.error(`❌ 设计记录未作答:design.md 有 ${dr.emptySlots.length} 个空 AGENT 槽(${dr.emptySlots.join('、')})`)
1379
1440
  console.error(` 每节至少写一行(小改动可写「不适用:<理由>」)——设计承诺是评审与 FR 对账的锚点,空槽=承诺未落盘`)
1380
- console.error(` 恢复指引:槽标记(<!--AGENT:槽N)被删时,从 .runtime/step-guides/ 的指纹缓存可找骨架原文;或删 design.md 后重入 flow start 补生成(criteria 从 proposal 回提)`)
1441
+ 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
1442
  reportMidFail('artifacts')
1382
1443
  process.exit(1)
1383
1444
  }
@@ -1578,13 +1639,49 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1578
1639
  }
1579
1640
  }
1580
1641
  } catch { /* 节奏门 best-effort:读流失败静默(fail-open——观测缺席不阻断) */ }
1642
+ // ── 收口预检(fail-together 一次报全,2026-10-23-flow-done-preflight):实测门前只读收集
1643
+ // 便宜阻断条件(dirty-gap/orphan 预判)——56% 触发率的 dirty 门不再每轮先付完整实测;
1644
+ // 命中即拦在付费实测之前,全部条件+处置出路一次报全(b1ab65b2「两条修复一次给全」
1645
+ // 模式升格)。全过静默零行为变化;判定与权威门同源同旗标镜像(collectCloseoutPreflight
1646
+ // fail-safe:异常静默放行,权威门仍在原子步原序)。
1647
+ {
1648
+ const { collectCloseoutPreflight } = await import('./flow-preflight.js')
1649
+ const pre = await collectCloseoutPreflight({ cwd, specBase, change, st, freezeDirty, acceptDirtyGap, acceptOrphanFace, turbo })
1650
+ if (pre.findings.length > 0) {
1651
+ console.error(`⛔ 收口预检未过——${pre.findings.length} 项阻断条件(fail-together 一次报全;实测未启动,处置后重跑同一条命令从断点续):`)
1652
+ for (const f of pre.findings) {
1653
+ if (f.id === 'dirty-gap') {
1654
+ // 逐字复用 patch 子步权威门文案(防两路口径漂移——文案即契约面)
1655
+ console.error(` ▸ ${f.count} 个未提交交付文件未入冻结面(共享主仓无法归属)——三选一:`)
1656
+ console.error(`⛔ 未提交交付缺口未显式处置——收口阻断(不归档;处置后重跑同一条命令从断点续):`)
1657
+ console.error(` ① git commit 这些文件(信息尾缀带 (${change}) 或 (${change})),重跑本命令——冻结面自动并入`)
1658
+ console.error(` ② 全部归属本变更:flow done --change ${change} --freeze-dirty`)
1659
+ console.error(` ③ 接受审计缺口:flow done --change ${change} --accept-dirty-gap`)
1660
+ console.error(` 💡 快速通道:脏文件全属并行会话在途/运行残渣且不在本变更交付面时,直接重跑带 --accept-dirty-gap(显式 pathspec 提交面已隔离归属;属本变更的文件必须走①提交)`)
1661
+ console.error(` ④ 已知残渣固定忽略:.sillyspec/local.yaml 加 dirty_gate_ignore: <目录前缀>(单行逗号分隔,同 module_gate_ignore)`)
1662
+ console.error(` ${f.files.slice(0, 3).join('、')}${f.files.length > 3 ? ' 等' : ''}`)
1663
+ } else if (f.id === 'orphan-face') {
1664
+ // 逐字复用 patch 子步权威门文案(防两路口径漂移——文案即契约面)
1665
+ console.error(` ▸ 会话专属 worktree 冻结面外交付改动——三选一:`)
1666
+ console.error(`⛔ 会话专属 worktree 存在冻结面外交付改动:${f.commits.length} 个提交不在 HEAD 祖先链(patch-id 不等价)触及 ${f.count} 个交付文件——收口阻断(不归档;处置后重跑同一条命令从断点续):`)
1667
+ console.error(` 提交:${f.commits.slice(0, 5).map((c) => c.slice(0, 10)).join('、')}${f.commits.length > 5 ? ' 等' : ''}`)
1668
+ console.error(` 文件:${f.files.slice(0, 5).join('、')}${f.files.length > 5 ? ' 等' : ''}`)
1669
+ console.error(` ① git rebase/cherry-pick 这些提交到 HEAD(补齐冻结面)后重跑`)
1670
+ console.error(` ② 确认放弃这些提交:清理对应分支/reflog 源后重跑`)
1671
+ console.error(` ③ 接受缺口留痕:flow done --change ${change} --accept-orphan-face`)
1672
+ }
1673
+ }
1674
+ reportMidFail('patch')
1675
+ process.exit(1)
1676
+ }
1677
+ }
1581
1678
  // 极速模式实测门跳过(2026-10-14-thin-turbo-mode FR-02/D-002):turbo 在场不调
1582
1679
  // runQuickTestLintGate,合成 gate 同形状 skipped 对象(带 status:'skipped' 完整形态——
1583
1680
  // 下方 fmt 对 null 显示 '—',带形态才输出「skipped — 跳过原因」,审计可见;
1584
1681
  // 对齐 quick-audit env 逃生门 skip 形态先例 src/run/quick-audit.js:482)。
1585
1682
  // 失败升厚分支(下方 action==='fail')turbo 下天然不触达——升厚语义不变。
1586
1683
  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 } }
1684
+ ? { 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
1685
  : await runQuickTestLintGate({ cwd, specBase, changedFiles, changeName: change, skipSentinel: true /* flow 侧哨兵带 baseline 区间,quick 侧区间不可靠——单判不双判 */ })
1589
1686
  if (gate && gate.action === 'fail') {
1590
1687
  console.error(`❌ 测试门 FAIL(整单 FAIL——实测失败/超时=失败,不继续 distill/归档):`)
@@ -1787,13 +1884,31 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1787
1884
  // P2 文档措辞也付整轮子代理评审费(循环,被迫手工重排执行顺序自赎)。
1788
1885
  console.log(` review.json 保留不隔离——窗口内本变更提交仅触及治理文件(${drift.ownFiles.length} 个,未触 requirements/design),评审对象(交付面)未变,结论对最新面等价;change.patch 自动重冻结(治理工件不入冻结面,治理修复随归档目录本体留档)`)
1789
1886
  } else {
1790
- const ts = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, '')
1887
+ // 交付面锚定(2026-10-24-flow-done-review-artifact-drift-loop,坑实证三连循环):
1888
+ // 复审锚之后的 own 提交只碰治理文件(review.json 自身提交的典型形态)→ 评审对象
1889
+ // (交付面)自锚点起未变,保留。堵两保留路径的复合盲区:reviewAnchoredToHead 要求
1890
+ // 锚==当前 HEAD(隔着 review.json 提交即恒 false);governanceOnly 聚合整个冻结窗
1891
+ // (复审已覆盖的更早修复提交把 ownFiles 拖进交付文件 → 恒 false)→ 隔离重评 →
1892
+ // 重评再提交 review.json 又前移……循环。度量单位从窗口换成锚点:锚后才是复审未见。
1893
+ let _deliveryAnchored = null
1791
1894
  try {
1792
- renameSync(reviewPath, `${reviewPath}.superseded-${ts}`)
1793
- console.warn(` 旧 review.json 已隔离为 review.json.superseded-${ts}(评审结论对着旧冻结面)——重新执行评审任务书后再收口`)
1794
- } catch (e) {
1795
- quarantineOk = false
1796
- console.error(` ⚠️ 旧 review.json 隔离失败(${e.message})——本次保持现状幂等跳过,手动删除/改名 ${reviewPath} 后重跑(将自动重冻结+重评)`)
1895
+ const { extractAnchorSha } = await import('./flow-review.js')
1896
+ const { deliveryFaceAnchoredAfter } = await import('./flow-parity.js')
1897
+ const _raw = JSON.parse(readFileSync(reviewPath, 'utf8'))
1898
+ const _anchor = extractAnchorSha(_raw && _raw.reviewedAgainst)
1899
+ _deliveryAnchored = _anchor ? deliveryFaceAnchoredAfter({ cwd, change, anchorSha: _anchor }) : null
1900
+ } catch { /* 锚读取/判定失败 → 走隔离(fail-safe 宁可多评) */ }
1901
+ if (_deliveryAnchored && _deliveryAnchored.ok) {
1902
+ console.log(` review.json 保留不隔离——复审锚(reviewedAgainst)之后的本变更提交仅触及治理文件(${_deliveryAnchored.ownCount} 个提交,未触 requirements/design),评审对象(交付面)自锚点起未变,结论有效;change.patch 自动重冻结(治理工件不入冻结面)`)
1903
+ } else {
1904
+ const ts = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, '')
1905
+ try {
1906
+ renameSync(reviewPath, `${reviewPath}.superseded-${ts}`)
1907
+ console.warn(` 旧 review.json 已隔离为 review.json.superseded-${ts}(评审结论对着旧冻结面)——重新执行评审任务书后再收口`)
1908
+ } catch (e) {
1909
+ quarantineOk = false
1910
+ console.error(` ⚠️ 旧 review.json 隔离失败(${e.message})——本次保持现状幂等跳过,手动删除/改名 ${reviewPath} 后重跑(将自动重冻结+重评)`)
1911
+ }
1797
1912
  }
1798
1913
  }
1799
1914
  }
@@ -1902,9 +2017,14 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1902
2017
  if (freeze.dirtyAdded.length > 0) {
1903
2018
  console.log(`🔒 ${exclusiveFrom === 'flag' ? '--freeze-dirty 显式声明' : '会话专属 worktree'}:${freeze.dirtyAdded.length} 个未提交交付文件一并入冻结面`)
1904
2019
  }
2020
+ // 已知残渣剔除留痕(2026-10-23-dirty-gate-ignore):collectFreezeFiles 单一源剔除
2021
+ // dirty_gate_ignore 命中面——每轮一行可审计(配了什么、剔了几个),预检同源继承。
2022
+ if (Array.isArray(freeze.dirtyIgnored) && freeze.dirtyIgnored.length > 0) {
2023
+ console.log(`ℹ️ ${freeze.dirtyIgnored.length} 个 dirty 文件按 local.yaml dirty_gate_ignore 剔除(已知残渣面,不入冻结缺口):${freeze.dirtyIgnored.slice(0, 3).join('、')}${freeze.dirtyIgnored.length > 3 ? ' 等' : ''}`)
2024
+ }
1905
2025
  if (freeze.dirtyWarned.length > 0) {
1906
2026
  console.warn(`⚠️ ${freeze.dirtyWarned.length} 个未提交交付文件未入冻结面(共享主仓无法归属)——三选一:`)
1907
- console.warn(` ① 先提交这些文件(信息尾缀带变更名)后重跑 ② 确认全部归属本变更:重跑 flow done --freeze-dirty 并入冻结 ③ 确认接受审计缺口:重跑 flow done --accept-dirty-gap(缺口数随 change-patch.json 留痕)。Git 中间提交可归档后压扁(见收口指引)`)
2027
+ console.warn(` ① 先提交这些文件(信息尾缀带变更名)后重跑 ② 确认全部归属本变更:重跑 flow done --freeze-dirty 并入冻结 ③ 确认接受审计缺口:重跑 flow done --accept-dirty-gap(缺口数随 change-patch.json 留痕)。Git 中间提交归档后用 sillyspec push 压扁并推送(见收口指引)`)
1908
2028
  console.warn(` ${freeze.dirtyWarned.slice(0, 3).join('、')}${freeze.dirtyWarned.length > 3 ? ' 等' : ''}`)
1909
2029
  // 收口阻断门(2026-10-08-thin-done-dirty-gate-and-paren-attribution,坑①修复:
1910
2030
  // multi-agent-platform docs/sillyspec/thin-done-src-commit-order-and-attribution-paren.md)
@@ -1922,6 +2042,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1922
2042
  console.error(` ② 全部归属本变更:flow done --change ${change} --freeze-dirty`)
1923
2043
  console.error(` ③ 接受审计缺口:flow done --change ${change} --accept-dirty-gap`)
1924
2044
  console.error(` 💡 快速通道:脏文件全属并行会话在途/运行残渣且不在本变更交付面时,直接重跑带 --accept-dirty-gap(显式 pathspec 提交面已隔离归属;属本变更的文件必须走①提交)`)
2045
+ console.error(` ④ 已知残渣固定忽略:.sillyspec/local.yaml 加 dirty_gate_ignore: <目录前缀>(单行逗号分隔,同 module_gate_ignore)`)
1925
2046
  reportMidFail('patch')
1926
2047
  process.exit(1)
1927
2048
  }
@@ -1984,7 +2105,10 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1984
2105
  // 首段命中注册仓的条目额外标注 key,点名+三种正确语法指引。
1985
2106
  const _declared = []
1986
2107
  for (const e of planEntries) {
1987
- const pe = String((e && e.path) || '').split('\\').join('/')
2108
+ // NEW: 前缀剥除(坑 flow-done-new-prefix-crossrepo-ambiguity,scope-audit.js:1263 同款
2109
+ // 口径):新建声明整串 NEW:<path> 当路径查主仓必 miss → 误落「疑似跨仓」;剥后存在性
2110
+ // 核验对真实路径(NEW 文件已提交则在盘),NEW 语义留给 design 门禁的原文校验。
2111
+ const pe = String((e && e.path) || '').split('\\').join('/').replace(/^NEW:/, '')
1988
2112
  if (!pe || pe.startsWith('.sillyspec/')) continue
1989
2113
  if (e.repo) continue
1990
2114
  _declared.push({ path: pe })
@@ -2282,7 +2406,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2282
2406
  const designPath = join(changeDir, 'design.md')
2283
2407
  if (existsSync(designPath)) {
2284
2408
  const { parseFileChangeListDetailed } = await import('./change-list.js')
2285
- const declared = parseFileChangeListDetailed(designPath).map((e) => e.path)
2409
+ const declared = parseFileChangeListDetailed(designPath).map((e) => String(e.path).replace(/^NEW:/, ''))
2286
2410
  const frozenSet = new Set(ownFiles.map((f) => String(f).replace(/\\/g, '/')))
2287
2411
  const missing = declared.filter((p) => !frozenSet.has(p) && !p.startsWith('.sillyspec/'))
2288
2412
  if (missing.length > 0) {
@@ -2309,17 +2433,18 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2309
2433
  // ②c review:危险证据定档的独立评审(2026-09-25-thin-review-slice)。定档=证据累积制
2310
2434
  // (承诺词一票/盲维实质作答/diff 原语/决策密度/声明一票),文件数出局;缺省要评审、豁免要
2311
2435
  // 多证并举、豁免者 1/4 定额抽查采样。评审任务书由 CLI 渲染(agent 起干净上下文子代理执行,
2312
- // 协议调用数不变);FAIL 或 P1 发现=拦截(修复后删件重评再重跑 done,断点续)。
2436
+ // 协议调用数不变);FAIL 或开放 P1 发现=拦截(修复后删件重评再重跑 done,断点续——开放判定
2437
+ // 见 flow-review.js isOpenFinding,2026-10-24-flow-done-review-findings-status-blind)。
2313
2438
  if (st.substeps?.review === 'done') {
2314
2439
  skip('review')
2315
2440
  // 回填(修评审 P2②:续跑成功轮遥测不失忆——review 已发生但子步已标,从留档件回读结论)
2316
2441
  try {
2317
- const { validateReviewJson } = await import('./flow-review.js')
2442
+ const { validateReviewJson, isOpenFinding } = await import('./flow-review.js')
2318
2443
  const rp = join(changeDir, 'review.json')
2319
2444
  if (existsSync(rp)) {
2320
2445
  const v = validateReviewJson(rp)
2321
2446
  reviewOutcome = v.ok
2322
- ? { required: true, sampled: false, verdict: v.review.verdict, findingsP1: (v.review.findings || []).filter((f) => /^P1$/i.test(String(f.severity || ''))).length, backfilled: true }
2447
+ ? { required: true, sampled: false, verdict: v.review.verdict, findingsP1: (v.review.findings || []).filter((f) => /^P1$/i.test(String(f.severity || '')) && isOpenFinding(f)).length, backfilled: true }
2323
2448
  : { required: true, sampled: false, verdict: 'invalid', backfilled: true }
2324
2449
  } else {
2325
2450
  reviewOutcome = { required: false, sampled: false, verdict: 'exempt', backfilled: true }
@@ -2351,6 +2476,46 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2351
2476
  } catch { /* 无/坏 JSON → 保持 null(既有判定) */ }
2352
2477
  }
2353
2478
  const tier = classifyReviewNeed({ changeDir, input: null, patchText, editRatio: st.edit_ratio ?? null, reviewForce: st.review_force ?? null, change })
2479
+ // 登记块单一源(2026-10-23-review-inline-dispatch):子代理重跑路径与平台 inline 回收
2480
+ // 路径同源消费——validateReviewJson+FAIL/P1 拦截+P2/P3 留债+PASS mark,无第二套判定。
2481
+ const registerReviewOutcome = async () => {
2482
+ const v = validateReviewJson(join(changeDir, 'review.json'))
2483
+ if (!v.ok) {
2484
+ console.error(`❌ review.json 校验失败(${v.errors.join(';')})——按任务书 schema 修正后重跑`)
2485
+ reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'invalid' }
2486
+ appendTelemetry(reviewOutcome) // 修评审 P2①
2487
+ reportMidFail('review')
2488
+ process.exit(1)
2489
+ }
2490
+ // 开放 P1 才阻断(2026-10-24-flow-done-review-findings-status-blind,坑实证):增量复审
2491
+ // 保留历史条目并标 status(resolved/resolved-as-accepted/accepted/false-positive)——
2492
+ // 旧口径 findings.some(P1) 盲判 status,把 verdict=PASS 的增量复审恒拦在收口门。
2493
+ const { isOpenFinding } = await import('./flow-review.js')
2494
+ const p1s = (v.review.findings || []).filter((f) => /^P1$/i.test(String(f.severity || '')) && isOpenFinding(f))
2495
+ const others = (v.review.findings || []).filter((f) => !p1s.includes(f))
2496
+ if (v.review.verdict === 'FAIL' || p1s.length > 0) {
2497
+ console.error(`❌ 独立评审未过(verdict=${v.review.verdict},开放 P1 发现 ${p1s.length} 项)——承诺与实现不一致,不许归档:`)
2498
+ for (const f of [...p1s, ...others]) console.error(` [${f.severity}${f.status ? `·${f.status}` : ''}] ${f.title} — ${f.evidence || ''}${f.location ? `(${f.location})` : ''}`)
2499
+ console.error(` 修复后删除 review.json 并重新执行评审任务书(起子代理重评),再重跑 flow done(断点续)`)
2500
+ console.error(` (增量复审提示:已解决/接受的历史条目保留并标 status=resolved/resolved-as-accepted/accepted/false-positive——阻断只计开放项,勿反复改 verdict)`)
2501
+ reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'FAIL', findingsP1: p1s.length }
2502
+ appendTelemetry(reviewOutcome) // 修评审 P2①:sampled+FAIL 恰是误豁免率校准的最关键信号
2503
+ reportMidFail('review')
2504
+ process.exit(1)
2505
+ }
2506
+ if (others.length > 0) {
2507
+ console.warn(`⚠️ 评审非阻断发现 ${others.length} 项(P2/P3,随归档留档):`)
2508
+ for (const f of others) console.warn(` [${f.severity}] ${f.title} — ${f.evidence || ''}`)
2509
+ // 处置提示(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop):
2510
+ // P2/P3 非阻断可留债归档;要修则修复面决定是否重评——只碰治理文件不动本变更
2511
+ // requirements/design 的后缀提交触发治理面等价保留(评审保留+自动重冻结,零重评);
2512
+ // 触及交付面或承诺面才隔离重评(任务书带前轮 findings,复审只验修复+新增)。
2513
+ console.warn(` 💡 处置参考:P2/P3 非阻断,可留债归档;要修——只碰治理文件(.sillyspec/**,不动本变更 requirements/design)的提交不触发重评;触及交付面或 requirements/design 需重评(复审只验修复+新增,非全量首评)`)
2514
+ }
2515
+ console.log(`✅ 独立评审通过(reviewer=${v.review.reviewer}${tier.sampled ? ',豁免抽查采样命中' : ''})`)
2516
+ reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'PASS', findingsP1: 0 }
2517
+ mark('review')
2518
+ }
2354
2519
  if (!tier.required) {
2355
2520
  console.log(`⚖️ 评审豁免(低风险证据齐全):${tier.exemptEvidence.join(';')}`)
2356
2521
  reviewOutcome = { required: false, sampled: false, verdict: 'exempt' }
@@ -2361,9 +2526,17 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2361
2526
  // 不再被漂移重冻结误隔离(实测竞态修复)。rev-parse 失败传 null(任务书退引导形态)。
2362
2527
  let taskbookHead = null
2363
2528
  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`)
2529
+ // 平台单调用通道(2026-10-23-review-inline-dispatch):review_inline_wait 键门控——键有效
2530
+ // 先试 inline(派发→有界等待→回收→同调用登记,missing→PASS 往返消灭);任何失败回退
2531
+ // 现状任务书面(零治理损失)。键 off 时本分支与改动前逐字一致。
2532
+ const inlineWaitSec = readFlowConfig(specBase).reviewInlineWaitSeconds
2533
+ if (inlineWaitSec > 0) {
2534
+ console.log(`⚖️ 本变更需要独立评审(${tier.reasons.join(';')})——平台单调用通道已启用(review_inline_wait=${inlineWaitSec}s),先试 inline 派发:`)
2535
+ } else {
2536
+ // 派发纪律(2026-10-19-thin-review-dispatch-verbatim):横幅是主会话派发时最先读到的通道,
2537
+ // 明确「原样复制」——实测手写缩编会丢材料包(厚道 stage-review.js:803 同款指令的薄道对齐)。
2538
+ console.log(`⚖️ 本变更需要独立评审(${tier.reasons.join(';')})——评审任务书如下,任务书原样复制给子代理(勿自行改写增删)执行后重跑本命令:\n`)
2539
+ }
2367
2540
  // 项目评审探针装配(2026-10-12-review-probe-injection):local.yaml 数据驱动的项目特有
2368
2541
  // 必查面注入任务书(未配置零注入;装载告警/帽留痕 best-effort 提示,不阻断渲染)。
2369
2542
  let _probeLines = null
@@ -2386,7 +2559,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2386
2559
  const _freezeFiles = parseFrozenPatchSummary(patchText).map((f) => f.path)
2387
2560
  if (_freezeFiles.length > 0) {
2388
2561
  const { parseFileChangeListDetailed } = await import('./change-list.js')
2389
- const _declared = parseFileChangeListDetailed(join(changeDir, 'design.md')).map((e) => e.path)
2562
+ const _declared = parseFileChangeListDetailed(join(changeDir, 'design.md')).map((e) => String(e.path).replace(/^NEW:/, ''))
2390
2563
  if (_declared.length > 0) {
2391
2564
  const { splitOwnVsForeignDiffFiles } = await import('./foreign-declared.js')
2392
2565
  const _split = splitOwnVsForeignDiffFiles(cwd, change, _freezeFiles, { specBase })
@@ -2403,43 +2576,39 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2403
2576
  const _pack = assembleReviewPack({ changeDir, patchText, attribution: _attribution, testReceipt: _receipt })
2404
2577
  if (_pack && Array.isArray(_pack.lines) && _pack.lines.length > 0) _packLines = _pack.lines
2405
2578
  } 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)
2579
+ const taskbookText = renderReviewerTaskbook({ change, changeDir, head: taskbookHead, probeLines: _probeLines, packLines: _packLines })
2580
+ let inlineWritten = false
2581
+ if (inlineWaitSec > 0) {
2582
+ try {
2583
+ const { runThinReviewInline } = await import('./review-dispatch.js')
2584
+ const inline = await runThinReviewInline({
2585
+ cwd, specBase, change, changeDir, taskbookText,
2586
+ waitMs: Math.min(300_000, inlineWaitSec * 1000),
2587
+ log: (m) => console.log(m),
2588
+ })
2589
+ if (inline.ok) {
2590
+ console.log(`✅ 平台评审单调用回收(mission ${String(inline.missionId).slice(0, 8)}/worker ${String(inline.workerId).slice(0, 8)},reviewer=platform-inline)——同调用登记结论(missing→PASS 往返消灭)`)
2591
+ inlineWritten = true
2592
+ } else {
2593
+ console.log(`ℹ️ 平台评审单调用未回收:${inline.reason}——回退评审任务书面`)
2594
+ }
2595
+ } catch (e) {
2596
+ console.log(`ℹ️ 平台评审单调用异常(${(e && e.message) || e})——回退评审任务书面`)
2597
+ }
2419
2598
  }
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 恰是误豁免率校准的最关键信号
2599
+ if (!inlineWritten) {
2600
+ if (inlineWaitSec > 0) {
2601
+ console.log(`⚖️ 回退评审任务书面——任务书原样复制给子代理(勿自行改写增删)执行后重跑本命令:\n`)
2602
+ }
2603
+ console.log(taskbookText)
2604
+ reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'missing' }
2605
+ appendTelemetry(reviewOutcome) // 修评审 P2①:失败面先落账再 exit(校准信号不丢)
2428
2606
  reportMidFail('review')
2429
2607
  process.exit(1)
2430
2608
  }
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')
2609
+ await registerReviewOutcome()
2610
+ } else {
2611
+ await registerReviewOutcome()
2443
2612
  }
2444
2613
  }
2445
2614
 
@@ -2699,7 +2868,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2699
2868
  try {
2700
2869
  const finalHead = gitQuiet(cwd, ['rev-parse', 'HEAD'])
2701
2870
  if (st.baseline_commit && typeof finalHead === 'string' && finalHead.trim() && finalHead.trim() !== st.baseline_commit) {
2702
- console.log(`🧹 Git 历史可自由整理:多轮中间提交可压扁为单提交——git reset --soft ${st.baseline_commit.slice(0, 10)} && git commit -m "<最终提交信息>"`)
2871
+ console.log(`🧹 推送前可用 sillyspec push 压扁本地未推送提交(默认 by-change;--mode one 合并为一笔;禁止 reset --soft / git push --force)`)
2703
2872
  console.log(` 审计真相已冻结在归档件 ${jsonOnlyFreeze ? 'change-patch.json(跨仓 repos[] 正文+sha256 锚定)' : 'change.patch(sha256 锚定)'} + verify-result + review.json,不依赖 git 历史;`)
2704
2873
  console.log(` 注:knowledge「最近确认」hash 为溯源快照,压扁后成孤儿引用属预期(非活引用)。`)
2705
2874
  }
@@ -2712,6 +2881,82 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2712
2881
  }
2713
2882
 
2714
2883
  /** flow 命令族入口(index.js case 'flow' 接线):flow start|done|amend-draft。 */
2884
+ /**
2885
+ * spec 面完整性只读校验单一源(2026-10-23-flow-verify-drafts):approve 批准门与
2886
+ * verify-drafts 自查命令同判定同源——verifyFlowDrafts repair:false 只读轨(不做 design 问题行
2887
+ * 自动回填,写盘修复属 done 面)+ ledger 机本位缺失降级(仅 v1 AGENT 槽形态跑空槽兜底)。
2888
+ * 纯计算零打印:调用方各自渲染(approve 文案钉死既有形态,verify-drafts 按新面渲染)。
2889
+ * 返回 { applicable, schema, violations, advisories, skipDesign, degraded, v1EmptySlots }。
2890
+ */
2891
+ async function computeSpecFaceChecks({ changeDir, change, specBase, runtimeRootOpt = null }) {
2892
+ const runtimeRoot = resolveRuntimeRoot(runtimeRootOpt ? { runtimeRoot: runtimeRootOpt } : {}, specBase)
2893
+ const { verifyFlowDrafts, verifyDesignRecordFilled } = await import('./flow-draft.js')
2894
+ const st = readFlowState(changeDir) || {}
2895
+ const adoptedDesign = st.adopted_from === 'brainstorm' && existsSync(join(changeDir, 'design.md'))
2896
+ const r = verifyFlowDrafts({ changeDir, change, runtimeRoot, skipDesign: adoptedDesign, repair: false })
2897
+ const out = {
2898
+ applicable: r.applicable === true, schema: r.schema || null,
2899
+ violations: r.violations || [], advisories: r.advisories || [],
2900
+ skipDesign: adoptedDesign, degraded: false, v1EmptySlots: [],
2901
+ }
2902
+ if (!r.applicable) {
2903
+ // draft-ledger 机本位缺失(跨机/存量变更):fail-open 降级——仅 v1(AGENT 槽形态)跑空槽
2904
+ // 兜底;v2 纯 markdown 无 ledger 无法对锚校验,如实降级(不假装能查)。
2905
+ out.degraded = true
2906
+ let v1Text = null
2907
+ try { v1Text = readFileSync(join(changeDir, 'design.md'), 'utf8') } catch { /* 无 design */ }
2908
+ if (v1Text && /<!--AGENT:槽\d+/.test(v1Text)) {
2909
+ const dr = verifyDesignRecordFilled({ changeDir })
2910
+ if (dr.applicable) out.v1EmptySlots = dr.emptySlots
2911
+ }
2912
+ }
2913
+ return out
2914
+ }
2915
+
2916
+ /**
2917
+ * flow verify-drafts(2026-10-23-flow-verify-drafts):spec 面起草自查只读命令——起草完
2918
+ * 先自查(与批准门同判定同源 computeSpecFaceChecks)、通过再请用户批准,把「用户跑 approve
2919
+ * 被 exit 1 硬拒」的往返消灭在 agent 侧。零写盘、零状态副作用(通过≠批准——批准仍走 approve
2920
+ * 留痕)。--json 与人类渲染同判定同退出码(对齐 flow status --json 双态同源先例)。
2921
+ */
2922
+ async function cmdFlowVerifyDrafts({ change, cwd, specBase, runtimeRootOpt = null, json = false }) {
2923
+ const changeDir = join(specBase, 'changes', change)
2924
+ if (!existsSync(changeDir)) {
2925
+ const msg = `❌ 变更不存在:${change}(flow verify-drafts 在 flow start 之后、approve 之前运行——起草面自查)`
2926
+ if (json) console.log(JSON.stringify({ change, status: 'missing' }))
2927
+ else console.error(msg)
2928
+ process.exit(2)
2929
+ }
2930
+ const r = await computeSpecFaceChecks({ changeDir, change, specBase, runtimeRootOpt })
2931
+ const hasViolations = r.applicable && r.violations.length > 0
2932
+ const hasEmptySlots = r.degraded && r.v1EmptySlots.length > 0
2933
+ const passed = !hasViolations && !hasEmptySlots
2934
+ if (json) {
2935
+ console.log(JSON.stringify({
2936
+ change, passed, applicable: r.applicable, schema: r.schema,
2937
+ violations: r.violations, advisories: r.advisories,
2938
+ skipDesign: r.skipDesign, degraded: r.degraded, v1EmptySlots: r.v1EmptySlots,
2939
+ }))
2940
+ if (!passed) process.exit(1) // 与人类渲染同退出码(json 不分叉)
2941
+ } else if (passed) {
2942
+ for (const a of r.advisories) console.warn(`⚠️ [门柱漂移 advisory] ${a}(不阻断批准)`)
2943
+ if (r.degraded) console.log(`ℹ️ 自查降级:draft-ledger 不在此机(.runtime 机本位/存量变更)——空槽兜底校验通过(v2 纯 markdown 无法对锚校验,如实降级)`)
2944
+ console.log(`✅ spec 面自查通过(0 violations)——通过≠批准:请把摘要给用户看,用户确认后 sillyspec flow approve --change ${change}(批准留痕后动码)`)
2945
+ } else {
2946
+ for (const a of r.advisories) console.warn(`⚠️ [门柱漂移 advisory] ${a}(不阻断批准)`)
2947
+ if (hasViolations) {
2948
+ console.error(`❌ spec 面自查未通过——${r.violations.length} 处违规(与 flow approve 批准门同判定;一次全列,修复后重跑本命令):`)
2949
+ for (const v of r.violations) console.error(` - ${v}`)
2950
+ }
2951
+ if (hasEmptySlots) {
2952
+ console.error(`❌ spec 面自查未通过——design.md 有 ${r.v1EmptySlots.length} 个空 AGENT 槽(${r.v1EmptySlots.join('、')}):每节至少一行(「不适用:<理由>」也算答)`)
2953
+ }
2954
+ console.error(` 修复后重跑:sillyspec flow verify-drafts --change ${change}——通过再请用户批准(flow approve),用户一次过`)
2955
+ process.exit(1)
2956
+ }
2957
+ return { change, passed }
2958
+ }
2959
+
2715
2960
  /**
2716
2961
  * flow approve(2026-10-04-thin-docs-v2 FR-06):spec 断点用户批准留痕——用户确认 FR+design
2717
2962
  * 方案后运行;写 flow-state {spec_approved,spec_approved_at,spec_approved_by}。flow done 的
@@ -2747,11 +2992,10 @@ async function cmdFlowApprove({ change, cwd, specBase, by = null, channelFlag =
2747
2992
  // 面只读,不做 design 问题行自动回填(写盘修复属 done 面)。诚实边界:本门证明「答在批前」,
2748
2993
  // 不证明「答在码前」——时序证据路线已实证不可行(批量收口下 git 时间戳说谎,thin-order-canon
2749
2994
  // 四路调查),不做假装能拦的检查。
2995
+ // 计算单一源 computeSpecFaceChecks(2026-10-23-flow-verify-drafts 抽取共享)——本块只保留
2996
+ // approve 面的渲染与退出,文案与退出码逐字保持(既有测试零改动作回归锚)。
2750
2997
  {
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 })
2998
+ const r = await computeSpecFaceChecks({ changeDir, change, specBase })
2755
2999
  for (const a of r.advisories || []) console.warn(`⚠️ [门柱漂移 advisory] ${a}(不阻断批准)`)
2756
3000
  if (r.applicable && r.violations.length > 0) {
2757
3001
  console.error(`❌ spec 断点未批准——文档未齐(批准是动码前置,先答 spec 面再批):`)
@@ -2762,15 +3006,10 @@ async function cmdFlowApprove({ change, cwd, specBase, by = null, channelFlag =
2762
3006
  if (!r.applicable) {
2763
3007
  // draft-ledger 机本位缺失(跨机批准/存量变更):fail-open 降级——仅 v1(AGENT 槽形态)跑
2764
3008
  // 空槽兜底;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
- }
3009
+ if (r.v1EmptySlots.length > 0) {
3010
+ console.error(`❌ spec 断点未批准——design.md 有 ${r.v1EmptySlots.length} 个空 AGENT 槽(${r.v1EmptySlots.join('、')}):每节至少一行(「不适用:<理由>」也算答)`)
3011
+ console.error(` 作答齐备后重跑:sillyspec flow approve --change ${change} [--user-confirmed|--self-approved]`)
3012
+ process.exit(1)
2774
3013
  }
2775
3014
  console.log(`ℹ️ 批准校验降级放行:draft-ledger 不在此机(.runtime 机本位/存量变更)——空槽兜底校验通过`)
2776
3015
  }
@@ -2962,6 +3201,15 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2962
3201
  await reportAgentLog(change, sub)
2963
3202
  return cmdFlowReject({ change, cwd, specBase, reason: getFlag('--reason') })
2964
3203
  }
3204
+ if (sub === 'verify-drafts') {
3205
+ // spec 面起草自查只读面(2026-10-23-flow-verify-drafts):与批准门同判定同源——起草完先
3206
+ // 自查、通过再请批准,用户被拒往返消灭在 agent 侧;零写盘零状态副作用。
3207
+ const change = getFlag('--change')
3208
+ if (!change) { console.error('❌ flow verify-drafts 需 --change <名>'); process.exit(2) }
3209
+ validateChangeName(change)
3210
+ await reportAgentLog(change, sub)
3211
+ return cmdFlowVerifyDrafts({ change, cwd, specBase, runtimeRootOpt, json: jsonGlobal || hasFlag('--json') })
3212
+ }
2965
3213
  if (sub === 'diff') {
2966
3214
  // 只读查询面(2026-10-13-flow-diff-cmd):change 粒度文件统计双态口——与 status 同级自愿面
2967
3215
  const change = getFlag('--change')
@@ -3152,7 +3400,7 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
3152
3400
  console.log(`✅ flow amend-draft 留痕重锚:${r.reanchored.join('、')}——ledger amendment 审计在案;editRatio=${maxRatio}(段级最大;基准=首版原文,AGENT 槽不计入)`)
3153
3401
  return r
3154
3402
  }
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](只读文件统计)')
3403
+ 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
3404
  console.error(' --input 可照抄形态(引号内换行合法):')
3157
3405
  console.error(' sillyspec flow start --change <名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字);随后动机与背景')
3158
3406
  console.error('')