sillyspec 3.32.6 → 3.32.8

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
@@ -33,21 +33,15 @@ import { writeAtomicSync } from './fs-atomic.js'
33
33
  import { resolveRuntimeRoot, triggerSync, assertDatedChangeName } from './run/shared.js'
34
34
  import { detectUiTouch, buildUiGuidanceLines, runUiVisualProbe, readUiVisualGate, UI_EVIDENCE_FILENAME } from './ui-visual.js'
35
35
  import { runHunkAttributionGate, readHunkGate, renderHunkAttributionLines } from './hunk-attribution.js'
36
+ // 2026-10-13-thin-sync-stage:FLOW_STATE_FILE/readFlowState 单一源上移叶子模块 flow-state.js
37
+ // (progress.js 同步投影也要读;叶子化避免 progress.js 为读一个状态文件拉入本协议整条
38
+ // 依赖链——评审 P3 勘误:非静态循环依赖,本文件 import 闭包实测不达 progress.js);
39
+ // re-export 保持 `import('../flow.js')`(run/command.js 既有动态引用)导出面不变。
40
+ import { FLOW_STATE_FILE, readFlowState } from './flow-state.js'
41
+ export { FLOW_STATE_FILE, readFlowState }
36
42
 
37
- const FLOW_STATE_FILE = 'flow-state.yaml'
38
43
  const SUBSTEPS = ['artifacts', 'ledger', 'patch', 'review', 'probes', 'distill', 'archive', 'events']
39
44
 
40
- /** 读 flow-state(缺文件/损坏 → null = 未参与 thin)。 */
41
- export function readFlowState(changeDir) {
42
- try {
43
- const raw = readFileSync(join(changeDir, FLOW_STATE_FILE), 'utf8')
44
- const obj = yaml.load(raw)
45
- return obj && typeof obj === 'object' ? obj : null
46
- } catch {
47
- return null
48
- }
49
- }
50
-
51
45
  /** 原子写 flow-state(patch 合并,幂等)。 */
52
46
  export function writeFlowState(changeDir, patch) {
53
47
  const cur = readFlowState(changeDir) || {}
@@ -62,6 +56,17 @@ export function writeFlowState(changeDir, patch) {
62
56
  }, { lineWidth: 120 }) + '\n')
63
57
  }
64
58
 
59
+ /** 批准通道取值(2026-10-12-approve-channel-attest):flow done 回显与遥测同源——历史变更
60
+ * 无 channel 字段时归一 unattested-legacy(2026-10-12 之前真批/自批不可区分的留痕形态),
61
+ * autopilot 是用户显式豁免通道非批准。三态封闭集(user-confirmed/agent-self/unattested)是
62
+ * 治理域(谁在主张批准),不承载内容语义——开放内容走 spec_rejected_reason 自由文本。 */
63
+ export function approveChannelOf(st) {
64
+ const s = st || {}
65
+ if (s.autopilot) return 'autopilot'
66
+ if (s.spec_approved_channel) return String(s.spec_approved_channel)
67
+ return s.spec_approved ? 'unattested-legacy' : 'none'
68
+ }
69
+
65
70
  /** 读 local.yaml 的 flow 配置(缺省 thin——2026-09-25-thin-default-flip 翻转:入口归一,
66
71
  * D-010@v2 实验通道定位由轻量变更加固件(设计记录/测试绑定/patch 留档/预段收编)的数据推翻;
67
72
  * 显式 mode: legacy 的仓维持旧道。文本级读同既有习惯)。 */
@@ -73,13 +78,17 @@ export function readFlowConfig(specBase) {
73
78
  if (/^flow\s*:\s*(thin|legacy)\s*$/m.test(raw)) mode = raw.match(/^flow\s*:\s*(thin|legacy)\s*$/m)[1]
74
79
  const th = raw.match(/^\s*edit_ratio_threshold\s*:\s*([0-9.]+)\s*$/m)
75
80
  const en = raw.match(/^\s*edit_ratio_enforcement\s*:\s*(advisory|block)\s*$/m)
81
+ // module_gate_ignore(2026-10-12-thin-module-gate-and-feed):未登记目录软门的固定忽略清单,
82
+ // 单行逗号串(目录前缀,posix)——列表块形态不支持,readFlowConfig 同款逐键正则防线
83
+ const ig = raw.match(/^\s*module_gate_ignore\s*:\s*(.+)$/m)
76
84
  return {
77
85
  mode,
78
86
  editRatioThreshold: th ? Number(th[1]) : 0.5,
79
87
  editRatioEnforcement: en ? en[1] : 'advisory',
88
+ moduleGateIgnore: ig ? ig[1].split(',').map((s) => s.trim()).filter(Boolean) : [],
80
89
  }
81
90
  } catch {
82
- return { mode: 'thin', editRatioThreshold: 0.5, editRatioEnforcement: 'advisory' }
91
+ return { mode: 'thin', editRatioThreshold: 0.5, editRatioEnforcement: 'advisory', moduleGateIgnore: [] }
83
92
  }
84
93
  }
85
94
 
