sillyspec 3.32.6 → 3.32.7

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
@@ -62,6 +62,17 @@ export function writeFlowState(changeDir, patch) {
62
62
  }, { lineWidth: 120 }) + '\n')
63
63
  }
64
64
 
65
+ /** 批准通道取值(2026-10-12-approve-channel-attest):flow done 回显与遥测同源——历史变更
66
+ * 无 channel 字段时归一 unattested-legacy(2026-10-12 之前真批/自批不可区分的留痕形态),
67
+ * autopilot 是用户显式豁免通道非批准。三态封闭集(user-confirmed/agent-self/unattested)是
68
+ * 治理域(谁在主张批准),不承载内容语义——开放内容走 spec_rejected_reason 自由文本。 */
69
+ export function approveChannelOf(st) {
70
+ const s = st || {}
71
+ if (s.autopilot) return 'autopilot'
72
+ if (s.spec_approved_channel) return String(s.spec_approved_channel)
73
+ return s.spec_approved ? 'unattested-legacy' : 'none'
74
+ }
75
+
65
76
  /** 读 local.yaml 的 flow 配置(缺省 thin——2026-09-25-thin-default-flip 翻转:入口归一,
66
77
  * D-010@v2 实验通道定位由轻量变更加固件(设计记录/测试绑定/patch 留档/预段收编)的数据推翻;
67
78
  * 显式 mode: legacy 的仓维持旧道。文本级读同既有习惯)。 */
@@ -73,13 +84,17 @@ export function readFlowConfig(specBase) {
73
84
  if (/^flow\s*:\s*(thin|legacy)\s*$/m.test(raw)) mode = raw.match(/^flow\s*:\s*(thin|legacy)\s*$/m)[1]
74
85
  const th = raw.match(/^\s*edit_ratio_threshold\s*:\s*([0-9.]+)\s*$/m)
75
86
  const en = raw.match(/^\s*edit_ratio_enforcement\s*:\s*(advisory|block)\s*$/m)
87
+ // module_gate_ignore(2026-10-12-thin-module-gate-and-feed):未登记目录软门的固定忽略清单,
88
+ // 单行逗号串(目录前缀,posix)——列表块形态不支持,readFlowConfig 同款逐键正则防线
89
+ const ig = raw.match(/^\s*module_gate_ignore\s*:\s*(.+)$/m)
76
90
  return {
77
91
  mode,
78
92
  editRatioThreshold: th ? Number(th[1]) : 0.5,
79
93
  editRatioEnforcement: en ? en[1] : 'advisory',
94
+ moduleGateIgnore: ig ? ig[1].split(',').map((s) => s.trim()).filter(Boolean) : [],
80
95
  }
81
96
  } catch {
82
- return { mode: 'thin', editRatioThreshold: 0.5, editRatioEnforcement: 'advisory' }
97
+ return { mode: 'thin', editRatioThreshold: 0.5, editRatioEnforcement: 'advisory', moduleGateIgnore: [] }
83
98
  }
84
99
  }
85
100
 
@@ -310,6 +325,16 @@ export async function flowKnowledgeDigest({ specBase, change, changeDir, input,
310
325
  lines.length = 0
311
326
  lines.push(`🧠 知识注入(轻量道读取面):语料未命中知识库(无触达域依据、无否决命中、INDEX 无对齐条目)——可自行查 knowledge/INDEX.md。`)
312
327
  }
328
+ // 模块卡欠账提示前移(2026-10-12-thin-module-gate-and-feed 三档):触碰命中模块 behind>0
329
+ // 才出事实行——动手前看到「你要动的卡已落后」比收口时警告有效;computeDocsDebt(厚道
330
+ // execute {DOCS_DEBT})同源,空债/无图零输出,异常走外层 catch fail-open 不阻断 start。
331
+ if (touched.length > 0) {
332
+ try {
333
+ const { renderThinDocsDebt } = await import('./flow-parity.js')
334
+ const debtLines = renderThinDocsDebt({ specBase, projectRoot: dirname(specBase), changedFiles: touched })
335
+ if (debtLines.length > 0) lines.push(...debtLines)
336
+ } catch { /* 欠账计算 fail-open */ }
337
+ }
313
338
  } catch (e) {
314
339
  lines.length = 0
315
340
  lines.push(`🧠 知识注入(轻量道读取面):注入失败(${(e && e.message) || e})——可自行读 knowledge/INDEX.md 与 knowledge/fr/`)
@@ -792,7 +817,10 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
792
817
  `🛑 三断点纪律(可控性要求——用户没说「全跑完」就必须在每个断点向用户汇报并等确认):`,
793
818
  ` ① spec 断点【机器门】:填完 FR 区和 design 四节后,把摘要给用户看(FR 条目+盲维作答+方案概述),`,
794
819
  ` 用户确认后请其运行 sillyspec flow approve --change ${change}(批准留痕)再动手写代码——`,
795
- ` 方案错了返工最贵;未批准且未声明 --autopilot 时 flow done 拒收(2026-10-04-thin-docs-v2)。`,
820
+ ` agent 代跑时如实声明通道:--user-confirmed(转述用户已确认)/ --self-approved(agent 自批),`,
821
+ ` 无 flag 按 unattested 留痕(审计面无法区分用户确认与自批)——2026-10-12-approve-channel-attest。`,
822
+ ` 方案错了返工最贵;未批准且未声明 --autopilot 时 flow done 拒收(2026-10-04-thin-docs-v2);`,
823
+ ` 用户否掉方案 → flow reject --change ${change} --reason <理由>(拒后批准须显式通道再确认)。`,
796
824
  ` ② 执行断点:写完代码跑完测试后,把测试结果(过了几个/挂了什么)给用户看,`,
797
825
  ` 等用户确认再跑收口。`,
798
826
  ` ③ 归档断点:flow done 跑完后(无论过/拒),把结果(归档成功/被什么拦了)给用户看。`,
@@ -872,7 +900,7 @@ export function remainingSubstepsAtFail({ snapshotSubsteps = {}, doneList = [],
872
900
  return SUBSTEPS.filter((k) => k !== failed && snapshotSubsteps?.[k] !== 'done' && !doneNow.has(k))
873
901
  }
874
902
 
875
- export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null, confirmArchive = true, freezeDirty = false, acceptDirtyGap = false, allowBatchTick = false }) {
903
+ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null, confirmArchive = true, freezeDirty = false, acceptDirtyGap = false, acceptOrphanFace = false, allowBatchTick = false, acceptUnmapped = false }) {
876
904
  const changeDir = join(specBase, 'changes', change)
877
905
  const st = readFlowState(changeDir)
878
906
  if (!st) {
@@ -906,6 +934,10 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
906
934
  const skip = (k) => { doneList.push(`${k}(skip)`) }
907
935
  let reviewOutcome = null
908
936
  let gateSummaryText = null
937
+ // 模块对账面(2026-10-12-thin-module-gate-and-feed):patch 子步算出后供 distill 回写消费;
938
+ // 断点续跑 patch 已 done 时从 change-patch.json files[] 回读重算(单一时点口径,不随图回写漂移)
939
+ let moduleScope = null
940
+ let changelogAppended = []
909
941
  // 遥测单点(2026-09-25-thin-parity-assets 修评审 P2①:失败路径 exit 前也要落账——校准信号
910
942
  // 不许在失败面丢失;成功收口走函数末尾同款记录,失败面只多不少)
911
943
  const appendTelemetry = (review) => {
@@ -914,6 +946,9 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
914
946
  ts: new Date().toISOString(), change, protocolCalls: 2,
915
947
  draftAmendments: st.edit_ratio != null ? 1 : 0, editRatio: st.edit_ratio ?? null,
916
948
  routeHint: st.route_hint ?? null, tier: st.tier, upgraded: st.upgrade_reason ?? null,
949
+ // 批准通道遥测(2026-10-12-approve-channel-attest):与收口回显同源取值,校准
950
+ // unattested/agent-self 占比用(dogfood 实证留痕不可区分 → 通道可聚合)。
951
+ approveChannel: approveChannelOf(st),
917
952
  review,
918
953
  }) + '\n', 'utf8')