@@ -310,6 +319,16 @@ export async function flowKnowledgeDigest({ specBase, change, changeDir, input,
310
319
  lines.length = 0
311
320
  lines.push(`🧠 知识注入(轻量道读取面):语料未命中知识库(无触达域依据、无否决命中、INDEX 无对齐条目)——可自行查 knowledge/INDEX.md。`)
312
321
  }
322
+ // 模块卡欠账提示前移(2026-10-12-thin-module-gate-and-feed 三档):触碰命中模块 behind>0
323
+ // 才出事实行——动手前看到「你要动的卡已落后」比收口时警告有效;computeDocsDebt(厚道
324
+ // execute {DOCS_DEBT})同源,空债/无图零输出,异常走外层 catch fail-open 不阻断 start。
325
+ if (touched.length > 0) {
326
+ try {
327
+ const { renderThinDocsDebt } = await import('./flow-parity.js')
328
+ const debtLines = renderThinDocsDebt({ specBase, projectRoot: dirname(specBase), changedFiles: touched })
329
+ if (debtLines.length > 0) lines.push(...debtLines)
330
+ } catch { /* 欠账计算 fail-open */ }
331
+ }
313
332
  } catch (e) {
314
333
  lines.length = 0
315
334
  lines.push(`🧠 知识注入(轻量道读取面):注入失败(${(e && e.message) || e})——可自行读 knowledge/INDEX.md 与 knowledge/fr/`)
@@ -511,6 +530,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
511
530
  `requirements 测试绑定槽(收编追加)每条 FR 至少一行作答;写码前后顺手填。`,
512
531
  `✅ 任务面归你(工作分解契约):tasks.md 是你的工作队列(机器种子可增删改,保持 \`- [ ] task-NN:\` 行形态)——CLI 以 checkbox 为进度状态机(task tick/flow status 实时读它),TodoWrite 类工具不替代;`,
513
532
  ` 勾选是收口证据面(逐格核提交 token/review.json):做一件 → 勾一格(task tick 即时回显进度与下一任务)→ 继续下一条,勿攒一把勾;`,
533
+ ` 任务拟法:收口/推送/发布类动作不单列 task(协议自身承担,成功标准照写 requirements);确需单列的,勾选前先有消息预埋 task-NN token 的交付提交(回执类证据写进提交消息)。`,
514
534
  ` flow status --change ${change} 为自愿查看/恢复面(非协议必需,D-007 中间零必需交互;恢复时给下一任务指针与进度)。`,
515
535
  `⚠️ 交付纪律:收口前交付代码显式 pathspec 提交——冻结件范围=baseline..HEAD,未提交不进审计件。`,
516
536
  ``,
@@ -748,6 +768,53 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
748
768
  try {
749
769
  digest = await flowKnowledgeDigest({ specBase, change, changeDir, input, filesOverride: await extractRoutingInputPaths(cwd, specBase, input) })
750
770
  } catch { /* 注入 best-effort 不阻断 start */ }
771
+ // 跨仓清单写法提示(2026-10-13-thin-empty-face-crosscheck):注册仓非空时提示段头契约——
772
+ // thin 通道此前零跨仓指引(fire-ledger-form-strict 实证:契约只活在厚流程模板里,裸写仓目录
773
+ // 前缀撞空面即静默丢对账)。注册为空 → 零新增输出(横幅逐字节不变)。
774
+ let crossRepoHintLines = []
775
+ try {
776
+ const { loadRegisteredRepoKeys } = await import('./scope-audit.js')
777
+ const _repoKeys = loadRegisteredRepoKeys(specBase)
778
+ if (_repoKeys && _repoKeys.length > 0) crossRepoHintLines = [
779
+ `🌐 跨仓交付:清单跨仓文件另用段头「## <注册key> 仓变更」段内写仓根相对路径(或子段「### <注册key>」/条目前缀 cross-repo:<注册key>:)——key 以 local.yaml repos 注册为准(本仓注册 ${_repoKeys.length} 个);`,
780
+ ` 裸写「仓目录名/路径」前缀不触发跨仓对账与冻结(收口空面门将点名阻断)。`,
781
+ ``,
782
+ ]
783
+ } catch { /* 注册仓读取失败 → 零提示(与无注册仓同形态) */ }
784
+ // FR 域预判(2026-10-13-batchtick-evidence-domain-hint):--input 提及路径 × 模块图(discoverModuleIndex
785
+ // 读 docs/<project>/modules/_module-map.yaml——与 distill 域路由同一匹配面)。第一次知道「路径未登记」
786
+ // 不该是收口模块门(最贵时点)。纯提示不设门:input 路径可能不全,路由真相以实际交付文件为准;
787
+ // 绿地无图/零路径 → 零新增输出(与模块门「绿地无图不拦」语义对齐)。fr-index.js 零编辑——
788
+ // resolveTouchedDomains(内部即 pseudoDomainFromPaths+词典守卫)复用 distill 同一伪域回退链。
789
+ let domainHintLines = []
790
+ try {
791
+ const _routingFace = await extractRoutingInputPaths(cwd, specBase, input)
792
+ if (_routingFace.length > 0) {
793
+ const { discoverModuleIndex } = await import('./decision-distill.js')
794
+ const { matchedModuleIds, resolveTouchedDomains } = await import('./fr-index.js')
795
+ const _knowledgeRoot = join(specBase, 'knowledge')
796
+ const _moduleIndex = discoverModuleIndex(_knowledgeRoot)
797
+ if (_moduleIndex && Object.keys(_moduleIndex).length > 0) {
798
+ const _hitRows = []
799
+ const _missFiles = []
800
+ for (const f of _routingFace) {
801
+ const ids = matchedModuleIds(_moduleIndex, f)
802
+ if (ids.length > 0) _hitRows.push({ f, ids })
803
+ else _missFiles.push(f)
804
+ }
805
+ if (_hitRows.length > 0 || _missFiles.length > 0) {
806
+ const _lines = ['🗺️ FR 域预判(按 --input 提及路径;最终以收口实际交付文件路由为准):']
807
+ for (const r of _hitRows) _lines.push(` ✓ ${r.f} → fr/${r.ids.join('、')}`)
808
+ if (_missFiles.length > 0) {
809
+ const _pseudo = (resolveTouchedDomains(changeDir, _moduleIndex, _missFiles, _knowledgeRoot)[0]) || 'unmapped'
810
+ _lines.push(` ⚠️ ${_missFiles.slice(0, 5).join('、')}${_missFiles.length > 5 ? ' 等' : ''} → 未命中知识模块图,FR 蒸馏将落域 ${_pseudo}(按 distill 同一伪域回退链预判——未吸附为 auto-<段>,词典吸附命中既有真域)`)
811
+ _lines.push(' 建议现在登记模块条目(docs/<project>/modules/_module-map.yaml 的 modules.<id>.paths 覆盖该目录)——登记后蒸馏自动落真域(收口模块门仍会兜底拦未登记目录)')
812
+ }
813
+ domainHintLines = _lines
814
+ }
815
+ }
816
+ }
817
+ } catch { /* 域预判 best-effort:图/路径读取失败零输出 */ }
751
818
  const lines = [
752
819
  `🏃 flow start(${thick ? 'thick 厚档(--thick 显式声明,人声明不做启发式)' : 'thin 轻量跑道'}): ${change}`,
753
820
  `══════════════════════════════════════`,
@@ -770,6 +837,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
770
837
  ` 文档内零指纹标记、零槽注释、零书写指导(规则以本横幅与命令卡为唯一源);成功标准漂移只出 advisory 不拒收。`,
771
838
  `⚠️ design.md 四节动码前后顺手作答;列改动文件用独立「## 文件变更清单」节+表格(| 操作 | 路径 | 说明 |)`,
772
839
  ` ——勿写在「接口契约」节内(收口按节名解析声明面)。`,
840
+ ...crossRepoHintLines,
773
841
  `📜 requirements 每条 FR = 标题锚 + 行为句 + 按需场景块(#### 场景:名 + Given/When/Then);`,
774
842
  ` 测试绑定节每条 FR 一行 \`FR-NN: test/路径「用例」\`(无测试面写「不适用:理由」)。`,
775
843
  `📦 冻结面在 flow done 时点采集(baseline..HEAD 提交面):未提交交付文件——会话专属 worktree 自动并入;`,
@@ -787,12 +855,18 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
787
855
  ` (sillyspec task tick --change ${change} --task task-NN 即时回显进度 N/M 与下一任务指针,flow status 同源;或直接 Edit 翻格 \`- [ ]\`→\`- [x]\`),watcher 实时上平台。`,
788
856
  ` ⚠️ harness 的 TodoWrite 类工具是会话内便利面,不替代 tasks.md——平台进度/收口证据只读它(0/12 事故实证:todo 全 completed 而 tasks.md 0 勾);`,
789
857
  ` 勾选是收口证据面:每格勾选要有对应提交(消息带 task-NN——逐个列出或 /、,连写如 task-01/02/03,「task-01..task-05」区间写法不认)或 review.json,全勾但零证据会被拒收;勿攒一把勾(收口硬门拒单拍多格);`,
858
+ // 任务拟法指引(2026-10-12-closeout-task-guidance,三次「哨兵抓收口型任务零证据」复盘):
859
+ // 成功标准≠工作分解——收口/推送/发布是协议自身承担的动作;单列则证据(token 提交)必须先于勾选存在。
860
+ ` 任务拟法:收口/推送/发布类动作不单列 task(提交、归档、推送是 flow start→done 协议自身承担的动作——成功标准照写 requirements,勿逐字搬进任务清单);确需单列的收口/操作型任务,勾选前先做消息预埋 task-NN token 的交付提交——回执类证据(push/publish 回执、构建日志路径)写进提交消息,勿依赖勾选后补;`,
790
861
  ` flow status --change ${change} 为自愿查看/恢复面(恢复时给下一任务指针与进度)。`,
791
862
  ``,
792
863
  `🛑 三断点纪律(可控性要求——用户没说「全跑完」就必须在每个断点向用户汇报并等确认):`,
793
864
  ` ① spec 断点【机器门】:填完 FR 区和 design 四节后,把摘要给用户看(FR 条目+盲维作答+方案概述),`,
794
865
  ` 用户确认后请其运行 sillyspec flow approve --change ${change}(批准留痕)再动手写代码——`,
795
- ` 方案错了返工最贵;未批准且未声明 --autopilot 时 flow done 拒收(2026-10-04-thin-docs-v2)。`,
866
+ ` agent 代跑时如实声明通道:--user-confirmed(转述用户已确认)/ --self-approved(agent 自批),`,
867
+ ` 无 flag 按 unattested 留痕(审计面无法区分用户确认与自批)——2026-10-12-approve-channel-attest。`,
868
+ ` 方案错了返工最贵;未批准且未声明 --autopilot 时 flow done 拒收(2026-10-04-thin-docs-v2);`,
869
+ ` 用户否掉方案 → flow reject --change ${change} --reason <理由>(拒后批准须显式通道再确认)。`,
796
870
  ` ② 执行断点:写完代码跑完测试后,把测试结果(过了几个/挂了什么)给用户看,`,
797
871
  ` 等用户确认再跑收口。`,
798
872
  ` ③ 归档断点:flow done 跑完后(无论过/拒),把结果(归档成功/被什么拦了)给用户看。`,
@@ -810,6 +884,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
810
884
  ...materials.map((p) => ` - ${p}`),
811
885
  ``,
812
886
  ...digest.lines,
887
+ ...(domainHintLines.length > 0 ? ['', ...domainHintLines] : []),
813
888
  ]
814
889
  if (json) {
815
890
  console.log(JSON.stringify({ change, tier: thick ? 'thick' : 'thin', baseline, materials, knowledgeDigest: digest.summary }))
@@ -872,7 +947,7 @@ export function remainingSubstepsAtFail({ snapshotSubsteps = {}, doneList = [],
872
947
  return SUBSTEPS.filter((k) => k !== failed && snapshotSubsteps?.[k] !== 'done' && !doneNow.has(k))
873
948
  }
874
949
 
875
- export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null, confirmArchive = true, freezeDirty = false, acceptDirtyGap = false, allowBatchTick = false }) {
950
+ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null, confirmArchive = true, freezeDirty = false, acceptDirtyGap = false, acceptOrphanFace = false, allowBatchTick = false, acceptUnmapped = false, acceptEmptyFace = false }) {
876
951
  const changeDir = join(specBase, 'changes', change)
877
952
  const st = readFlowState(changeDir)
878
953
  if (!st) {
@@ -906,6 +981,10 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
906
981
  const skip = (k) => { doneList.push(`${k}(skip)`) }
907
982
  let reviewOutcome = null
908
983
  let gateSummaryText = null
984
+ // 模块对账面(2026-10-12-thin-module-gate-and-feed):patch 子步算出后供 distill 回写消费;
985
+ // 断点续跑 patch 已 done 时从 change-patch.json files[] 回读重算(单一时点口径,不随图回写漂移)
986
+ let moduleScope = null
987
+ let changelogAppended = []
909
988
  // 遥测单点(2026-09-25-thin-parity-assets 修评审 P2①:失败路径 exit 前也要落账——校准信号
910
989
  // 不许在失败面丢失;成功收口走函数末尾同款记录,失败面只多不少)
911
990
  const appendTelemetry = (review) => {
@@ -914,6 +993,9 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
914
993
  ts: new Date().toISOString(), change, protocolCalls: 2,
915
994
  draftAmendments: st.edit_ratio != null ? 1 : 0, editRatio: st.edit_ratio ?? null,
916
995
  routeHint: st.route_hint ?? null, tier: st.tier, upgraded: st.upgrade_reason ?? null,
996
+ // 批准通道遥测(2026-10-12-approve-channel-attest):与收口回显同源取值,校准
997
+ // unattested/agent-self 占比用(dogfood 实证留痕不可区分 → 通道可聚合)。
998
+ approveChannel: approveChannelOf(st),
917
999
  review,
918
1000
  }) + '\n', 'utf8')