919
954
  } catch { /* 遥测 best-effort */ }
@@ -946,11 +981,27 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
946
981
  if (!st.autopilot && !st.spec_approved) {
947
982
  console.error(`❌ spec 断点未批准:本变更(v2 起草)收口需要方案确认留痕`)
948
983
  console.error(` 修复:把 FR 条目+design 四节摘要给用户看,用户确认后由用户运行:`)
949
- console.error(` sillyspec flow approve --change ${change}`)
984
+ console.error(` sillyspec flow approve --change ${change} [--user-confirmed|--self-approved]`)
950
985
  console.error(` 或用户显式豁免(自主跑到底):重跑 flow start --change ${change} --autopilot(声明留痕)`)
951
986
  reportMidFail('artifacts')
952
987
  process.exit(1)
953
988
  }
989
+ // 批准通道回显(2026-10-12-approve-channel-attest):channel 是审计面第一公民——agent-self
990
+ // 与 unattested 醒目(dogfood 实证:用户真批转述与 agent 自批在留痕层面曾不可区分)。
991
+ if (st.autopilot) {
992
+ console.log('🔑 spec 断点:autopilot 显式豁免(用户授权自主跑到底)')
993
+ } else {
994
+ const _ch = approveChannelOf(st)
995
+ const _warn = _ch === 'agent-self' || _ch.startsWith('unattested')
996
+ // 评审 P3② 清偿:翻越语只在 superseded 戳在场时陈述——并发窄窗混合态(approved 真 +
997
+ // 拒绝未翻越)不得假称「已显式再确认」,按状态异常如实告警。
998
+ const _rej = st.spec_rejected_reason
999
+ ? (st.spec_rejected_superseded_at
1000
+ ? `(曾拒绝:${String(st.spec_rejected_reason).slice(0, 60)}→显式再确认翻越)`
1001
+ : `(⚠️ 存在未翻越拒绝记录(spec_rejected 未 superseded)——状态异常,请核查 flow-state)`)
1002
+ : ''
1003
+ console.log(`${_warn ? '⚠️' : '🔑'} spec 断点批准留痕:channel=${_ch}|by=${st.spec_approved_by || '?'}${_rej}`)
1004
+ }
954
1005
  } else {
955
1006
  if (r.applicable && r.violations.length > 0) {
956
1007
  console.error(`❌ 工件校验拒收(机器稿指纹三态):`)
@@ -1046,6 +1097,10 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1046
1097
  // token 合法形态写明(2026-10-11-flow-start-redline-frontload):实证「task-01..task-05」
1047
1098
  // 区间写法被判零证据时才知道格式约束——拒收文案自释,减少一轮试错。
1048
1099
  console.error(' 补证据(提交标题或正文带 task-NN——逐个列出或 /、,连写(如 task-01/02/03),「task-01..task-05」区间写法不认;或产 review.json)或取消勾选后重跑——假完成主张不许过门')
1100
+ // 扫描面透明化(2026-10-12-cli-friction-signal,dogfood 实证:跨仓交付的 agent 靠猜
1101
+ // 证据该落在哪)——声明哨兵只扫工作区仓与变更目录,跨仓仓提交天然不在面内。
1102
+ console.error(` 证据扫描面:工作区仓 git log ${st.baseline_commit ? String(st.baseline_commit).slice(0, 10) : 'baseline'}..HEAD 的提交消息(标题+正文)+ 变更目录内 review.json;`)
1103
+ console.error(' 跨仓仓(register-repo 登记的外部仓)的提交不在扫描面——交付提交在其他仓时,须在工作区仓的提交消息带 task-NN(如治理工件提交)或产出 review.json 补证。')
1049
1104
  appendTelemetry({ sentinel: 'fake', missing: sent.missing.length })
1050
1105
  reportMidFail('ledger')
1051
1106
  process.exit(1)
@@ -1286,10 +1341,11 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1286
1341
  // 治理面等价保留(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop
1287
1342
  // 用户实证):评审结论的对象是交付面——窗口内本变更提交只碰 .sillyspec/** 治理文件且
1288
1343
  // 未触本变更 requirements/design(承诺面也是评审对象)时,结论对最新交付面逐字节等价,
1289
- // 保留不隔离不重置标记;change.patch 仍走下方 driftRefreeze 自动重冻结吸收治理面增量
1344
+ // 保留不隔离不重置标记;change.patch 仍走下方 driftRefreeze 自动重冻结(FR-01 后治理
1345
+ // 工件不入冻结面——治理修复的留档真相=归档目录本体文件,重冻结保交付面时点对齐)
1290
1346
  // (审计件覆盖最新提交面不变量不破)。此前任何后缀提交一律隔离重评——评审后修一个
1291
1347
  // P2 文档措辞也付整轮子代理评审费(循环,被迫手工重排执行顺序自赎)。
1292
- console.log(` review.json 保留不隔离——窗口内本变更提交仅触及治理文件(${drift.ownFiles.length} 个,未触 requirements/design),评审对象(交付面)未变,结论对最新面等价;change.patch 自动重冻结吸收治理面增量`)
1348
+ console.log(` review.json 保留不隔离——窗口内本变更提交仅触及治理文件(${drift.ownFiles.length} 个,未触 requirements/design),评审对象(交付面)未变,结论对最新面等价;change.patch 自动重冻结(治理工件不入冻结面,治理修复随归档目录本体留档)`)
1293
1349
  } else {
1294
1350
  const ts = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, '')
1295
1351
  try {
@@ -1318,11 +1374,14 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1318
1374
  }
1319
1375
  if (st.substeps?.patch !== 'done' || driftRefreeze) {
1320
1376
  let patchOk = false
1377
+ let orphanGapCount = 0
1321
1378
  try {
1322
- // patch 面 = 本变更可归属变化(2026-09-25-thin-patch-scope-fix 收窄:dirty 全扫面会把并行
1323
- // 会话未声明 WIP 冻结进来——上变更实测 49 文件泄漏):①baseline..HEAD 提交面(.sillyspec/
1324
- // 治理面只留本变更目录,他侧 quicklog/knowledge WIP 不入)②本变更目录全部工作树件(未提交
1325
- // 治理面/槽位作答)——排除 patch 自引用。提交面再过他侧声明切分(他侧同窗口提交的防线)。
1379
+ // patch 面 = 本变更可归属的交付面(2026-10-12-close-trace-freeze-integrity FR-01 收敛):
1380
+ // ①baseline..HEAD 提交面过滤 .sillyspec/(只留 docs/ 交付文档——治理工件目录不入冻,
1381
+ // 对齐 execute --done 沉淀资产面投影;工件真相随归档目录本体入库,patch 再冻是冗余且
1382
+ // 掩盖「可归属面为空」哨兵——ws-ref-ws-name +137/-0 全工件 0 代码实证)②独占树未提交
1383
+ // 交付面(worktree/--freeze-dirty)。他侧 quicklog/knowledge WIP 不入;提交面再过他侧
1384
+ // 声明切分(他侧同窗口提交的防线)。
1326
1385
  if (!st.baseline_commit) {
1327
1386
  console.log('📦 patch 留档跳过:无 git 基线(无历史仓)')
1328
1387
  patchOk = true
@@ -1383,18 +1442,6 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1383
1442
  if (dirtyForeign.length > 0) {
1384
1443
  console.warn(`⚠️ patch 留档:${dirtyForeign.length} 个 dirty 文件被其他活跃变更声明排除(${dirtyForeign.slice(0, 3).map((x) => `${x.file}←${x.owners[0]}`).join(', ')}${dirtyForeign.length > 3 ? ' 等' : ''})——如有误(陈旧声明),清理该旧变更或用 --refreeze 重冻结`)
1385
1444
  }
1386
- const changeDirFiles = []
1387
- const walk = (dir) => {
1388
- for (const e of readdirSync(dir, { withFileTypes: true })) {
1389
- // flow-state.yaml 是运行态(机器写、冻结后仍随子步推进变化)——不入审计 patch
1390
- //(2026-09-27-tool-debt-cleanup:旧口径把它当 new file 冻进 patch,评审 P2)
1391
- if (e.name === 'flow-state.yaml') continue
1392
- const p = join(dir, e.name)
1393
- if (e.isDirectory()) walk(p)
1394
- else changeDirFiles.push(relative(cwd, p).replace(/\\/g, '/'))
1395
- }
1396
- }
1397
- try { walk(changeDir) } catch { /* 目录异常=空面 */ }
1398
1445
  // 冻结面收集(2026-09-25-thin-r16-patches 修复①):committed 为主;会话专属 worktree
1399
1446
  // (gate-snapshot 同款路径判定)下未提交 dirty 交付面一并入冻结——独占树内全归属本变更;
1400
1447
  // 共享主仓 dirty 无法归属只警告(他侧声明免警告),审计缺口显式化不再静默。
@@ -1443,26 +1490,49 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1443
1490
  // change-patch.json 两件,但旧轮冻结件里可能仍有 scope-audit.json/.patch——
1444
1491
  // legacy 防护保留):上一轮留痕件作为 untracked 新文件被全文自嵌入即自引用循环
1445
1492
  // (旧 patch 内容混进新 patch,三评 P1 根因)。
1446
- const ownFiles = [...new Set([...freeze.files, ...changeDirFiles])]
1493
+ const ownFiles = [...new Set([...freeze.files])]
1447
1494
  .filter((f) => f && !f.endsWith('change.patch') && !f.endsWith('change-patch.json')
1448
1495
  && !f.endsWith('scope-audit.json') && !f.endsWith('scope-audit.patch'))
1496
+ // 独占 worktree 冻结完备性守卫(2026-10-12-close-trace-freeze-integrity FR-02):
1497
+ // 事故形态(multi-agent-platform ws-ref-ws-name 取证):实现提交在会话 worktree 内完成
1498
+ // 但不在 flow done 时点 HEAD 的祖先链(reset/detach/平行分支)——baseline..HEAD 提交面
1499
+ // 与 git status 脏面都看不到,冻结面静默缺实现。本 worktree 的 HEAD reflog 会话独占
1500
+ // (linked worktree 独立 reflog),扫全部轨迹:不在 HEAD 祖先链且触及交付面的提交,
1501
+ // patch-id 与 baseline..HEAD 提交等价(amend/rebase 残留)视为已覆盖;否则阻断收口
1502
+ // (与 dirty gate 同款断点续语义)。共享主仓路径明确不启用——reflog 混他侧会话操作
1503
+ // 无法归属,--freeze-dirty 手动声明只覆盖脏面不进 reflog 扫描。
1504
+ if (exclusiveFrom === 'worktree') {
1505
+ try {
1506
+ const { scanOrphanFace } = await import('./flow-parity.js')
1507
+ const orphan = scanOrphanFace(cwd, { baseline: st.baseline_commit })
1508
+ if (orphan.files.length > 0 && !acceptOrphanFace) {
1509
+ console.error(`⛔ 会话专属 worktree 存在冻结面外交付改动:${orphan.commits.length} 个提交不在 HEAD 祖先链(patch-id 不等价)触及 ${orphan.files.length} 个交付文件——收口阻断(不归档;处置后重跑同一条命令从断点续):`)
1510
+ console.error(` 提交:${orphan.commits.slice(0, 5).map((c) => c.slice(0, 10)).join('、')}${orphan.commits.length > 5 ? ' 等' : ''}`)
1511
+ console.error(` 文件:${orphan.files.slice(0, 5).join('、')}${orphan.files.length > 5 ? ' 等' : ''}`)
1512
+ console.error(` ① git rebase/cherry-pick 这些提交到 HEAD(补齐冻结面)后重跑`)
1513
+ console.error(` ② 确认放弃这些提交:清理对应分支/reflog 源后重跑`)
1514
+ console.error(` ③ 接受缺口留痕:flow done --change ${change} --accept-orphan-face`)
1515
+ reportMidFail('patch')
1516
+ process.exit(1)
1517
+ }
1518
+ if (orphan.files.length > 0 && acceptOrphanFace) {
1519
+ orphanGapCount = orphan.files.length
1520
+ console.warn(`⚠️ --accept-orphan-face:显式接受 ${orphan.commits.length} 个冻结面外提交的 ${orphan.files.length} 个交付文件不进冻结面(缺口数随 change-patch.json acceptedOrphanFace 留痕)`)
1521
+ }
1522
+ } catch { /* 守卫异常不阻断(fail-safe 不新造阻断面) */ }
1523
+ }
1449
1524
  if (ownFiles.length === 0) {
1450
- console.log('📦 patch 留档跳过:本变更可归属文件面为空')
1525
+ console.log('📦 patch 留档跳过:本变更可归属文件面为空(无交付面改动——治理工件不入冻结面;若实现代码应在此,先提交后重跑)')
1451
1526
  patchOk = true
1452
1527
  } else {
1453
1528
  const { buildFrozenPatch, collectNumstatByPath } = await import('./scope-audit.js')
1454
1529
  // 双口径冻结(2026-09-28-split-guard-and-gate-report 评审 P2 清偿):交付面(committed)
1455
1530
  // 用 baseRef..HEAD 提交区间 diff——共享主仓里并行会话对同文件的未提交 hunk 不再漏进
1456
- // 冻结件;治理工件目录(changeDirFiles,本变更独占无他会话面)保留工作树口径(未提交
1457
- // 的槽位作答/勾选要进审计件)。重叠文件归提交区间,避免双 hunk。
1531
+ // 冻结件;独占树未提交交付(dirtyAdded)保留工作树口径(FR-01 后治理工件目录不再入冻,
1532
+ // 工作树口径集只剩 dirtyAdded)。重叠文件归提交区间,避免双 hunk。
1458
1533
  const headCommit = String(gitQuiet(cwd, ['rev-parse', 'HEAD']) || '').trim() || null
1459
1534
  const toPosix = (p) => String(p).replace(/\\/g, '/')
1460
- // 复审 P2/P3a 清偿:工作树口径集 = 治理工件目录(含已提交后又改的槽位/勾选——工作树
1461
- // diff 能捕捉提交后编辑)∪ 独占树未提交交付(dirtyAdded);其余交付面走提交区间。
1462
- // 排除项与 ownFiles 同款(四名全排,含 scope-audit.* 旧轮冻结件 legacy 防护)。
1463
- const dirFilesForPatch = changeDirFiles.filter((f) => !f.endsWith('change.patch') && !f.endsWith('change-patch.json')
1464
- && !f.endsWith('scope-audit.json') && !f.endsWith('scope-audit.patch'))
1465
- const worktreeSet = new Set([...dirFilesForPatch, ...((freeze.dirtyAdded) || [])].map(toPosix))
1535
+ const worktreeSet = new Set([...((freeze.dirtyAdded) || [])].map(toPosix))
1466
1536
  const committedFace = freeze.files.map(toPosix).filter((f) => !worktreeSet.has(f))
1467
1537
  const frozenCommitted = buildFrozenPatch(cwd, committedFace, { baseRef: st.baseline_commit, headRef: headCommit })
1468
1538
  const frozenDir = buildFrozenPatch(cwd, [...worktreeSet], { baseRef: st.baseline_commit })
@@ -1483,13 +1553,36 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1483
1553
  // 范围,单一计算喂 console 与落盘两处。ownFiles 传冻结面(与 files[] 同口径——
1484
1554
  // worktree/--freeze-dirty 场景 dirty 交付也计入模块命中;.sillyspec 治理面对账内部
1485
1555
  // 滤除);三键恒在场(对账异常兜底空数组,评审 P3 清偿);best-effort 不阻断。
1486
- let moduleScope = { modules: [], uncoveredDirs: [], moduleMaps: [] }
1556
+ moduleScope = { modules: [], uncoveredDirs: [], moduleMaps: [] }
1487
1557
  try {
1488
1558
  const { reconcileModuleDocs } = await import('./flow-parity.js')
1489
1559
  const rec = reconcileModuleDocs({ cwd, specBase, ownFiles, committedRaw })
1490
1560
  if (rec.lines.length > 0) for (const l of rec.lines) console.log(l)
1491
1561
  moduleScope = { modules: rec.modules, uncoveredDirs: rec.uncoveredDirs, moduleMaps: rec.moduleMaps }
1492
1562
  } catch { /* 对账 best-effort(三键空数组兜底) */ }
1563
+ // 未登记目录软门(2026-10-12-thin-module-gate-and-feed 一档):拦在 distill 之前——
1564
+ // indexRequirements 的域路由发生在 distill,拦在前面 FR 才不落伪域。拦但可签收:登记/
1565
+ // --accept-unmapped(缺口数留痕)/ module_gate_ignore 固定忽略三条出路;绿地无图不拦。
1566
+ // 门消费的是本子步刚算的对账面(与收口留痕同时点口径),失败在 mark('patch') 前——
1567
+ // 重入 done 从 patch 断点续,登记后重算自然通过。
1568
+ let moduleGate = { block: false, dirs: [], maps: [] }
1569
+ try {
1570
+ const { evaluateModuleGate } = await import('./flow-parity.js')
1571
+ moduleGate = evaluateModuleGate({ moduleScope, ignore: readFlowConfig(specBase).moduleGateIgnore })
1572
+ } catch { /* 门判定异常不拦(对账面空 → 无可拦事实,fail-open 同对账口径) */ }
1573
+ if (moduleGate.block && !acceptUnmapped) {
1574
+ const dirList = moduleGate.dirs.slice(0, 5).map((d) => `${d.dir}(${d.files} 文件)`).join('、') + (moduleGate.dirs.length > 5 ? ' 等' : '')
1575
+ console.error(`❌ 模块图未登记交付目录(门):${dirList}——共 ${moduleGate.dirs.length} 个目录不在任何 _module-map.yaml 的 paths 下`)
1576
+ console.error(' 未登记后果是静默的:FR 蒸馏将落伪域 auto-*、知识图谱模块侧边(module-files/describes/changelog-of)断供、模块上下文注入绕开这些目录。')
1577
+ console.error(` ① 登记后重跑:在 docs/<project>/modules/_module-map.yaml 增补 modules.<id>(paths 指向该目录,可带 doc: modules/<id>.md)后重跑 flow done`)
1578
+ console.error(` ② 确认无需登记:flow done --change ${change} --accept-unmapped(缺口数随 change-patch.json acceptedUnmappedDirs 留痕)`)
1579
+ console.error(' ③ 固定忽略(合法未登记面:根级脚本/配置等):.sillyspec/local.yaml 加 module_gate_ignore: <目录>(单行逗号分隔,前缀匹配)')
1580
+ reportMidFail('patch')
1581
+ process.exit(1)
1582
+ }
1583
+ if (moduleGate.block && acceptUnmapped) {
1584
+ console.warn(`⚠️ --accept-unmapped:显式接受 ${moduleGate.dirs.length} 个未登记目录不进模块图(缺口数随 change-patch.json acceptedUnmappedDirs 留痕)`)
1585
+ }
1493
1586
  // 收口留痕单套写(2026-10-09-close-trace-single-set):共用 writeCloseTraceArtifacts
1494
1587
  // 只写 change.patch + change-patch.json 两件——对账面数据并入 JSON 的 scopeAudit
1495
1588
  // 子对象(thin 通道同样有对账面),旧名 scope-audit.* 停写、读侧兼容兜存量归档。
@@ -1544,13 +1637,18 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1544
1637
  ...(crossRepos.length > 0 ? { repos: crossRepos } : {}),
1545
1638
  },
1546
1639
  meta: {
1547
- note: 'flow done 时点冻结(本变更可归属面:baseline..HEAD 提交面过滤 .sillyspec/ 非本变更目录但保留 .sillyspec/docs/ 交付文档 + 本变更目录工作树件;含未提交与 untracked,排除 flow-state.yaml 运行态)',
1640
+ note: 'flow done 时点冻结(本变更可归属交付面:baseline..HEAD 提交面过滤 .sillyspec/ 只留 .sillyspec/docs/ 交付文档 + 独占树未提交交付;治理工件目录不入冻结——2026-10-12-close-trace-freeze-integrity FR-01)',
1548
1641
  // 模块对账结构化面(2026-09-27-thin-module-scope-persist):done 时点口径
1549
1642
  // (与 files[] 同时点语义,不随模块图后续变更回写);modules[] 空≠无影响
1550
1643
  ...moduleScope,
1551
1644
  // dirty 缺口显式接受留痕(2026-10-08-thin-done-dirty-gate-and-paren-attribution):
1552
1645
  // --accept-dirty-gap 放行时缺口数入档(审计面可见,非只 console)
1553
1646
  ...(acceptDirtyGap && freeze.dirtyWarned.length > 0 ? { acceptedDirtyGap: freeze.dirtyWarned.length } : {}),
1647
+ // 未登记目录签收留痕(2026-10-12-thin-module-gate-and-feed 一档):同款显式接受语义
1648
+ ...(acceptUnmapped && moduleGate.dirs.length > 0 ? { acceptedUnmappedDirs: moduleGate.dirs.length } : {}),
1649
+ // 孤儿面缺口显式接受留痕(2026-10-12-close-trace-freeze-integrity FR-02):
1650
+ // --accept-orphan-face 放行时缺口数入档
1651
+ ...(acceptOrphanFace && orphanGapCount > 0 ? { acceptedOrphanFace: orphanGapCount } : {}),
1554
1652
  },
1555
1653
  })
1556
1654
  console.log(`📦 变更 patch 留档(单套):change.patch + change-patch.json(${ownFiles.length} 文件,+${additions}/-${deletions},对账面并入 scopeAudit 子对象)${closeTrace.patchStatus === 'ok' ? ',sha256 已锚' : '——patch 采集失败已留痕'}`)
@@ -1618,7 +1716,16 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1618
1716
  let taskbookHead = null
1619
1717
  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 */ }
1620
1718
  console.log(`⚖️ 本变更需要独立评审(${tier.reasons.join(';')})——评审任务书如下,起一个干净上下文的子代理执行后重跑本命令:\n`)
1621
- console.log(renderReviewerTaskbook({ change, changeDir, head: taskbookHead }))
1719
+ // 项目评审探针装配(2026-10-12-review-probe-injection):local.yaml 数据驱动的项目特有
1720
+ // 必查面注入任务书(未配置零注入;装载告警/帽留痕 best-effort 提示,不阻断渲染)。
1721
+ let _probeLines = null
1722
+ try {
1723
+ const { assembleProbeLinesForScope } = await import('./review-probes.js')
1724
+ const _assembled = assembleProbeLinesForScope(specBase, 'thin')
1725
+ for (const _w of _assembled.warnings) console.warn(`⚠️ review_probes:${_w}`)
1726
+ if (_assembled.lines.length > 0) _probeLines = _assembled.lines
1727
+ } catch { /* 探针面 fail-soft:装配失败零注入 */ }
1728
+ console.log(renderReviewerTaskbook({ change, changeDir, head: taskbookHead, probeLines: _probeLines }))
1622
1729
  reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'missing' }
1623
1730
  appendTelemetry(reviewOutcome) // 修评审 P2①:失败面先落账再 exit(校准信号不丢)
1624
1731
  reportMidFail('review')
@@ -1729,6 +1836,20 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1729
1836
  mark('probes')
1730
1837
  }
1731
1838
 
1839
+ // 模块对账面断点续跑回读(2026-10-12-thin-module-gate-and-feed 二档):patch 子步已 done 的
1840
+ // 重入轮次上 moduleScope 为空(进程内变量不跨轮)——从 change-patch.json files[] 回读重算
1841
+ // (distill 回写与 archive 前的 extraDocs 推导共用;files[] 与冻结面同时点口径)。
1842
+ if (!moduleScope) {
1843
+ try {
1844
+ const pm = JSON.parse(readFileSync(join(changeDir, 'change-patch.json'), 'utf8'))
1845
+ const filesArr = Array.isArray(pm && pm.files) ? pm.files : []
1846
+ if (filesArr.length > 0) {
1847
+ const { reconcileModuleDocs } = await import('./flow-parity.js')
1848
+ const rec = reconcileModuleDocs({ cwd, specBase, ownFiles: filesArr, committedRaw: [] })
1849
+ moduleScope = { modules: rec.modules, uncoveredDirs: rec.uncoveredDirs, moduleMaps: rec.moduleMaps }
1850
+ }
1851
+ } catch { /* 回读失败 → 空面(回写零命中静默) */ }
1852
+ }
1732
1853
  // ④ distill:决策提炼(rejected/needsWait 异态 → 升厚留人工裁决,不静默吞)+ FR 索引提炼