919
1001
  } catch { /* 遥测 best-effort */ }
@@ -933,6 +1015,9 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
933
1015
  const r = verifyFlowDrafts({ changeDir, change, runtimeRoot, skipDesign: adoptedDesign })
934
1016
  if (r.applicable && r.schema === 2) {
935
1017
  // v2 纯 markdown 轨(2026-10-04-thin-docs-v2):violations 拒收 → 漂移 advisory → spec 断点机器门
1018
+ // 问题行机器回填留痕(2026-10-13-design-question-autorepair):先于拒收渲染(评审 P3 清偿:
1019
+ // 混合拒收场景回填已落盘,若渲染在 exit(1) 之后则留痕不可达且幂等重跑后永久丢失)。
1020
+ for (const p of r.repairs || []) console.log(`🔧 [design 问题行机器回填] ${p}`)
936
1021
  if (r.violations.length > 0) {
937
1022
  console.error(`❌ 工件校验拒收(v2 文档↔锚对比):`)
938
1023
  for (const v of r.violations) console.error(` - ${v}`)
@@ -946,11 +1031,27 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
946
1031
  if (!st.autopilot && !st.spec_approved) {
947
1032
  console.error(`❌ spec 断点未批准:本变更(v2 起草)收口需要方案确认留痕`)
948
1033
  console.error(` 修复:把 FR 条目+design 四节摘要给用户看,用户确认后由用户运行:`)
949
- console.error(` sillyspec flow approve --change ${change}`)
1034
+ console.error(` sillyspec flow approve --change ${change} [--user-confirmed|--self-approved]`)
950
1035
  console.error(` 或用户显式豁免(自主跑到底):重跑 flow start --change ${change} --autopilot(声明留痕)`)
951
1036
  reportMidFail('artifacts')
952
1037
  process.exit(1)
953
1038
  }
1039
+ // 批准通道回显(2026-10-12-approve-channel-attest):channel 是审计面第一公民——agent-self
1040
+ // 与 unattested 醒目(dogfood 实证:用户真批转述与 agent 自批在留痕层面曾不可区分)。
1041
+ if (st.autopilot) {
1042
+ console.log('🔑 spec 断点:autopilot 显式豁免(用户授权自主跑到底)')
1043
+ } else {
1044
+ const _ch = approveChannelOf(st)
1045
+ const _warn = _ch === 'agent-self' || _ch.startsWith('unattested')
1046
+ // 评审 P3② 清偿:翻越语只在 superseded 戳在场时陈述——并发窄窗混合态(approved 真 +
1047
+ // 拒绝未翻越)不得假称「已显式再确认」,按状态异常如实告警。
1048
+ const _rej = st.spec_rejected_reason
1049
+ ? (st.spec_rejected_superseded_at
1050
+ ? `(曾拒绝:${String(st.spec_rejected_reason).slice(0, 60)}→显式再确认翻越)`
1051
+ : `(⚠️ 存在未翻越拒绝记录(spec_rejected 未 superseded)——状态异常,请核查 flow-state)`)
1052
+ : ''
1053
+ console.log(`${_warn ? '⚠️' : '🔑'} spec 断点批准留痕:channel=${_ch}|by=${st.spec_approved_by || '?'}${_rej}`)
1054
+ }
954
1055
  } else {
955
1056
  if (r.applicable && r.violations.length > 0) {
956
1057
  console.error(`❌ 工件校验拒收(机器稿指纹三态):`)
@@ -990,6 +1091,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
990
1091
  // 哨兵断言(2026-09-25-sentinel-wiring):tasks.md 全勾但零完成证据(区间提交消息标题与正文
991
1092
  // 均无 task-NN token 且无对应 review.json)→ 拒收——L0 硬门接线,两道收口同一哨兵(quick 侧同判)。
992
1093
  let _sentinelNonMirrorTasks = null // 镜像豁免面外的任务数(null=哨兵未跑;0=纯镜像任务面——节奏 advisory 静默)
1094
+ let _sentinelEvidenceComplete = false // 哨兵证据完备(status=complete——每格已勾任务均有 token/review.json):单拍降级判据(2026-10-13-batchtick-evidence-domain-hint)
993
1095
  let _autopilotTicked = 0 // 机器代勾格数(>0 时勾选节奏门降 advisory——代勾是单拍多格机械写,非 agent 纪律面)
994
1096
  // 证据面=整条提交消息(2026-09-25-thin-done-gate-calibration 坑2:%s 只取标题行,正文里的
995
1097
  // token 被判零证据,与文案「提交带 task-NN」口径漂移)——%B%x1e 按提交切记录,advisory 的
@@ -1046,12 +1148,17 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1046
1148
  // token 合法形态写明(2026-10-11-flow-start-redline-frontload):实证「task-01..task-05」
1047
1149
  // 区间写法被判零证据时才知道格式约束——拒收文案自释,减少一轮试错。
1048
1150
  console.error(' 补证据(提交标题或正文带 task-NN——逐个列出或 /、,连写(如 task-01/02/03),「task-01..task-05」区间写法不认;或产 review.json)或取消勾选后重跑——假完成主张不许过门')
1151
+ // 扫描面透明化(2026-10-12-cli-friction-signal,dogfood 实证:跨仓交付的 agent 靠猜
1152
+ // 证据该落在哪)——声明哨兵只扫工作区仓与变更目录,跨仓仓提交天然不在面内。
1153
+ console.error(` 证据扫描面:工作区仓 git log ${st.baseline_commit ? String(st.baseline_commit).slice(0, 10) : 'baseline'}..HEAD 的提交消息(标题+正文)+ 变更目录内 review.json;`)
1154
+ console.error(' 跨仓仓(register-repo 登记的外部仓)的提交不在扫描面——交付提交在其他仓时,须在工作区仓的提交消息带 task-NN(如治理工件提交)或产出 review.json 补证。')
1049
1155
  appendTelemetry({ sentinel: 'fake', missing: sent.missing.length })
1050
1156
  reportMidFail('ledger')
1051
1157
  process.exit(1)
1052
1158
  }
1053
1159
  _sentinelNonMirrorTasks = sent.claimTotal
1054
1160
  if (sent.status === 'complete') {
1161
+ _sentinelEvidenceComplete = true
1055
1162
  console.log(`🛡️ 哨兵:全勾 ${sent.checked}/${sent.claimTotal} 证据齐(提交 token/review.json)`)
1056
1163
  // 勾选时点判定(2026-09-26-tick-loop-nudge,R19 行为发现):tasks.md 的 git 首次提交
1057
1164
  // == 收口窗口最后提交(一把勾模式)→ 放行但打行为提醒(不阻断——token 证据已验,
@@ -1101,16 +1208,24 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1101
1208
  // 单源(resolveBatchTickAction)四态与豁免优先序见其 JSDoc——代勾须解释整跳才降级
1102
1209
  const _persistedAuto = Number(st?.autopilot_ticked) || 0
1103
1210
  const _autoN = Math.max(_autopilotTicked, _persistedAuto)
1104
- const v = resolveBatchTickAction({ batchTick: batch, nonMirrorCount: _sentinelNonMirrorTasks, allowBatchTick, autopilotTicked: _autoN })
1211
+ const v = resolveBatchTickAction({ batchTick: batch, nonMirrorCount: _sentinelNonMirrorTasks, allowBatchTick, autopilotTicked: _autoN, evidenceComplete: _sentinelEvidenceComplete })
1105
1212
  const at = Number.isFinite(batch.ts) ? new Date(batch.ts).toLocaleTimeString() : '未知时刻'
1106
1213
  if (v.action === 'bypass') {
1107
1214
  try { writeFlowState(changeDir, { allow_batch_tick: true }) } catch { /* 留痕 best-effort */ }
1108
1215
  console.warn(`⚠️ --allow-batch-tick:单拍多格勾选(${batch.detail},${at})硬门显式旁路——留痕 flow-state 与平台时间线`)
1109
1216
  } else if (v.action === 'advisory') {
1110
- const why = v.reason === 'autopilot-ticked'
1111
- ? `机器代勾 ${_autoN} 格(governance-autopilot 单拍机械写,非 agent 纪律面;本拍 ${_autopilotTicked}/持久化 ${_persistedAuto})`
1112
- : '哨兵任务面未知(fail-open 防误拒)'
1113
- console.warn(`⚠️ 勾选节奏:单拍多格勾选(${batch.detail},${at})——${why},降级提醒不拒`)
1217
+ // 证据完备降级(2026-10-13-batchtick-evidence-domain-hint):同形态 git 时点检查(一把勾
1218
+ // 模式)因 token 证据齐放行——事件流门对齐该语义;损害只剩 per-task 时间戳信号面,advisory
1219
+ // + flow-state 留痕与损害对齐。证据缺失形态不走此态(哨兵先拒 / 决策函数维持 reject)。
1220
+ if (v.reason === 'evidence-complete') {
1221
+ try { writeFlowState(changeDir, { batch_ticked: true }) } catch { /* 留痕 best-effort */ }
1222
+ console.warn(`⚠️ 勾选节奏:单拍多格勾选(${batch.detail},${at})——哨兵证据完备(每格已勾任务均有 token 提交/review.json),降级提醒不拒(batch_ticked 已留痕;下次完成单元即勾)`)
1223
+ } else {
1224
+ const why = v.reason === 'autopilot-ticked'
1225
+ ? `机器代勾 ${_autoN} 格(governance-autopilot 单拍机械写,非 agent 纪律面;本拍 ${_autopilotTicked}/持久化 ${_persistedAuto})`
1226
+ : '哨兵任务面未知(fail-open 防误拒)'
1227
+ console.warn(`⚠️ 勾选节奏:单拍多格勾选(${batch.detail},${at})——${why},降级提醒不拒`)
1228
+ }
1114
1229
  } else if (v.action === 'reject') {
1115
1230
  console.error(`🚫 单拍勾选拒收:事件流观测到一拍勾选 ${batch.detail}(${at})——勾选纪律要求逐格(做一件→勾一格→下一个),勾选任务 ${_sentinelNonMirrorTasks} 格`)
1116
1231
  console.error(' 出口:①节奏违例已既成——认知后重跑带 --allow-batch-tick 显式留痕过门(平台时间线可见旁路);②疑观测误判→sillyspec doctor 核对事件流')
@@ -1286,10 +1401,11 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1286
1401
  // 治理面等价保留(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop
1287
1402
  // 用户实证):评审结论的对象是交付面——窗口内本变更提交只碰 .sillyspec/** 治理文件且
1288
1403
  // 未触本变更 requirements/design(承诺面也是评审对象)时,结论对最新交付面逐字节等价,
1289
- // 保留不隔离不重置标记;change.patch 仍走下方 driftRefreeze 自动重冻结吸收治理面增量
1404
+ // 保留不隔离不重置标记;change.patch 仍走下方 driftRefreeze 自动重冻结(FR-01 后治理
1405
+ // 工件不入冻结面——治理修复的留档真相=归档目录本体文件,重冻结保交付面时点对齐)
1290
1406
  // (审计件覆盖最新提交面不变量不破)。此前任何后缀提交一律隔离重评——评审后修一个
1291
1407
  // P2 文档措辞也付整轮子代理评审费(循环,被迫手工重排执行顺序自赎)。
1292
- console.log(` review.json 保留不隔离——窗口内本变更提交仅触及治理文件(${drift.ownFiles.length} 个,未触 requirements/design),评审对象(交付面)未变,结论对最新面等价;change.patch 自动重冻结吸收治理面增量`)
1408
+ console.log(` review.json 保留不隔离——窗口内本变更提交仅触及治理文件(${drift.ownFiles.length} 个,未触 requirements/design),评审对象(交付面)未变,结论对最新面等价;change.patch 自动重冻结(治理工件不入冻结面,治理修复随归档目录本体留档)`)
1293
1409
  } else {
1294
1410
  const ts = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, '')
1295
1411
  try {
@@ -1318,11 +1434,14 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1318
1434
  }
1319
1435
  if (st.substeps?.patch !== 'done' || driftRefreeze) {
1320
1436
  let patchOk = false
1437
+ let orphanGapCount = 0
1321
1438
  try {
1322
- // patch 面 = 本变更可归属变化(2026-09-25-thin-patch-scope-fix 收窄:dirty 全扫面会把并行
1323
- // 会话未声明 WIP 冻结进来——上变更实测 49 文件泄漏):①baseline..HEAD 提交面(.sillyspec/
1324
- // 治理面只留本变更目录,他侧 quicklog/knowledge WIP 不入)②本变更目录全部工作树件(未提交
1325
- // 治理面/槽位作答)——排除 patch 自引用。提交面再过他侧声明切分(他侧同窗口提交的防线)。
1439
+ // patch 面 = 本变更可归属的交付面(2026-10-12-close-trace-freeze-integrity FR-01 收敛):
1440
+ // ①baseline..HEAD 提交面过滤 .sillyspec/(只留 docs/ 交付文档——治理工件目录不入冻,
1441
+ // 对齐 execute --done 沉淀资产面投影;工件真相随归档目录本体入库,patch 再冻是冗余且
1442
+ // 掩盖「可归属面为空」哨兵——ws-ref-ws-name +137/-0 全工件 0 代码实证)②独占树未提交
1443
+ // 交付面(worktree/--freeze-dirty)。他侧 quicklog/knowledge WIP 不入;提交面再过他侧
1444
+ // 声明切分(他侧同窗口提交的防线)。
1326
1445
  if (!st.baseline_commit) {
1327
1446
  console.log('📦 patch 留档跳过:无 git 基线(无历史仓)')
1328
1447
  patchOk = true
@@ -1383,18 +1502,6 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1383
1502
  if (dirtyForeign.length > 0) {
1384
1503
  console.warn(`⚠️ patch 留档:${dirtyForeign.length} 个 dirty 文件被其他活跃变更声明排除(${dirtyForeign.slice(0, 3).map((x) => `${x.file}←${x.owners[0]}`).join(', ')}${dirtyForeign.length > 3 ? ' 等' : ''})——如有误(陈旧声明),清理该旧变更或用 --refreeze 重冻结`)
1385
1504
  }
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
1505
  // 冻结面收集(2026-09-25-thin-r16-patches 修复①):committed 为主;会话专属 worktree
1399
1506
  // (gate-snapshot 同款路径判定)下未提交 dirty 交付面一并入冻结——独占树内全归属本变更;
1400
1507
  // 共享主仓 dirty 无法归属只警告(他侧声明免警告),审计缺口显式化不再静默。
@@ -1443,26 +1550,119 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1443
1550
  // change-patch.json 两件,但旧轮冻结件里可能仍有 scope-audit.json/.patch——
1444
1551
  // legacy 防护保留):上一轮留痕件作为 untracked 新文件被全文自嵌入即自引用循环
1445
1552
  // (旧 patch 内容混进新 patch,三评 P1 根因)。
1446
- const ownFiles = [...new Set([...freeze.files, ...changeDirFiles])]
1553
+ const ownFiles = [...new Set([...freeze.files])]
1447
1554
  .filter((f) => f && !f.endsWith('change.patch') && !f.endsWith('change-patch.json')
1448
1555
  && !f.endsWith('scope-audit.json') && !f.endsWith('scope-audit.patch'))
1556
+ // 独占 worktree 冻结完备性守卫(2026-10-12-close-trace-freeze-integrity FR-02):
1557
+ // 事故形态(multi-agent-platform ws-ref-ws-name 取证):实现提交在会话 worktree 内完成
1558
+ // 但不在 flow done 时点 HEAD 的祖先链(reset/detach/平行分支)——baseline..HEAD 提交面
1559
+ // 与 git status 脏面都看不到,冻结面静默缺实现。本 worktree 的 HEAD reflog 会话独占
1560
+ // (linked worktree 独立 reflog),扫全部轨迹:不在 HEAD 祖先链且触及交付面的提交,
1561
+ // patch-id 与 baseline..HEAD 提交等价(amend/rebase 残留)视为已覆盖;否则阻断收口
1562
+ // (与 dirty gate 同款断点续语义)。共享主仓路径明确不启用——reflog 混他侧会话操作
1563
+ // 无法归属,--freeze-dirty 手动声明只覆盖脏面不进 reflog 扫描。
1564
+ if (exclusiveFrom === 'worktree') {
1565
+ try {
1566
+ const { scanOrphanFace } = await import('./flow-parity.js')
1567
+ const orphan = scanOrphanFace(cwd, { baseline: st.baseline_commit })
1568
+ if (orphan.files.length > 0 && !acceptOrphanFace) {
1569
+ console.error(`⛔ 会话专属 worktree 存在冻结面外交付改动:${orphan.commits.length} 个提交不在 HEAD 祖先链(patch-id 不等价)触及 ${orphan.files.length} 个交付文件——收口阻断(不归档;处置后重跑同一条命令从断点续):`)
1570
+ console.error(` 提交:${orphan.commits.slice(0, 5).map((c) => c.slice(0, 10)).join('、')}${orphan.commits.length > 5 ? ' 等' : ''}`)
1571
+ console.error(` 文件:${orphan.files.slice(0, 5).join('、')}${orphan.files.length > 5 ? ' 等' : ''}`)
1572
+ console.error(` ① git rebase/cherry-pick 这些提交到 HEAD(补齐冻结面)后重跑`)
1573
+ console.error(` ② 确认放弃这些提交:清理对应分支/reflog 源后重跑`)
1574
+ console.error(` ③ 接受缺口留痕:flow done --change ${change} --accept-orphan-face`)
1575
+ reportMidFail('patch')
1576
+ process.exit(1)
1577
+ }
1578
+ if (orphan.files.length > 0 && acceptOrphanFace) {
1579
+ orphanGapCount = orphan.files.length
1580
+ console.warn(`⚠️ --accept-orphan-face:显式接受 ${orphan.commits.length} 个冻结面外提交的 ${orphan.files.length} 个交付文件不进冻结面(缺口数随 change-patch.json acceptedOrphanFace 留痕)`)
1581
+ }
1582
+ } catch { /* 守卫异常不阻断(fail-safe 不新造阻断面) */ }
1583
+ }
1584
+ // ── 声明面自证前置(2026-10-13-thin-empty-face-crosscheck):planEntries 解析与跨仓防呆
1585
+ // 告警提到 ownFiles 判空之前——空面也要跑。fire-ledger-form-strict 实证(multi-agent-platform
1586
+ // docs/sillyspec/thin-done-empty-face-no-declared-crosscheck.md 37c7d146b):跨仓交付撞空面时
1587
+ // 本段整挂在非空 else 分支内从未执行,归档件永久缺 patch 且零告警;空面恰是高危形态。
1588
+ let planEntries = []
1589
+ try {
1590
+ const { parseFileChangeListDetailed } = await import('./change-list.js')
1591
+ // repoKeys(2026-10-10-cross-repo-patch-freeze D-002@v1):注册仓清单传入——跨仓
1592
+ // 子段/cross-repo: 前缀条目才带 repo 标注(不传则跨仓声明全落主仓口径, thin 跨仓
1593
+ // 对账无从谈起)
1594
+ const { loadRegisteredRepoKeys } = await import('./scope-audit.js')
1595
+ planEntries = parseFileChangeListDetailed(join(changeDir, 'design.md'), { keepSillyspecDocs: true, repoKeys: loadRegisteredRepoKeys(specBase) })
1596
+ } catch { /* 清单缺失/不可解析 → 实际侧 only 快照行(无 verdict,与主链路降级同语义) */ }
1597
+ // 跨仓声明防呆(坑 design-filelist-crossrepo-prefix-silent-main):嫌疑判定从「首段命中
1598
+ // 注册仓」放宽为「声明路径主仓不存在即点名」(仓根相对路径形态首段不命中是盲区);
1599
+ // 首段命中注册仓的条目额外标注 key,点名+三种正确语法指引。
1600
+ const _declared = []
1601
+ for (const e of planEntries) {
1602
+ const pe = String((e && e.path) || '').split('\\').join('/')
1603
+ if (!pe || pe.startsWith('.sillyspec/')) continue
1604
+ if (e.repo) continue
1605
+ _declared.push({ path: pe })
1606
+ }
1607
+ let _repoAliases = null
1608
+ try {
1609
+ const _yamlText = existsSync(join(specBase, 'local.yaml')) ? readFileSync(join(specBase, 'local.yaml'), 'utf8') : ''
1610
+ if (_yamlText) {
1611
+ const { parseRepoRegistry } = await import('./stages/plan-postcheck.js')
1612
+ const _repos = parseRepoRegistry(_yamlText)
1613
+ if (_repos && _repos.size > 0) {
1614
+ _repoAliases = new Map()
1615
+ for (const [k, v] of _repos) {
1616
+ _repoAliases.set(k, k)
1617
+ const norm = String(v || '').split('\\').join('/')
1618
+ const bn = norm.replace(/\/+$/, '').split('/').pop()
1619
+ if (bn) _repoAliases.set(bn, k)
1620
+ }
1621
+ }
1622
+ }
1623
+ } catch { /* 注册表读取失败 → 别名标注缺席,存在性判定不受影响 */ }
1624
+ const _suspects = []
1625
+ for (const d of _declared) {
1626
+ if (existsSync(join(cwd, d.path))) continue
1627
+ _suspects.push({ path: d.path, key: (_repoAliases && _repoAliases.get(d.path.split('/')[0])) || null })
1628
+ }
1629
+ if (_suspects.length > 0) {
1630
+ console.warn(`⚠️ 疑似跨仓声明未按规范标注(${_suspects.length} 条已按主仓路径对账):${_suspects.slice(0, 5).map((x) => `${x.path}(主仓不存在${x.key ? `,首段命中注册仓 ${x.key}` : ''})`).join('、')}${_suspects.length > 5 ? ' 等' : ''}`)
1631
+ console.warn(' 主仓无这些路径 → 冻结面将标「计划未动」、跨仓对账与 patch 冻结不触发。正确写法三选一:① 段头「## <注册key> 仓变更」段内写仓根相对路径;② 子段「### <注册key>」;③ 条目前缀 cross-repo:<注册key>:<仓根相对路径>——key 以 local.yaml repos 注册为准')
1632
+ }
1633
+ // ── 空面门:空冻结面 + 声明清单有未标注交付条目 → 阻断(三处置一次给全;fire-ledger-form-strict 形态补位)──
1634
+ if (ownFiles.length === 0 && _declared.length > 0 && !acceptEmptyFace && !st.accept_empty_face) {
1635
+ console.error(`❌ 冻结面为空但 design 声明清单有 ${_declared.length} 条未标注交付条目(实现不在冻结窗口——跨仓交付撞空面的实证形态)——收口阻断(不归档;处置后重跑同一条命令从断点续):`)
1636
+ for (const x of _suspects.slice(0, 5)) console.error(` - ${x.path}${x.key ? `(首段命中注册仓 ${x.key})` : '(主仓不存在)'}`)
1637
+ console.error(' ① 实现在主仓:先提交后重跑')
1638
+ console.error(' ② 跨仓交付:清单按规范标注后重跑——正确写法三选一:段头「## <注册key> 仓变更」段内写仓根相对路径;子段「### <注册key>」;条目前缀 cross-repo:<注册key>:<仓根相对路径>(key 以 local.yaml repos 注册为准)')
1639
+ console.error(` ③ 纯文档变更/清单误列:同步裁剪 design 清单后重跑;或显式签收:flow done --change ${change} --accept-empty-face(留痕 flow-state,重跑免重拦)`)
1640
+ reportMidFail('patch')
1641
+ process.exit(1)
1642
+ }
1643
+ if (ownFiles.length === 0 && _declared.length > 0 && (acceptEmptyFace || st.accept_empty_face)) {
1644
+ if (acceptEmptyFace && !st.accept_empty_face) {
1645
+ writeFlowState(changeDir, { accept_empty_face: { at: new Date().toISOString(), declaredCount: _declared.length, by: process.env.SILLYSPEC_SESSION_ID || null } })
1646
+ st.accept_empty_face = true
1647
+ }
1648
+ console.warn(`⚠️ --accept-empty-face:显式签收空冻结面与声明面矛盾(${_declared.length} 条声明交付不进冻结面,留痕 flow-state accept_empty_face)`)
1649
+ }
1650
+ // 纯跨仓规范标注空面:合规书写不拦,advisory 点名 thin 空面分支不做跨仓冻结的已知缺口
1651
+ if (ownFiles.length === 0 && _declared.length === 0 && planEntries.some((e) => e && e.repo)) {
1652
+ console.warn('⚠️ 声明清单全部为跨仓规范标注且主仓冻结面为空——thin 空面分支不做跨仓冻结与 patch 留档(缺口已知:归档件将无 patch 两件;如需冻结请主仓侧交付或待后续版本支持空面跨仓冻结)')
1653
+ }
1449
1654
  if (ownFiles.length === 0) {
1450
- console.log('📦 patch 留档跳过:本变更可归属文件面为空')
1655
+ console.log('📦 patch 留档跳过:本变更可归属文件面为空(无交付面改动——治理工件不入冻结面;若实现代码应在此,先提交后重跑)')
1451
1656
  patchOk = true
1452
1657
  } else {
1453
1658
  const { buildFrozenPatch, collectNumstatByPath } = await import('./scope-audit.js')
1454
1659
  // 双口径冻结(2026-09-28-split-guard-and-gate-report 评审 P2 清偿):交付面(committed)
1455
1660
  // 用 baseRef..HEAD 提交区间 diff——共享主仓里并行会话对同文件的未提交 hunk 不再漏进
1456
- // 冻结件;治理工件目录(changeDirFiles,本变更独占无他会话面)保留工作树口径(未提交
1457
- // 的槽位作答/勾选要进审计件)。重叠文件归提交区间,避免双 hunk。
1661
+ // 冻结件;独占树未提交交付(dirtyAdded)保留工作树口径(FR-01 后治理工件目录不再入冻,
1662
+ // 工作树口径集只剩 dirtyAdded)。重叠文件归提交区间,避免双 hunk。
1458
1663
  const headCommit = String(gitQuiet(cwd, ['rev-parse', 'HEAD']) || '').trim() || null
1459
1664
  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))
1665
+ const worktreeSet = new Set([...((freeze.dirtyAdded) || [])].map(toPosix))
1466
1666
  const committedFace = freeze.files.map(toPosix).filter((f) => !worktreeSet.has(f))
1467
1667
  const frozenCommitted = buildFrozenPatch(cwd, committedFace, { baseRef: st.baseline_commit, headRef: headCommit })
1468
1668
  const frozenDir = buildFrozenPatch(cwd, [...worktreeSet], { baseRef: st.baseline_commit })
@@ -1483,25 +1683,40 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1483
1683
  // 范围,单一计算喂 console 与落盘两处。ownFiles 传冻结面(与 files[] 同口径——
1484
1684
  // worktree/--freeze-dirty 场景 dirty 交付也计入模块命中;.sillyspec 治理面对账内部
1485
1685
  // 滤除);三键恒在场(对账异常兜底空数组,评审 P3 清偿);best-effort 不阻断。
1486
- let moduleScope = { modules: [], uncoveredDirs: [], moduleMaps: [] }
1686
+ moduleScope = { modules: [], uncoveredDirs: [], moduleMaps: [] }
1487
1687
  try {
1488
1688
  const { reconcileModuleDocs } = await import('./flow-parity.js')
1489
1689
  const rec = reconcileModuleDocs({ cwd, specBase, ownFiles, committedRaw })
1490
1690
  if (rec.lines.length > 0) for (const l of rec.lines) console.log(l)
1491
1691
  moduleScope = { modules: rec.modules, uncoveredDirs: rec.uncoveredDirs, moduleMaps: rec.moduleMaps }
1492
1692
  } catch { /* 对账 best-effort(三键空数组兜底) */ }
1693
+ // 未登记目录软门(2026-10-12-thin-module-gate-and-feed 一档):拦在 distill 之前——
1694
+ // indexRequirements 的域路由发生在 distill,拦在前面 FR 才不落伪域。拦但可签收:登记/
1695
+ // --accept-unmapped(缺口数留痕)/ module_gate_ignore 固定忽略三条出路;绿地无图不拦。
1696
+ // 门消费的是本子步刚算的对账面(与收口留痕同时点口径),失败在 mark('patch') 前——
1697
+ // 重入 done 从 patch 断点续,登记后重算自然通过。
1698
+ let moduleGate = { block: false, dirs: [], maps: [] }
1699
+ try {
1700
+ const { evaluateModuleGate } = await import('./flow-parity.js')
1701
+ moduleGate = evaluateModuleGate({ moduleScope, ignore: readFlowConfig(specBase).moduleGateIgnore })
1702
+ } catch { /* 门判定异常不拦(对账面空 → 无可拦事实,fail-open 同对账口径) */ }
1703
+ if (moduleGate.block && !acceptUnmapped) {
1704
+ const dirList = moduleGate.dirs.slice(0, 5).map((d) => `${d.dir}(${d.files} 文件)`).join('、') + (moduleGate.dirs.length > 5 ? ' 等' : '')
1705
+ console.error(`❌ 模块图未登记交付目录(门):${dirList}——共 ${moduleGate.dirs.length} 个目录不在任何 _module-map.yaml 的 paths 下`)
1706
+ console.error(' 未登记后果是静默的:FR 蒸馏将落伪域 auto-*、知识图谱模块侧边(module-files/describes/changelog-of)断供、模块上下文注入绕开这些目录。')
1707
+ console.error(` ① 登记后重跑:在 docs/<project>/modules/_module-map.yaml 增补 modules.<id>(paths 指向该目录,可带 doc: modules/<id>.md)后重跑 flow done`)
1708
+ console.error(` ② 确认无需登记:flow done --change ${change} --accept-unmapped(缺口数随 change-patch.json acceptedUnmappedDirs 留痕)`)
1709
+ console.error(' ③ 固定忽略(合法未登记面:根级脚本/配置等):.sillyspec/local.yaml 加 module_gate_ignore: <目录>(单行逗号分隔,前缀匹配)')
1710
+ reportMidFail('patch')
1711
+ process.exit(1)
1712
+ }
1713
+ if (moduleGate.block && acceptUnmapped) {
1714
+ console.warn(`⚠️ --accept-unmapped:显式接受 ${moduleGate.dirs.length} 个未登记目录不进模块图(缺口数随 change-patch.json acceptedUnmappedDirs 留痕)`)
1715
+ }
1493
1716
  // 收口留痕单套写(2026-10-09-close-trace-single-set):共用 writeCloseTraceArtifacts
1494
1717
  // 只写 change.patch + change-patch.json 两件——对账面数据并入 JSON 的 scopeAudit
1495
1718
  // 子对象(thin 通道同样有对账面),旧名 scope-audit.* 停写、读侧兼容兜存量归档。
1496
- let planEntries = []
1497
- try {
1498
- const { parseFileChangeListDetailed } = await import('./change-list.js')
1499
- // repoKeys(2026-10-10-cross-repo-patch-freeze D-002@v1):注册仓清单传入——跨仓
1500
- // 子段/cross-repo: 前缀条目才带 repo 标注(不传则跨仓声明全落主仓口径, thin 跨仓
1501
- // 对账无从谈起)
1502
- const { loadRegisteredRepoKeys } = await import('./scope-audit.js')
1503
- planEntries = parseFileChangeListDetailed(join(changeDir, 'design.md'), { keepSillyspecDocs: true, repoKeys: loadRegisteredRepoKeys(specBase) })
1504
- } catch { /* 清单缺失/不可解析 → 实际侧 only 快照行(无 verdict,与主链路降级同语义) */ }
1719
+ // planEntries 解析与跨仓防呆告警已前移至 ownFiles 判空之前(2026-10-13-thin-empty-face-crosscheck)。
1505
1720
  // 跨仓声明在场 → 共享集成层分仓对账 + patch 冻结(D-001/D-002@v1):与 heavy 通道
1506
1721
  // (execute --done)同源 reconcileCrossRepoPlan,repos[](锚点/计数/patch 正文)
1507
1722
  // 随 snapObj 进 change-patch.json;任何异常 fail-soft 退 ⊘ 补行现行为(不阻断收口)
@@ -1544,13 +1759,18 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1544
1759
  ...(crossRepos.length > 0 ? { repos: crossRepos } : {}),
1545
1760
  },
1546
1761
  meta: {
1547
- note: 'flow done 时点冻结(本变更可归属面:baseline..HEAD 提交面过滤 .sillyspec/ 非本变更目录但保留 .sillyspec/docs/ 交付文档 + 本变更目录工作树件;含未提交与 untracked,排除 flow-state.yaml 运行态)',
1762
+ note: 'flow done 时点冻结(本变更可归属交付面:baseline..HEAD 提交面过滤 .sillyspec/ 只留 .sillyspec/docs/ 交付文档 + 独占树未提交交付;治理工件目录不入冻结——2026-10-12-close-trace-freeze-integrity FR-01)',
1548
1763
  // 模块对账结构化面(2026-09-27-thin-module-scope-persist):done 时点口径
1549
1764
  // (与 files[] 同时点语义,不随模块图后续变更回写);modules[] 空≠无影响
1550
1765
  ...moduleScope,
1551
1766
  // dirty 缺口显式接受留痕(2026-10-08-thin-done-dirty-gate-and-paren-attribution):
1552
1767
  // --accept-dirty-gap 放行时缺口数入档(审计面可见,非只 console)
1553
1768
  ...(acceptDirtyGap && freeze.dirtyWarned.length > 0 ? { acceptedDirtyGap: freeze.dirtyWarned.length } : {}),
1769
+ // 未登记目录签收留痕(2026-10-12-thin-module-gate-and-feed 一档):同款显式接受语义
1770
+ ...(acceptUnmapped && moduleGate.dirs.length > 0 ? { acceptedUnmappedDirs: moduleGate.dirs.length } : {}),
1771
+ // 孤儿面缺口显式接受留痕(2026-10-12-close-trace-freeze-integrity FR-02):
1772
+ // --accept-orphan-face 放行时缺口数入档
1773
+ ...(acceptOrphanFace && orphanGapCount > 0 ? { acceptedOrphanFace: orphanGapCount } : {}),
1554
1774
  },
1555
1775
  })
1556
1776
  console.log(`📦 变更 patch 留档(单套):change.patch + change-patch.json(${ownFiles.length} 文件,+${additions}/-${deletions},对账面并入 scopeAudit 子对象)${closeTrace.patchStatus === 'ok' ? ',sha256 已锚' : '——patch 采集失败已留痕'}`)
@@ -1618,7 +1838,16 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1618
1838
  let taskbookHead = null
1619
1839
  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
1840
  console.log(`⚖️ 本变更需要独立评审(${tier.reasons.join(';')})——评审任务书如下,起一个干净上下文的子代理执行后重跑本命令:\n`)
1621
- console.log(renderReviewerTaskbook({ change, changeDir, head: taskbookHead }))
1841
+ // 项目评审探针装配(2026-10-12-review-probe-injection):local.yaml 数据驱动的项目特有
1842
+ // 必查面注入任务书(未配置零注入;装载告警/帽留痕 best-effort 提示,不阻断渲染)。
1843
+ let _probeLines = null
1844
+ try {
1845
+ const { assembleProbeLinesForScope } = await import('./review-probes.js')
1846
+ const _assembled = assembleProbeLinesForScope(specBase, 'thin')
1847
+ for (const _w of _assembled.warnings) console.warn(`⚠️ review_probes:${_w}`)
1848
+ if (_assembled.lines.length > 0) _probeLines = _assembled.lines
1849
+ } catch { /* 探针面 fail-soft:装配失败零注入 */ }
1850
+ console.log(renderReviewerTaskbook({ change, changeDir, head: taskbookHead, probeLines: _probeLines }))
1622
1851
  reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'missing' }
1623
1852
  appendTelemetry(reviewOutcome) // 修评审 P2①:失败面先落账再 exit(校准信号不丢)
1624
1853
  reportMidFail('review')
@@ -1729,6 +1958,20 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1729
1958
  mark('probes')
1730
1959
  }