1733
1854
  // (2026-09-22-thin-fr-distill-sync:轻量变更此前只蒸馏 decisions 不调 indexRequirements——
1734
1855
  // requirements 永不进 knowledge/fr,知识复利在新默认道断流;轻量变更无 design.md,域路由
@@ -1741,6 +1862,21 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1741
1862
  const h = harvestSlot4Decision({ changeDir, change })
1742
1863
  if (h.harvested) console.log(`🌱 槽4(风险与死路)收割 → decisions.md(底稿——作答里的放弃方案请拆成 rejected 条目再收口;随蒸馏链进 knowledge)`)
1743
1864
  } catch { /* 收割 best-effort */ }
1865
+ // 模块变更索引机械回写(2026-10-12-thin-module-gate-and-feed 二档):命中模块的卡/sidecar
1866
+ // 追加一行本变更条目——零语义判断(日期/变更名/动机首行全已知),split-changelog 契约 sidecar
1867
+ // 优先、卡内「## 变更索引」节次之、变更名在场即跳过(断点续跑幂等)。moduleScope 由上方
1868
+ // 回读块保证在场(patch 本轮算出或 change-patch.json 回读)。
1869
+ try {
1870
+ if (moduleScope && Array.isArray(moduleScope.modules) && moduleScope.modules.length > 0) {
1871
+ const { appendChangeIndexEntries } = await import('./flow-parity.js')
1872
+ const motive = String(st.input || '').split('\n').map((s) => s.trim()).find(Boolean) || ''
1873
+ const wr = appendChangeIndexEntries({ specBase, modules: moduleScope.modules, change, summary: motive, date: new Date().toISOString().slice(0, 10) })
1874
+ changelogAppended = wr.appended
1875
+ if (wr.appended.length > 0) console.log(`📖 模块变更索引回写:${wr.appended.length} 个目标(${wr.appended.join('、')})——sidecar 优先/卡内节次之,随归档窄化入暂存`)
1876
+ const skippedReal = wr.skipped.filter((s) => !/已记录/.test(s.reason))
1877
+ if (skippedReal.length > 0) console.log(` ↩️ 跳过 ${skippedReal.length} 个(${skippedReal.slice(0, 3).map((s) => `${s.id}:${s.reason}`).join(';')}${skippedReal.length > 3 ? ' 等' : ''})`)
1878
+ }
1879
+ } catch (e) { console.warn(`⚠️ 变更索引回写失败(fail-soft 不阻断归档):${(e && e.message) || e}`) }
1744
1880
  try {
1745
1881
  const { distillIntoKnowledge } = await import('./decision-distill.js')
1746
1882
  const knowledgeRoot = join(specBase, 'knowledge')
@@ -1813,6 +1949,16 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1813
1949
  } catch (e) { console.warn(`⚠️ 回执合成失败(不阻断归档):${(e && e.message) || e}`) }