1731
1960
 
1961
+ // 模块对账面断点续跑回读(2026-10-12-thin-module-gate-and-feed 二档):patch 子步已 done 的
1962
+ // 重入轮次上 moduleScope 为空(进程内变量不跨轮)——从 change-patch.json files[] 回读重算
1963
+ // (distill 回写与 archive 前的 extraDocs 推导共用;files[] 与冻结面同时点口径)。
1964
+ if (!moduleScope) {
1965
+ try {
1966
+ const pm = JSON.parse(readFileSync(join(changeDir, 'change-patch.json'), 'utf8'))
1967
+ const filesArr = Array.isArray(pm && pm.files) ? pm.files : []
1968
+ if (filesArr.length > 0) {
1969
+ const { reconcileModuleDocs } = await import('./flow-parity.js')
1970
+ const rec = reconcileModuleDocs({ cwd, specBase, ownFiles: filesArr, committedRaw: [] })
1971
+ moduleScope = { modules: rec.modules, uncoveredDirs: rec.uncoveredDirs, moduleMaps: rec.moduleMaps }
1972
+ }
1973
+ } catch { /* 回读失败 → 空面(回写零命中静默) */ }
1974
+ }
1732
1975
  // ④ distill:决策提炼(rejected/needsWait 异态 → 升厚留人工裁决,不静默吞)+ FR 索引提炼
1733
1976
  // (2026-09-22-thin-fr-distill-sync:轻量变更此前只蒸馏 decisions 不调 indexRequirements——
1734
1977
  // requirements 永不进 knowledge/fr,知识复利在新默认道断流;轻量变更无 design.md,域路由
@@ -1741,6 +1984,21 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1741
1984
  const h = harvestSlot4Decision({ changeDir, change })
1742
1985
  if (h.harvested) console.log(`🌱 槽4(风险与死路)收割 → decisions.md(底稿——作答里的放弃方案请拆成 rejected 条目再收口;随蒸馏链进 knowledge)`)
1743
1986
  } catch { /* 收割 best-effort */ }
1987
+ // 模块变更索引机械回写(2026-10-12-thin-module-gate-and-feed 二档):命中模块的卡/sidecar
1988
+ // 追加一行本变更条目——零语义判断(日期/变更名/动机首行全已知),split-changelog 契约 sidecar
1989
+ // 优先、卡内「## 变更索引」节次之、变更名在场即跳过(断点续跑幂等)。moduleScope 由上方
1990
+ // 回读块保证在场(patch 本轮算出或 change-patch.json 回读)。
1991
+ try {
1992
+ if (moduleScope && Array.isArray(moduleScope.modules) && moduleScope.modules.length > 0) {
1993
+ const { appendChangeIndexEntries } = await import('./flow-parity.js')
1994
+ const motive = String(st.input || '').split('\n').map((s) => s.trim()).find(Boolean) || ''
1995
+ const wr = appendChangeIndexEntries({ specBase, modules: moduleScope.modules, change, summary: motive, date: new Date().toISOString().slice(0, 10) })
1996
+ changelogAppended = wr.appended
1997
+ if (wr.appended.length > 0) console.log(`📖 模块变更索引回写:${wr.appended.length} 个目标(${wr.appended.join('、')})——sidecar 优先/卡内节次之,随归档窄化入暂存`)
1998
+ const skippedReal = wr.skipped.filter((s) => !/已记录/.test(s.reason))
1999
+ if (skippedReal.length > 0) console.log(` ↩️ 跳过 ${skippedReal.length} 个(${skippedReal.slice(0, 3).map((s) => `${s.id}:${s.reason}`).join(';')}${skippedReal.length > 3 ? ' 等' : ''})`)
2000
+ }
2001
+ } catch (e) { console.warn(`⚠️ 变更索引回写失败(fail-soft 不阻断归档):${(e && e.message) || e}`) }
1744
2002
  try {
1745
2003
  const { distillIntoKnowledge } = await import('./decision-distill.js')
1746
2004
  const knowledgeRoot = join(specBase, 'knowledge')
@@ -1813,6 +2071,16 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1813
2071
  } catch (e) { console.warn(`⚠️ 回执合成失败(不阻断归档):${(e && e.message) || e}`) }