1814
1950
  const { ProgressManager } = await import('./progress.js')
1815
1951
  const { runArchiveChain } = await import('./run/complete-handlers.js')
1952
+ // extraDocs 断点续跑推导(评审 P3 清偿 2026-10-12-thin-module-gate-and-feed):distill 已
1953
+ // done 的重入轮次上 changelogAppended 为空(进程内变量不跨轮)——从命中模块的卡/sidecar
1954
+ // 反查含本变更名的目标文件(幂等事实,判据与回写去重同款 text.includes(change)),
1955
+ // extraDocs 精确入暂存不靠目录级回退兜底。
1956
+ if (changelogAppended.length === 0 && moduleScope && Array.isArray(moduleScope.modules) && moduleScope.modules.length > 0) {
1957
+ try {
1958
+ const { deriveRecordedChangeIndexTargets } = await import('./flow-parity.js')
1959
+ changelogAppended = deriveRecordedChangeIndexTargets({ specBase, modules: moduleScope.modules, change })
1960
+ } catch { /* 推导失败 → extraDocs 空(目录级回退兜底) */ }
1961
+ }
1816
1962
  // 锚定同 start(平台参数面)
1817
1963
  const { archiveDestDirName } = await import('./stage-contract.js')
1818
1964
  const pm = new ProgressManager()