1814
2072
  const { ProgressManager } = await import('./progress.js')
1815
2073
  const { runArchiveChain } = await import('./run/complete-handlers.js')
2074
+ // extraDocs 断点续跑推导(评审 P3 清偿 2026-10-12-thin-module-gate-and-feed):distill 已
2075
+ // done 的重入轮次上 changelogAppended 为空(进程内变量不跨轮)——从命中模块的卡/sidecar
2076
+ // 反查含本变更名的目标文件(幂等事实,判据与回写去重同款 text.includes(change)),
2077
+ // extraDocs 精确入暂存不靠目录级回退兜底。
2078
+ if (changelogAppended.length === 0 && moduleScope && Array.isArray(moduleScope.modules) && moduleScope.modules.length > 0) {
2079
+ try {
2080
+ const { deriveRecordedChangeIndexTargets } = await import('./flow-parity.js')
2081
+ changelogAppended = deriveRecordedChangeIndexTargets({ specBase, modules: moduleScope.modules, change })
2082
+ } catch { /* 推导失败 → extraDocs 空(目录级回退兜底) */ }
2083
+ }
1816
2084
  // 锚定同 start(平台参数面)
1817
2085
  const { archiveDestDirName } = await import('./stage-contract.js')
1818
2086
  const pm = new ProgressManager()
@@ -1824,6 +2092,8 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1824
2092
  srcDir: changeDir,
1825
2093
  destDir,
1826
2094
  skipPlanCheck: (st.born_face ?? st.tier) === 'thin',
2095
+ // 二档回写文件随归档窄化精确入暂存(文件级 pathspec;空数组传 null 走既有零行为)
2096
+ extraDocs: changelogAppended.length > 0 ? changelogAppended.map((p) => join(specBase, p)) : null,
1827
2097
  })
1828
2098
  // 归档后 changeDir 已搬走——后续子步标记随归档目录走(flow-state 随变更包留存审计)
1829
2099
  markDir = destDir
@@ -1875,17 +2145,34 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1875
2145
  /** flow 命令族入口(index.js case 'flow' 接线):flow start|done|amend-draft。 */
1876
2146
  /**
1877
2147
  * 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 显式署名供平台/脚本侧调用。
2148
+ * 方案后运行;写 flow-state {spec_approved,spec_approved_at,spec_approved_by}。flow done 的
2149
+ * v2 机器门消费该证据(未批准且未声明 autopilot → 拒收)。
2150
+ *
2151
+ * 通道 attest(2026-10-12-approve-channel-attest,dogfood 实证驱动:8 次 approve 中 2 次为
2152
+ * agent 自批、其中一次在用户明确拒绝方案后把重复催促反读为授权,且留痕层面用户真批转述与
2153
+ * agent 自批不可区分——spec_approved_by 恒为 agent session id):
2154
+ * --user-confirmed agent 转述「用户已确认方案」(channel=user-confirmed)
2155
+ * --self-approved agent 自批如实声明(channel=agent-self)
2156
+ * 无 flag 默认路径行为不变(零破坏),channel=unattested 留痕+警示
2157
+ * 拒后派生授权硬拦:spec_rejected 在场且未被显式翻越(无 spec_rejected_superseded_at)时,
2158
+ * approve 必须带显式通道——「用户拒绝过方案」之后的无通道批准正是实证事故形态。
2159
+ * 诚实边界:同机 agent 仍可谎报 --user-confirmed(与 --by 同界)——通道的价值是必留痕+
2160
+ * 可区分+评审抽查兜底,非防伪;强凭据属平台层课题(CLI 直连用户问询),不在此层伪造。
1882
2161
  */