@@ -1824,6 +1970,8 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1824
1970
  srcDir: changeDir,
1825
1971
  destDir,
1826
1972
  skipPlanCheck: (st.born_face ?? st.tier) === 'thin',
1973
+ // 二档回写文件随归档窄化精确入暂存(文件级 pathspec;空数组传 null 走既有零行为)
1974
+ extraDocs: changelogAppended.length > 0 ? changelogAppended.map((p) => join(specBase, p)) : null,
1827
1975
  })
1828
1976
  // 归档后 changeDir 已搬走——后续子步标记随归档目录走(flow-state 随变更包留存审计)
1829
1977
  markDir = destDir
@@ -1875,17 +2023,34 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1875
2023
  /** flow 命令族入口(index.js case 'flow' 接线):flow start|done|amend-draft。 */
1876
2024
  /**
1877
2025
  * flow approve(2026-10-04-thin-docs-v2 FR-06):spec 断点用户批准留痕——用户确认 FR+design
1878
- * 方案后由**用户**运行(不是 agent 代跑);写 flow-state {spec_approved,spec_approved_at,
1879
- * spec_approved_by}。flow done 的 v2 机器门消费该证据(未批准且未声明 autopilot → 拒收)。
1880
- * 诚实边界:同机 agent 理论上也能跑本命令——门的价值是仪式+留痕(谁在何时批的)+评审抽查
1881
- * 兜底,非绝对防伪;--by 显式署名供平台/脚本侧调用。
2026
+ * 方案后运行;写 flow-state {spec_approved,spec_approved_at,spec_approved_by}。flow done 的
2027
+ * v2 机器门消费该证据(未批准且未声明 autopilot → 拒收)。
2028
+ *
2029
+ * 通道 attest(2026-10-12-approve-channel-attest,dogfood 实证驱动:8 次 approve 中 2 次为
2030
+ * agent 自批、其中一次在用户明确拒绝方案后把重复催促反读为授权,且留痕层面用户真批转述与
2031
+ * agent 自批不可区分——spec_approved_by 恒为 agent session id):
2032
+ * --user-confirmed agent 转述「用户已确认方案」(channel=user-confirmed)
2033
+ * --self-approved agent 自批如实声明(channel=agent-self)
2034
+ * 无 flag 默认路径行为不变(零破坏),channel=unattested 留痕+警示
2035
+ * 拒后派生授权硬拦:spec_rejected 在场且未被显式翻越(无 spec_rejected_superseded_at)时,
2036
+ * approve 必须带显式通道——「用户拒绝过方案」之后的无通道批准正是实证事故形态。
2037
+ * 诚实边界:同机 agent 仍可谎报 --user-confirmed(与 --by 同界)——通道的价值是必留痕+
2038
+ * 可区分+评审抽查兜底,非防伪;强凭据属平台层课题(CLI 直连用户问询),不在此层伪造。
1882
2039
  */
1883
- async function cmdFlowApprove({ change, cwd, specBase, by = null }) {
2040
+ async function cmdFlowApprove({ change, cwd, specBase, by = null, channelFlag = null }) {
1884
2041
  const changeDir = join(specBase, 'changes', change)
1885
2042
  if (!existsSync(changeDir)) {
1886
2043
  console.error(`❌ 变更不存在:${change}(flow approve 在 flow start 之后运行)`)
1887
2044
  process.exit(2)
1888
2045
  }
2046
+ const st = readFlowState(changeDir) || {}
2047
+ if (st.spec_rejected && !st.spec_rejected_superseded_at && !channelFlag) {
2048
+ console.error(`❌ 本变更存在未翻越的拒绝记录(spec_rejected,reason=${String(st.spec_rejected_reason || '?').slice(0, 80)})——拒绝后的批准必须显式声明通道:`)
2049
+ console.error(` 用户重新确认后:sillyspec flow approve --change ${change} --user-confirmed(agent 转述用户确认)`)
2050
+ console.error(` agent 自批如实声明:sillyspec flow approve --change ${change} --self-approved`)
2051
+ console.error(` (拒绝后派生授权须显式再确认——2026-10-12-approve-channel-attest)`)
2052
+ process.exit(2)
2053
+ }
1889
2054
  let approvedBy = by
1890
2055
  if (!approvedBy) {
1891
2056
  try {
@@ -1893,11 +2058,64 @@ async function cmdFlowApprove({ change, cwd, specBase, by = null }) {
1893
2058
  approvedBy = (resolveSessionIdentity({ cwd }) || {}).session || 'user'
1894
2059
  } catch { approvedBy = 'user' }
1895
2060
  }
2061
+ const channel = channelFlag === 'user-confirmed' ? 'user-confirmed'
2062
+ : channelFlag === 'self-approved' ? 'agent-self' : 'unattested'
1896
2063
  const at = new Date().toISOString()
1897
- writeFlowState(changeDir, { spec_approved: true, spec_approved_at: at, spec_approved_by: approvedBy })
2064
+ const patch = {
2065
+ spec_approved: true, spec_approved_at: at, spec_approved_by: approvedBy,
2066
+ spec_approved_channel: channel,
2067
+ }
2068
+ if (channelFlag) patch.spec_attested_by = approvedBy
2069
+ else patch.spec_attested_by = null // 评审 P3① 清偿:无通道再批准清除旧声明者,防 unattested 态残留陈旧 attest
2070
+ if (st.spec_rejected && !st.spec_rejected_superseded_at) patch.spec_rejected_superseded_at = at
2071
+ writeFlowState(changeDir, patch)
1898
2072
  console.log(`✅ spec 断点已批准(change=${change})`)
1899
- console.log(` 批准人:${approvedBy}|时间:${at}`)
1900
- console.log(` flow done 的断点机器门已满足;agent 可继续执行/收口。撤销:编辑 flow-state.yaml 删除 spec_approved 三字段。`)
2073
+ const chLabel = channelFlag === 'user-confirmed' ? 'user-confirmed(agent 转述用户确认)'
2074
+ : channelFlag === 'self-approved' ? 'agent-self(agent 自批如实声明)' : 'unattested(未声明通道)'
2075
+ console.log(` 批准人:${approvedBy}|时间:${at}|通道:${chLabel}`)
2076
+ if (!channelFlag) {
2077
+ console.warn(`⚠️ 本次批准未带 attest 通道(--user-confirmed / --self-approved)——按 unattested 留痕,评审/审计无法区分用户确认与 agent 自批。`)
2078
+ }
2079
+ if (channelFlag === 'self-approved') {
2080
+ console.warn(`⚠️ agent 自批留痕:如用户实际未确认,请撤销——sillyspec flow reject --change ${change} --reason <理由>`)
2081
+ }
2082
+ if (patch.spec_rejected_superseded_at) console.log(` 已翻越既有拒绝记录(superseded_at=${patch.spec_rejected_superseded_at},拒绝理由保留于 spec_rejected_reason)`)
2083
+ console.log(` flow done 的断点机器门已满足;agent 可继续执行/收口。撤销:sillyspec flow reject --change ${change} --reason <理由>`)
2084
+ }
2085
+
2086
+ /**
2087
+ * flow reject(2026-10-12-approve-channel-attest):spec 断点拒绝留痕——撤销既有批准并记录
2088
+ * 拒绝理由(审计可复原「批准→用户否掉→显式再确认」序);此后 approve 必须带显式 attest 通道
2089
+ * (防「拒绝后派生授权」——dogfood 实证:用户拒绝方案后 agent 把重复催促反读为授权自批)。
2090
+ * spec_approved 撤销为显式 false(非删键)——重入读侧恒见;spec_rejected* 历史字段保留不删。
2091
+ */
2092
+ async function cmdFlowReject({ change, cwd, specBase, reason }) {
2093
+ const changeDir = join(specBase, 'changes', change)
2094
+ if (!existsSync(changeDir)) {
2095
+ console.error(`❌ 变更不存在:${change}(flow reject 在 flow start 之后运行)`)
2096
+ process.exit(2)
2097
+ }
2098
+ const reasonText = String(reason || '').trim()
2099
+ if (!reasonText) {
2100
+ console.error('❌ flow reject 需要 --reason <文本>(拒绝理由,留痕供审计)')
2101
+ process.exit(2)
2102
+ }
2103
+ let rejectedBy = 'user'
2104
+ try {
2105
+ const { resolveSessionIdentity } = await import('./progress.js')
2106
+ rejectedBy = (resolveSessionIdentity({ cwd }) || {}).session || 'user'
2107
+ } catch { /* 缺省 user */ }
2108
+ const at = new Date().toISOString()
2109
+ writeFlowState(changeDir, {
2110
+ spec_approved: false,
2111
+ spec_rejected: true, spec_rejected_at: at,
2112
+ spec_rejected_reason: reasonText, spec_rejected_by: rejectedBy,
2113
+ spec_rejected_superseded_at: null, // 再拒绝重置翻越戳(上一轮「拒→显式确认」序已被新拒绝终结)
2114
+ })
2115
+ console.log(`🚫 spec 断点已拒绝留痕(change=${change})`)
2116
+ console.log(` 理由:${reasonText}|操作者:${rejectedBy}|时间:${at}`)
2117
+ console.log(` 既有批准已撤销(spec_approved=false)——flow done 断点机器门将重新拦收。`)
2118
+ console.log(` 用户重新确认后批准须显式通道:flow approve --change ${change} --user-confirmed(agent 自批则 --self-approved 如实声明)`)
1901
2119
  }
1902
2120
 
1903
2121
  export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
@@ -2001,8 +2219,22 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2001
2219
  const change = getFlag('--change')
2002
2220
  if (!change) { console.error('❌ flow approve 需 --change <名>'); process.exit(2) }
2003
2221
  validateChangeName(change)
2222
+ // attest 通道(2026-10-12-approve-channel-attest):互斥二选一,无 flag=unattested(留痕+警示)
2223
+ const userConfirmed = hasFlag('--user-confirmed')
2224
+ const selfApproved = hasFlag('--self-approved')
2225
+ if (userConfirmed && selfApproved) {
2226
+ console.error('❌ --user-confirmed 与 --self-approved 互斥(一次只能声明一个 attest 通道)')
2227
+ process.exit(2)
2228
+ }
2229
+ await reportAgentLog(change, sub)
2230
+ return cmdFlowApprove({ change, cwd, specBase, by: getFlag('--by') || null, channelFlag: userConfirmed ? 'user-confirmed' : (selfApproved ? 'self-approved' : null) })
2231
+ }
2232
+ if (sub === 'reject') {
2233
+ const change = getFlag('--change')
2234
+ if (!change) { console.error('❌ flow reject 需 --change <名>'); process.exit(2) }
2235
+ validateChangeName(change)
2004
2236
  await reportAgentLog(change, sub)
2005
- return cmdFlowApprove({ change, cwd, specBase, by: getFlag('--by') || null })
2237
+ return cmdFlowReject({ change, cwd, specBase, reason: getFlag('--reason') })
2006
2238
  }
2007
2239
  if (sub === 'status') {
2008
2240
  // 三断点配套(2026-09-25-flow-checkpoints):随时可查当前变更阶段/槽位/子步进度
@@ -2148,7 +2380,7 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2148
2380
  console.log('🔄 --refreeze:patch 子步标记已重置,本次 done 将重新冻结(change.patch 按 baseline..HEAD 最新面重建)')
2149
2381
  }
2150
2382
  }
2151
- return cmdFlowDone({ change, cwd, specBase, runtimeRootOpt, freezeDirty: hasFlag('--freeze-dirty'), acceptDirtyGap: hasFlag('--accept-dirty-gap'), allowBatchTick: hasFlag('--allow-batch-tick') })
2383
+ return cmdFlowDone({ change, cwd, specBase, runtimeRootOpt, freezeDirty: hasFlag('--freeze-dirty'), acceptDirtyGap: hasFlag('--accept-dirty-gap'), acceptOrphanFace: hasFlag('--accept-orphan-face'), allowBatchTick: hasFlag('--allow-batch-tick'), acceptUnmapped: hasFlag('--accept-unmapped') })
2152
2384
  }
2153
2385
  if (sub === 'amend-draft') {
2154
2386
  // 机器稿唯一留痕修改通道(R7 切片三 / FR-08):重锚哈希 + ledger amendment 审计;