1883
- async function cmdFlowApprove({ change, cwd, specBase, by = null }) {
2162
+ async function cmdFlowApprove({ change, cwd, specBase, by = null, channelFlag = null }) {
1884
2163
  const changeDir = join(specBase, 'changes', change)
1885
2164
  if (!existsSync(changeDir)) {
1886
2165
  console.error(`❌ 变更不存在:${change}(flow approve 在 flow start 之后运行)`)
1887
2166
  process.exit(2)
1888
2167
  }
2168
+ const st = readFlowState(changeDir) || {}
2169
+ if (st.spec_rejected && !st.spec_rejected_superseded_at && !channelFlag) {
2170
+ console.error(`❌ 本变更存在未翻越的拒绝记录(spec_rejected,reason=${String(st.spec_rejected_reason || '?').slice(0, 80)})——拒绝后的批准必须显式声明通道:`)
2171
+ console.error(` 用户重新确认后:sillyspec flow approve --change ${change} --user-confirmed(agent 转述用户确认)`)
2172
+ console.error(` agent 自批如实声明:sillyspec flow approve --change ${change} --self-approved`)
2173
+ console.error(` (拒绝后派生授权须显式再确认——2026-10-12-approve-channel-attest)`)
2174
+ process.exit(2)
2175
+ }
1889
2176
  let approvedBy = by
1890
2177
  if (!approvedBy) {
1891
2178
  try {
@@ -1893,11 +2180,64 @@ async function cmdFlowApprove({ change, cwd, specBase, by = null }) {
1893
2180
  approvedBy = (resolveSessionIdentity({ cwd }) || {}).session || 'user'
1894
2181
  } catch { approvedBy = 'user' }
1895
2182
  }
2183
+ const channel = channelFlag === 'user-confirmed' ? 'user-confirmed'
2184
+ : channelFlag === 'self-approved' ? 'agent-self' : 'unattested'
1896
2185
  const at = new Date().toISOString()
1897
- writeFlowState(changeDir, { spec_approved: true, spec_approved_at: at, spec_approved_by: approvedBy })
2186
+ const patch = {
2187
+ spec_approved: true, spec_approved_at: at, spec_approved_by: approvedBy,
2188
+ spec_approved_channel: channel,
2189
+ }
2190
+ if (channelFlag) patch.spec_attested_by = approvedBy
2191
+ else patch.spec_attested_by = null // 评审 P3① 清偿:无通道再批准清除旧声明者,防 unattested 态残留陈旧 attest
2192
+ if (st.spec_rejected && !st.spec_rejected_superseded_at) patch.spec_rejected_superseded_at = at
2193
+ writeFlowState(changeDir, patch)
1898
2194
  console.log(`✅ spec 断点已批准(change=${change})`)
1899
- console.log(` 批准人:${approvedBy}|时间:${at}`)
1900
- console.log(` flow done 的断点机器门已满足;agent 可继续执行/收口。撤销:编辑 flow-state.yaml 删除 spec_approved 三字段。`)
2195
+ const chLabel = channelFlag === 'user-confirmed' ? 'user-confirmed(agent 转述用户确认)'
2196
+ : channelFlag === 'self-approved' ? 'agent-self(agent 自批如实声明)' : 'unattested(未声明通道)'
2197
+ console.log(` 批准人:${approvedBy}|时间:${at}|通道:${chLabel}`)
2198
+ if (!channelFlag) {
2199
+ console.warn(`⚠️ 本次批准未带 attest 通道(--user-confirmed / --self-approved)——按 unattested 留痕,评审/审计无法区分用户确认与 agent 自批。`)
2200
+ }
2201
+ if (channelFlag === 'self-approved') {
2202
+ console.warn(`⚠️ agent 自批留痕:如用户实际未确认,请撤销——sillyspec flow reject --change ${change} --reason <理由>`)
2203
+ }
2204
+ if (patch.spec_rejected_superseded_at) console.log(` 已翻越既有拒绝记录(superseded_at=${patch.spec_rejected_superseded_at},拒绝理由保留于 spec_rejected_reason)`)
2205
+ console.log(` flow done 的断点机器门已满足;agent 可继续执行/收口。撤销:sillyspec flow reject --change ${change} --reason <理由>`)
2206
+ }
2207
+
2208
+ /**
2209
+ * flow reject(2026-10-12-approve-channel-attest):spec 断点拒绝留痕——撤销既有批准并记录
2210
+ * 拒绝理由(审计可复原「批准→用户否掉→显式再确认」序);此后 approve 必须带显式 attest 通道
2211
+ * (防「拒绝后派生授权」——dogfood 实证:用户拒绝方案后 agent 把重复催促反读为授权自批)。
2212
+ * spec_approved 撤销为显式 false(非删键)——重入读侧恒见;spec_rejected* 历史字段保留不删。
2213
+ */
2214
+ async function cmdFlowReject({ change, cwd, specBase, reason }) {
2215
+ const changeDir = join(specBase, 'changes', change)
2216
+ if (!existsSync(changeDir)) {
2217
+ console.error(`❌ 变更不存在:${change}(flow reject 在 flow start 之后运行)`)
2218
+ process.exit(2)
2219
+ }
2220
+ const reasonText = String(reason || '').trim()
2221
+ if (!reasonText) {
2222
+ console.error('❌ flow reject 需要 --reason <文本>(拒绝理由,留痕供审计)')
2223
+ process.exit(2)
2224
+ }
2225
+ let rejectedBy = 'user'
2226
+ try {
2227
+ const { resolveSessionIdentity } = await import('./progress.js')
2228
+ rejectedBy = (resolveSessionIdentity({ cwd }) || {}).session || 'user'
2229
+ } catch { /* 缺省 user */ }
2230
+ const at = new Date().toISOString()
2231
+ writeFlowState(changeDir, {
2232
+ spec_approved: false,
2233
+ spec_rejected: true, spec_rejected_at: at,
2234
+ spec_rejected_reason: reasonText, spec_rejected_by: rejectedBy,
2235
+ spec_rejected_superseded_at: null, // 再拒绝重置翻越戳(上一轮「拒→显式确认」序已被新拒绝终结)
2236
+ })
2237
+ console.log(`🚫 spec 断点已拒绝留痕(change=${change})`)
2238
+ console.log(` 理由:${reasonText}|操作者:${rejectedBy}|时间:${at}`)
2239
+ console.log(` 既有批准已撤销(spec_approved=false)——flow done 断点机器门将重新拦收。`)
2240
+ console.log(` 用户重新确认后批准须显式通道:flow approve --change ${change} --user-confirmed(agent 自批则 --self-approved 如实声明)`)
1901
2241
  }
1902
2242
 
1903
2243
  export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
@@ -2001,8 +2341,22 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2001
2341
  const change = getFlag('--change')
2002
2342
  if (!change) { console.error('❌ flow approve 需 --change <名>'); process.exit(2) }
2003
2343
  validateChangeName(change)
2344
+ // attest 通道(2026-10-12-approve-channel-attest):互斥二选一,无 flag=unattested(留痕+警示)
2345
+ const userConfirmed = hasFlag('--user-confirmed')
2346
+ const selfApproved = hasFlag('--self-approved')
2347
+ if (userConfirmed && selfApproved) {
2348
+ console.error('❌ --user-confirmed 与 --self-approved 互斥(一次只能声明一个 attest 通道)')
2349
+ process.exit(2)
2350
+ }
2351
+ await reportAgentLog(change, sub)
2352
+ return cmdFlowApprove({ change, cwd, specBase, by: getFlag('--by') || null, channelFlag: userConfirmed ? 'user-confirmed' : (selfApproved ? 'self-approved' : null) })
2353
+ }
2354
+ if (sub === 'reject') {
2355
+ const change = getFlag('--change')
2356
+ if (!change) { console.error('❌ flow reject 需 --change <名>'); process.exit(2) }
2357
+ validateChangeName(change)
2004
2358
  await reportAgentLog(change, sub)
2005
- return cmdFlowApprove({ change, cwd, specBase, by: getFlag('--by') || null })
2359
+ return cmdFlowReject({ change, cwd, specBase, reason: getFlag('--reason') })
2006
2360
  }
2007
2361
  if (sub === 'status') {
2008
2362
  // 三断点配套(2026-09-25-flow-checkpoints):随时可查当前变更阶段/槽位/子步进度
@@ -2148,7 +2502,7 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2148
2502
  console.log('🔄 --refreeze:patch 子步标记已重置,本次 done 将重新冻结(change.patch 按 baseline..HEAD 最新面重建)')
2149
2503
  }
2150
2504
  }
2151
- return cmdFlowDone({ change, cwd, specBase, runtimeRootOpt, freezeDirty: hasFlag('--freeze-dirty'), acceptDirtyGap: hasFlag('--accept-dirty-gap'), allowBatchTick: hasFlag('--allow-batch-tick') })
2505
+ 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'), acceptEmptyFace: hasFlag('--accept-empty-face') })
2152
2506
  }
2153
2507
  if (sub === 'amend-draft') {
2154
2508
  // 机器稿唯一留痕修改通道(R7 切片三 / FR-08):重锚哈希 + ledger amendment 审计;