sillyspec 3.32.7 → 3.32.9

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
@@ -27,27 +27,22 @@
27
27
  */
28
28
  import { existsSync, readFileSync, writeFileSync, readdirSync, appendFileSync, renameSync, statSync } from 'node:fs'
29
29
  import { join, relative, dirname } from 'node:path'
30
+ import { DatabaseSync } from 'node:sqlite'
30
31
  import yaml from 'js-yaml'
31
32
  import { git, gitQuiet } from './git-helper.js'
32
33
  import { writeAtomicSync } from './fs-atomic.js'
33
34
  import { resolveRuntimeRoot, triggerSync, assertDatedChangeName } from './run/shared.js'
34
35
  import { detectUiTouch, buildUiGuidanceLines, runUiVisualProbe, readUiVisualGate, UI_EVIDENCE_FILENAME } from './ui-visual.js'
35
36
  import { runHunkAttributionGate, readHunkGate, renderHunkAttributionLines } from './hunk-attribution.js'
37
+ // 2026-10-13-thin-sync-stage:FLOW_STATE_FILE/readFlowState 单一源上移叶子模块 flow-state.js
38
+ // (progress.js 同步投影也要读;叶子化避免 progress.js 为读一个状态文件拉入本协议整条
39
+ // 依赖链——评审 P3 勘误:非静态循环依赖,本文件 import 闭包实测不达 progress.js);
40
+ // re-export 保持 `import('../flow.js')`(run/command.js 既有动态引用)导出面不变。
41
+ import { FLOW_STATE_FILE, readFlowState } from './flow-state.js'
42
+ export { FLOW_STATE_FILE, readFlowState }
36
43
 
37
- const FLOW_STATE_FILE = 'flow-state.yaml'
38
44
  const SUBSTEPS = ['artifacts', 'ledger', 'patch', 'review', 'probes', 'distill', 'archive', 'events']
39
45
 
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
46
  /** 原子写 flow-state(patch 合并,幂等)。 */
52
47
  export function writeFlowState(changeDir, patch) {
53
48
  const cur = readFlowState(changeDir) || {}
@@ -448,7 +443,136 @@ function changeArtifactPaths(changeDir) {
448
443
  * flow start —— 第 1 次协议调用(建卡+下发)。已存在 change → 恢复简报(不新建不重置)。
449
444
  * @param {{change:string, input?:string, thick?:boolean, withTasks?:boolean, cwd:string, specBase:string, json?:boolean}} p
450
445
  */
451
- export async function cmdFlowStart({ change, input, title: titleFlag = null, thick = false, withTasks = false, reviewForce = null, autopilot = false, cwd, specBase, runtimeRootOpt = null, json = false }) {
446
+ /**
447
+ * flow diff —— change 粒度文件统计只读查询(2026-10-13-flow-diff-cmd)。
448
+ * 双态同构:归档态读 change-patch.json scopeAudit(冻结时点真相,含 verdict);活跃态实时算
449
+ * baseline→工作树 累计口径(与 patch 子步冻结面同规则:filterCommittedFace 剔 ownPrefix 治理件
450
+ * 保 .sillyspec/docs/;行数 git numstat,untracked 按文件行数、二进制 ∅)。只读零副作用:
451
+ * 不 pull(无 triggerPullActiveChange)、不写盘、不动状态机——scan diff 同纪律。
452
+ * rows[].state:committed(工作树==HEAD)/ dirty(有未提交改动)/ untracked / archived。
453
+ */
454
+ async function cmdFlowDiff({ change, cwd, specBase, json = false }) {
455
+ const toPosix = (p) => String(p).replace(/\\/g, '/')
456
+ const activeDir = join(specBase, 'changes', change)
457
+ const archiveJson = join(specBase, 'changes', 'archive', change, 'change-patch.json')
458
+ const rows = []
459
+ let mode = null
460
+ let baseline = null
461
+ if (existsSync(archiveJson)) {
462
+ mode = 'archived'
463
+ try {
464
+ const meta = JSON.parse(readFileSync(archiveJson, 'utf8'))
465
+ const sa = meta.scopeAudit || {}
466
+ for (const r of (sa.rows || [])) {
467
+ if (!r || !r.path) continue
468
+ rows.push({ path: toPosix(r.path), additions: Number.isFinite(r.additions) ? r.additions : null, deletions: Number.isFinite(r.deletions) ? r.deletions : null, state: 'archived', verdict: r.verdict || null })
469
+ }
470
+ const t = (sa.totals && sa.totals.files != null) ? sa.totals : meta.totals
471
+ var archivedTotals = (t && typeof t === 'object') ? { files: t.files ?? rows.length, additions: t.additions ?? null, deletions: t.deletions ?? null } : null
472
+ } catch { /* 损坏件 → 按活跃态兜底(下方 activeDir 分支不命中则报不存在) */
473
+ mode = null
474
+ }
475
+ }
476
+ if (mode === null && existsSync(join(activeDir, 'flow-state.yaml'))) {
477
+ mode = 'active'
478
+ const st = readFlowState(activeDir)
479
+ baseline = st?.baseline_commit || null
480
+ if (baseline) {
481
+ const ownPrefix = `.sillyspec/changes/${change}/`
482
+ const committedRaw = String(gitQuiet(cwd, ['diff', '--name-only', `${baseline}..HEAD`]) || '').split('\n').map((s) => s.trim().replace(/\\/g, '/')).filter(Boolean)
483
+ const { filterCommittedFace } = await import('./flow-parity.js')
484
+ const committed = filterCommittedFace(committedRaw, ownPrefix)
485
+ // dirty 面(含 untracked):porcelain 全列,交付规则同冻结面(.sillyspec/ 剔、.sillyspec/docs/ 保、ownPrefix 剔)
486
+ // ⚠ 行不可 trim——porcelain 前导状态码含空格(' M x');git() 默认整段 trim 会吃掉首行
487
+ // 前导空格(实测 ' M work.txt' → 'M work.txt' → slice(3) 错位吃路径首字符),须 trim:false
488
+ const por = String(gitQuiet(cwd, ['status', '--porcelain', '-uall'], { trim: false }) || '').split('\n').filter((s) => s.length >= 4)
489
+ const dirtyState = new Map() // path -> 'dirty' | 'untracked'
490
+ for (const line of por) {
491
+ const xy = line.slice(0, 2)
492
+ const raw = line.slice(3).trim()
493
+ const path = toPosix(raw.includes(' -> ') ? raw.split(' -> ').pop() : raw)
494
+ if (!path || path.startsWith(ownPrefix)) continue
495
+ if (path.startsWith('.sillyspec/') && !path.startsWith('.sillyspec/docs/')) continue
496
+ dirtyState.set(path, xy === '??' ? 'untracked' : 'dirty')
497
+ }
498
+ const face = [...new Set([...committed, ...dirtyState.keys()])]
499
+ // 累计口径 numstat:baseline → 工作树(无 HEAD 上限,含未提交;untracked 不入 git diff 单独算)
500
+ const tracked = face.filter((f) => dirtyState.get(f) !== 'untracked')
501
+ const nsOut = String(gitQuiet(cwd, ['diff', '--numstat', baseline, '--', ...tracked]) || '')
502
+ const ns = new Map()
503
+ for (const l of nsOut.split('\n').filter(Boolean)) {
504
+ const [a, d, ...rest] = l.split('\t')
505
+ if (rest.length === 0) continue
506
+ ns.set(toPosix(rest.join('\t')), { additions: /^\d+$/.test(a) ? Number(a) : null, deletions: /^\d+$/.test(d) ? Number(d) : null })
507
+ }
508
+ for (const f of face) {
509
+ const st3 = dirtyState.get(f)
510
+ if (st3 === 'untracked') {
511
+ try {
512
+ const buf = readFileSync(join(cwd, f))
513
+ const text = buf.toString('utf8')
514
+ const lines = text.split('\n')
515
+ if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop()
516
+ rows.push({ path: f, additions: buf.includes(0) ? null : lines.length, deletions: null, state: 'untracked' })
517
+ } catch { rows.push({ path: f, additions: null, deletions: null, state: 'untracked' }) }
518
+ } else {
519
+ const s = ns.get(f)
520
+ rows.push({ path: f, additions: s ? s.additions : null, deletions: s ? s.deletions : null, state: st3 === 'dirty' ? 'dirty' : 'committed' })
521
+ }
522
+ }
523
+ }
524
+ }
525
+ if (mode === null) {
526
+ console.error(`❌ 变更「${change}」不存在(活跃 flow-state 与归档 change-patch.json 均未找到)`)
527
+ console.error(' 修复:sillyspec status 查变更列表核对拼写/日期前缀;归档变更名与归档目录名逐字一致')
528
+ process.exit(1)
529
+ }
530
+ const totals = mode === 'archived' && archivedTotals
531
+ ? archivedTotals
532
+ : { files: rows.length, additions: rows.reduce((s, r) => s + (r.additions || 0), 0), deletions: rows.reduce((s, r) => s + (r.deletions || 0), 0) }
533
+ if (json) {
534
+ console.log(JSON.stringify({ change, mode, baseline, rows, totals }))
535
+ return
536
+ }
537
+ if (mode === 'active' && !baseline) {
538
+ console.log(`📦 flow diff — ${change}(active):无 git 基线(无历史仓)——无可对账面`)
539
+ return
540
+ }
541
+ console.log(`📐 flow diff — ${change}(${mode}${mode === 'active' ? `|baseline ${String(baseline).slice(0, 8)}→工作树` : '|flow done 冻结件'})`)
542
+ for (const r of rows) {
543
+ const a = r.additions == null ? ' ∅' : `+${String(r.additions).padStart(4)}`
544
+ const d = r.deletions == null ? ' ∅' : `-${String(r.deletions).padStart(4)}`
545
+ const tag = r.state === 'dirty' ? '(未提交)' : r.state === 'untracked' ? '(未跟踪·新文件)' : r.state === 'archived' && r.verdict ? `|${r.verdict}` : ''
546
+ console.log(` ${a} ${d} ${r.path}${tag}`)
547
+ }
548
+ console.log(`合计:${totals.files} 文件 +${totals.additions ?? '?'} -${totals.deletions ?? '?'}${mode === 'active' ? '(baseline→工作树累计口径,含未提交)' : ''}`)
549
+ }
550
+
551
+ /**
552
+ * 升厚行值重置(坑 thin-upgrade-stale-stage-window,2026-10-13):升厚只写 flow-state
553
+ * 不碰 DB 行——行值残留 'thin'(平台 pull 往返透传形态)时,serializeForSync 的 thin 投影
554
+ * 在首次 run <stage> 前持续误标平台「轻量」出身(短窗展示面失真)。升厚三写点统一调本
555
+ * helper:行 current_stage 'thin' → 'brainstorm'(厚档起点,与 run 阶段机首写同值),
556
+ * 仅 thin 值才动(其余行值不越权重置)。best-effort 不阻断升厚链。
557
+ */
558
+ function resetThinStageRowOnUpgrade(cwd, change, specBase = null) {
559
+ try {
560
+ // cmdFlowDone 作用域无共享 ProgressManager——helper 直走底层数据库文件(node:sqlite)
561
+ // 零依赖;specBase 优先(与 flow 侧锚定同口径,平台模式 specRoot 异位不迷路)
562
+ const db = new DatabaseSync(join(specBase || join(cwd, '.sillyspec'), '.runtime', 'sillyspec.db'))
563
+ try {
564
+ const row = db.prepare('SELECT current_stage FROM changes WHERE name = ?').get(change)
565
+ if (row && row.current_stage === 'thin') {
566
+ db.prepare('UPDATE changes SET current_stage = ? WHERE name = ?').run('brainstorm', change)
567
+ console.log(' ↺ 升厚重置 DB 行 current_stage: thin → brainstorm(防平台短窗误标轻量出身)')
568
+ }
569
+ } finally {
570
+ try { db.close() } catch { /* 关闭失败不放大 */ }
571
+ }
572
+ } catch { /* 行值重置 best-effort,不阻断升厚 */ }
573
+ }
574
+
575
+ export async function cmdFlowStart({ change, input, title: titleFlag = null, thick = false, withTasks = false, reviewForce = null, autopilot = false, prestageAck = false, turbo = false, cwd, specBase, runtimeRootOpt = null, json = false }) {
452
576
  // local.yaml 缺席 fail-fast(R22 实证:缺 local.yaml 时 flow done 测试门静默兜底裸
453
577
  // python -m pytest → aiobotocore 假红/全量 860s 撞 600s 帽 → 11 轮重试 135min)。
454
578
  // local.yaml 是 init 的职责面——flow start 在此拒绝,不让 agent 在错误的测试配置上走完全程
@@ -483,9 +607,18 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
483
607
  // 保留,机器只补缺件与绑定面。无产物 = 真 legacy 既有变更,维持原拒收。
484
608
  const hasBsArtifacts = existsSync(join(changeDir, 'proposal.md')) || existsSync(join(changeDir, 'design.md'))
485
609
  if (hasBsArtifacts) {
610
+ // 预段反馈闭环(2026-10-14-prestage-consent-gate):走了头脑风暴预段=选道正确,
611
+ // 消费 route-hindsight 标记(同意门的一次性豁免在这里用掉,无声明 flag 也放行)
612
+ try {
613
+ const { clearHindsightMark } = await import('./route-hindsight.js')
614
+ if (clearHindsightMark({ specBase })) console.log('🕰️ 预段同意门:头脑风暴收编即选道正确,route-hindsight 标记已消费')
615
+ } catch { /* 消费 best-effort */ }
486
616
  const head = gitQuiet(cwd, ['rev-parse', 'HEAD'])
487
617
  const baseline = typeof head === 'string' && head.trim() ? head.trim() : null
488
- writeFlowState(changeDir, { tier: 'thin', born_face: 'thin', adopted_from: 'brainstorm', review_force: reviewForce, baseline_commit: baseline, substeps: {} })
618
+ writeFlowState(changeDir, { tier: 'thin', born_face: 'thin', adopted_from: 'brainstorm', review_force: reviewForce, baseline_commit: baseline, substeps: {},
619
+ // 极速模式留痕(2026-10-14-thin-turbo-mode D-005):--turbo 用户显式触发声明性落盘;
620
+ // flow done 读态以 turbo && tier!=='thick' 判定(D-007 升厚即失效)
621
+ ...(turbo ? { turbo: true, turbo_since: new Date().toISOString() } : {}) })
489
622
  try {
490
623
  const { redraftMissingArtifacts, ensureBindingSlots } = await import('./flow-draft.js')
491
624
  const r = redraftMissingArtifacts({ changeDir, change, input: null, runtimeRoot })
@@ -525,6 +658,8 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
525
658
  try { digestLines = (await flowKnowledgeDigest({ specBase, change, changeDir, input: null })).lines } catch { /* 注入 best-effort */ }
526
659
  console.log([
527
660
  `🧲 头脑风暴产物已收编进轻量跑道: ${change}(adopted_from=brainstorm,baseline=${baseline ? baseline.slice(0, 10) : '(无 git 历史)'})`,
661
+ // 极速指引行(2026-10-14-thin-turbo-mode FR-01):--turbo 在场才输出,缺省路径逐字不变
662
+ ...(turbo ? [`⚡ 极速模式(用户显式触发):只产 spec+代码,收口自动跳过实测与重仪式门(必留面照常);本次收口不含测试客观核验`] : []),
528
663
  `══════════════════════════════════════`,
529
664
  `📖 必读(头脑风暴产出——方案的承诺锚,先读完再动手):`,
530
665
  ...artifacts.map((p) => ` - ${p}`),
@@ -536,6 +671,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
536
671
  `requirements 测试绑定槽(收编追加)每条 FR 至少一行作答;写码前后顺手填。`,
537
672
  `✅ 任务面归你(工作分解契约):tasks.md 是你的工作队列(机器种子可增删改,保持 \`- [ ] task-NN:\` 行形态)——CLI 以 checkbox 为进度状态机(task tick/flow status 实时读它),TodoWrite 类工具不替代;`,
538
673
  ` 勾选是收口证据面(逐格核提交 token/review.json):做一件 → 勾一格(task tick 即时回显进度与下一任务)→ 继续下一条,勿攒一把勾;`,
674
+ ` 任务拟法:收口/推送/发布类动作不单列 task(协议自身承担,成功标准照写 requirements);确需单列的,勾选前先有消息预埋 task-NN token 的交付提交(回执类证据写进提交消息)。`,
539
675
  ` flow status --change ${change} 为自愿查看/恢复面(非协议必需,D-007 中间零必需交互;恢复时给下一任务指针与进度)。`,
540
676
  `⚠️ 交付纪律:收口前交付代码显式 pathspec 提交——冻结件范围=baseline..HEAD,未提交不进审计件。`,
541
677
  ``,
@@ -545,6 +681,10 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
545
681
  ...materials.map((p) => ` - ${p}`),
546
682
  ``,
547
683
  ...digestLines,
684
+ ...(prototypePaths.length > 0 ? [
685
+ `变更原型(头脑风暴产出,UI 实现前必看——中部 🖼️ 行的绝对尾部重列,防长输出截断丢指针):`,
686
+ ...prototypePaths.map((p) => ` - ${p}`),
687
+ ] : []),
548
688
  ].filter(Boolean).join('\n'))
549
689
  try { await triggerSync(cwd, change) } catch { /* 同步绝不阻断协议面 */ }
550
690
  return { adopted: true, change, baseline }
@@ -668,6 +808,37 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
668
808
  }
669
809
  }
670
810
 
811
+ // 预段选道同意门(2026-10-14-prestage-consent-gate):route-hindsight 标记从「下次 start 点名
812
+ // 提示」升级为声明即过的同意门——上个轻量变更被标「疑似该走预段未走」时,本次全新 start 须
813
+ // 显式选道一次(--no-prestage 声明留痕 / 先走 brainstorm 预段)。边界:门不判本次需求清不清晰
814
+ // (判定归 agent/用户,D-005 弱信号),只把上次的过程形态信号摆上台面要求一次显式消费——同
815
+ // --autopilot/--upgrade-thick 的同意门模式(「可无视」升级为「可无视但需留痕声明」)。
816
+ // 豁免面:resume(上方分支早退)天然不受影响;平台 writer 预建空目录(proceedFreshEmptyDir)
817
+ // 豁免——平台自动派发无选道对话面,标记留给人工会话消化;adopt(头脑风暴收编)= 选道正确,
818
+ // 在 adopt 分支消费标记。读取 fail-open(读不到=无门),消费失败不阻断(下次再声明一次)。
819
+ if (!proceedFreshEmptyDir) {
820
+ let prestageMark = null
821
+ try {
822
+ const { readHindsightMark } = await import('./route-hindsight.js')
823
+ prestageMark = readHindsightMark({ specBase })
824
+ } catch { /* 标记读取 fail-open */ }
825
+ if (prestageMark) {
826
+ if (!prestageAck) {
827
+ const _why = Array.isArray(prestageMark.reasons) && prestageMark.reasons.length > 0
828
+ ? prestageMark.reasons.join(';') : '过程形态超阈'
829
+ console.error(`❓ 预段选道同意门:上个轻量变更「${prestageMark.change}」被 route-hindsight 标记「疑似该走头脑风暴预段未走」(${_why})。本次全新 start 须显式选道(声明即过——标记是过程形态信号非定罪):`)
830
+ console.error(` ① 确认需求已含决策(成功标准来自用户原话或既有文档转写,而非你的推断):sillyspec flow start --change ${change} --input "<原输入>" --no-prestage(声明留痕 flow-state.prestage_ack,标记消费)`)
831
+ console.error(` ② 需求不清晰或需方案探索:sillyspec run brainstorm --change ${change}(完成后回来 flow start 收编,标记自动消费)`)
832
+ process.exit(2)
833
+ }
834
+ try {
835
+ const { clearHindsightMark } = await import('./route-hindsight.js')
836
+ clearHindsightMark({ specBase })
837
+ console.log(`🕰️ 预段同意门:已按 --no-prestage 声明放行(本次需求已含决策,留痕 flow-state.prestage_ack;上个标记「${prestageMark.change}」已消费)`)
838
+ } catch { /* 标记消费 best-effort:失败不阻断(下次 start 再声明一次) */ }
839
+ }
840
+ }
841
+
671
842
  // 升厚预判已删除(2026-09-25-thin-precheck-removal):技术面关键词(数据库/迁移等)测错轴——
672
843
  // R16 实证碰迁移的变更轻量变更带评审最优收口,预判反诱发误升厚(臂 A 129M)与误报打断。选道只留
673
844
  // 形态信号:清晰度门管「需求说不清楚」(预段收编),升厚只留用户决策(--upgrade-thick 同意门)
@@ -713,10 +884,44 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
713
884
  // autopilot(2026-10-04-thin-docs-v2 FR-06):用户显式声明跳过 spec 断点人审(--autopilot);
714
885
  // 缺省 false——flow done 要求 flow approve 批准证据(spec 断点机器化,护栏#2:零 prompt 劝说)
715
886
  autopilot: Boolean(autopilot),
887
+ // 预段同意门声明留痕(2026-10-14-prestage-consent-gate):hindsight 标记在场时全新 start
888
+ // 以 --no-prestage 显式声明「本次需求已含决策」放行——审计面可区分「确认过」与「没被问过」
889
+ prestage_ack: Boolean(prestageAck),
890
+ // 极速模式留痕(2026-10-14-thin-turbo-mode D-005):--turbo 用户显式触发声明性落盘;
891
+ // flow done 读态以 turbo && tier!=='thick' 判定(D-007 升厚即失效)
892
+ ...(turbo ? { turbo: true, turbo_since: new Date().toISOString() } : {}),
716
893
  baseline_commit: baseline,
717
894
  substeps: {},
718
895
  })
719
896
 
897
+ // 跨仓基线锚(2026-10-13-cross-repo-baseline-anchor):thin 手工交付的跨仓对账此前无锚
898
+ // (B 档 HEAD~1 单笔窗口,多提交交付漏文件+交错时序抓错面——fire-ledger 两笔交付实证)。
899
+ // start 时点记各注册仓 HEAD(cross_baselines),收口窗口=基线..工作树(与主仓 baseline_commit
900
+ // 同构)。首写者胜(对齐 baseline_sha256 模式:重入不刷新,防跨仓已有并行提交后重锚漂移窗口)。
901
+ // 纯读 fail-soft:注册不可达/非 git 仓/rev-parse 失败 → 跳过该仓,绝不阻断 start。
902
+ try {
903
+ if (!(readFlowState(changeDir) || {}).cross_baselines) {
904
+ const _yamlText = existsSync(join(specBase, 'local.yaml')) ? readFileSync(join(specBase, 'local.yaml'), 'utf8') : ''
905
+ if (_yamlText) {
906
+ const { parseRepoRegistry } = await import('./stages/plan-postcheck.js')
907
+ const { isAbsolute: _isAbs, resolve: _res } = await import('node:path')
908
+ const _repos = parseRepoRegistry(_yamlText)
909
+ const _bl = {}
910
+ for (const [k, rawPath] of _repos || []) {
911
+ const rp = _isAbs(rawPath) ? rawPath : _res(cwd, rawPath)
912
+ if (!rp || !existsSync(rp)) continue
913
+ const sha = gitQuiet(rp, ['rev-parse', 'HEAD'], { timeout: 10_000 })
914
+ const s = sha === null ? '' : String(sha).trim()
915
+ if (/^[0-9a-f]{7,40}$/i.test(s)) _bl[k] = s
916
+ }
917
+ if (Object.keys(_bl).length > 0) {
918
+ writeFlowState(changeDir, { cross_baselines: _bl })
919
+ console.log(`🌐 跨仓基线锚:${Object.keys(_bl).length} 个注册仓 HEAD 已记入 flow-state(cross_baselines 首写者胜)——收口跨仓对账窗口=基线..工作树`)
920
+ }
921
+ }
922
+ }
923
+ } catch { /* 跨仓基线锚 best-effort:失败不阻断 start(收口回退既有档位) */ }
924
+
720
925
  // 全件机器起草(R7 切片三 / FR-07:工件回填轮=0——治理工件 CLI 写,agent 只裁例外)。
721
926
  // 任务卡分岔(用户裁定#3):默认 thin+直写零任务卡;--thick/--with-tasks 才生成任务卡。
722
927
  let drafted = []
@@ -773,9 +978,61 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
773
978
  try {
774
979
  digest = await flowKnowledgeDigest({ specBase, change, changeDir, input, filesOverride: await extractRoutingInputPaths(cwd, specBase, input) })
775
980
  } catch { /* 注入 best-effort 不阻断 start */ }
981
+ // 跨仓清单写法提示(2026-10-13-thin-empty-face-crosscheck):注册仓非空时提示段头契约——
982
+ // thin 通道此前零跨仓指引(fire-ledger-form-strict 实证:契约只活在厚流程模板里,裸写仓目录
983
+ // 前缀撞空面即静默丢对账)。注册为空 → 零新增输出(横幅逐字节不变)。
984
+ let crossRepoHintLines = []
985
+ try {
986
+ const { loadRegisteredRepoKeys } = await import('./scope-audit.js')
987
+ const _repoKeys = loadRegisteredRepoKeys(specBase)
988
+ if (_repoKeys && _repoKeys.length > 0) crossRepoHintLines = [
989
+ `🌐 跨仓交付:清单跨仓文件另用段头「## <注册key> 仓变更」段内写仓根相对路径(或子段「### <注册key>」/条目前缀 cross-repo:<注册key>:)——key 以 local.yaml repos 注册为准(本仓注册 ${_repoKeys.length} 个);`,
990
+ ` 裸写「仓目录名/路径」前缀不触发跨仓对账与冻结(收口空面门将点名阻断)。`,
991
+ ``,
992
+ ]
993
+ } catch { /* 注册仓读取失败 → 零提示(与无注册仓同形态) */ }
994
+ // FR 域预判(2026-10-13-batchtick-evidence-domain-hint):--input 提及路径 × 模块图(discoverModuleIndex
995
+ // 读 docs/<project>/modules/_module-map.yaml——与 distill 域路由同一匹配面)。第一次知道「路径未登记」
996
+ // 不该是收口模块门(最贵时点)。纯提示不设门:input 路径可能不全,路由真相以实际交付文件为准;
997
+ // 绿地无图/零路径 → 零新增输出(与模块门「绿地无图不拦」语义对齐)。fr-index.js 零编辑——
998
+ // resolveTouchedDomains(内部即 pseudoDomainFromPaths+词典守卫)复用 distill 同一伪域回退链。
999
+ let domainHintLines = []
1000
+ try {
1001
+ const _routingFace = await extractRoutingInputPaths(cwd, specBase, input)
1002
+ if (_routingFace.length > 0) {
1003
+ const { discoverModuleIndex } = await import('./decision-distill.js')
1004
+ const { matchedModuleIds, resolveTouchedDomains } = await import('./fr-index.js')
1005
+ const _knowledgeRoot = join(specBase, 'knowledge')
1006
+ const _moduleIndex = discoverModuleIndex(_knowledgeRoot)
1007
+ if (_moduleIndex && Object.keys(_moduleIndex).length > 0) {
1008
+ const _hitRows = []
1009
+ const _missFiles = []
1010
+ for (const f of _routingFace) {
1011
+ const ids = matchedModuleIds(_moduleIndex, f)
1012
+ if (ids.length > 0) _hitRows.push({ f, ids })
1013
+ else _missFiles.push(f)
1014
+ }
1015
+ if (_hitRows.length > 0 || _missFiles.length > 0) {
1016
+ const _lines = ['🗺️ FR 域预判(按 --input 提及路径;最终以收口实际交付文件路由为准):']
1017
+ for (const r of _hitRows) _lines.push(` ✓ ${r.f} → fr/${r.ids.join('、')}`)
1018
+ if (_missFiles.length > 0) {
1019
+ const _pseudo = (resolveTouchedDomains(changeDir, _moduleIndex, _missFiles, _knowledgeRoot)[0]) || 'unmapped'
1020
+ _lines.push(` ⚠️ ${_missFiles.slice(0, 5).join('、')}${_missFiles.length > 5 ? ' 等' : ''} → 未命中知识模块图,FR 蒸馏将落域 ${_pseudo}(按 distill 同一伪域回退链预判——未吸附为 auto-<段>,词典吸附命中既有真域)`)
1021
+ _lines.push(' 建议现在登记模块条目(docs/<project>/modules/_module-map.yaml 的 modules.<id>.paths 覆盖该目录)——登记后蒸馏自动落真域(收口模块门仍会兜底拦未登记目录)')
1022
+ }
1023
+ domainHintLines = _lines
1024
+ }
1025
+ }
1026
+ }
1027
+ } catch { /* 域预判 best-effort:图/路径读取失败零输出 */ }
776
1028
  const lines = [
777
1029
  `🏃 flow start(${thick ? 'thick 厚档(--thick 显式声明,人声明不做启发式)' : 'thin 轻量跑道'}): ${change}`,
778
1030
  `══════════════════════════════════════`,
1031
+ // 极速指引行(2026-10-14-thin-turbo-mode FR-01;2026-10-14-turbo-guide-tier-copy 评审 P3
1032
+ // 清偿):承诺文案随档位收窄——thick 起步时 done 读态 turbo 不生效(D-007),文案如实告知
1033
+ // 全量仪式,防「提示说 A 实现是 B」漂移(cli-copy-selfsufficient②)。缺省路径逐字不变。
1034
+ ...(turbo && !thick ? [`⚡ 极速模式(用户显式触发):只产 spec+代码,收口自动跳过实测与重仪式门(必留面照常);本次收口不含测试客观核验`]
1035
+ : turbo && thick ? [`⚡ 极速留痕已记(--turbo),但 --thick 厚档起步——收口走全量仪式,实测与仪式门照跑不跳过(升厚即失效)`] : []),
779
1036
  ...(hindsightHint ? [hindsightHint, ''] : []),
780
1037
  `🔎 选道自检——对本需求,你还有没有必须问用户才能动手的问题?`,
781
1038
  ` 有 → sillyspec run brainstorm --change ${change}(它就是结构化问询协议,产物随后 flow start 自动收编续跑);`,
@@ -795,6 +1052,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
795
1052
  ` 文档内零指纹标记、零槽注释、零书写指导(规则以本横幅与命令卡为唯一源);成功标准漂移只出 advisory 不拒收。`,
796
1053
  `⚠️ design.md 四节动码前后顺手作答;列改动文件用独立「## 文件变更清单」节+表格(| 操作 | 路径 | 说明 |)`,
797
1054
  ` ——勿写在「接口契约」节内(收口按节名解析声明面)。`,
1055
+ ...crossRepoHintLines,
798
1056
  `📜 requirements 每条 FR = 标题锚 + 行为句 + 按需场景块(#### 场景:名 + Given/When/Then);`,
799
1057
  ` 测试绑定节每条 FR 一行 \`FR-NN: test/路径「用例」\`(无测试面写「不适用:理由」)。`,
800
1058
  `📦 冻结面在 flow done 时点采集(baseline..HEAD 提交面):未提交交付文件——会话专属 worktree 自动并入;`,
@@ -812,6 +1070,9 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
812
1070
  ` (sillyspec task tick --change ${change} --task task-NN 即时回显进度 N/M 与下一任务指针,flow status 同源;或直接 Edit 翻格 \`- [ ]\`→\`- [x]\`),watcher 实时上平台。`,
813
1071
  ` ⚠️ harness 的 TodoWrite 类工具是会话内便利面,不替代 tasks.md——平台进度/收口证据只读它(0/12 事故实证:todo 全 completed 而 tasks.md 0 勾);`,
814
1072
  ` 勾选是收口证据面:每格勾选要有对应提交(消息带 task-NN——逐个列出或 /、,连写如 task-01/02/03,「task-01..task-05」区间写法不认)或 review.json,全勾但零证据会被拒收;勿攒一把勾(收口硬门拒单拍多格);`,
1073
+ // 任务拟法指引(2026-10-12-closeout-task-guidance,三次「哨兵抓收口型任务零证据」复盘):
1074
+ // 成功标准≠工作分解——收口/推送/发布是协议自身承担的动作;单列则证据(token 提交)必须先于勾选存在。
1075
+ ` 任务拟法:收口/推送/发布类动作不单列 task(提交、归档、推送是 flow start→done 协议自身承担的动作——成功标准照写 requirements,勿逐字搬进任务清单);确需单列的收口/操作型任务,勾选前先做消息预埋 task-NN token 的交付提交——回执类证据(push/publish 回执、构建日志路径)写进提交消息,勿依赖勾选后补;`,
815
1076
  ` flow status --change ${change} 为自愿查看/恢复面(恢复时给下一任务指针与进度)。`,
816
1077
  ``,
817
1078
  `🛑 三断点纪律(可控性要求——用户没说「全跑完」就必须在每个断点向用户汇报并等确认):`,
@@ -838,6 +1099,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
838
1099
  ...materials.map((p) => ` - ${p}`),
839
1100
  ``,
840
1101
  ...digest.lines,
1102
+ ...(domainHintLines.length > 0 ? ['', ...domainHintLines] : []),
841
1103
  ]
842
1104
  if (json) {
843
1105
  console.log(JSON.stringify({ change, tier: thick ? 'thick' : 'thin', baseline, materials, knowledgeDigest: digest.summary }))
@@ -879,6 +1141,15 @@ function printRecoveryBriefing({ cwd, specBase, change, changeDir, runtimeRoot,
879
1141
  `- 剩什么:${left.length === 0 && dirty === 0 ? '活已干完' : dirty > 0 ? '活未干完(继续改代码)' : '收尾待裁决'}`,
880
1142
  `- 下一步:${left.length === 0 ? `sillyspec flow done --change ${change}` : `继续干活;干完跑 flow done(断点续)`}`,
881
1143
  ...(digestLines.length > 0 ? ['', ...digestLines] : []),
1144
+ // 尾部原型锚(2026-10-14-guide-anchor-adopt-proto):收编/含原型变更重入时原型指针不隐身
1145
+ //(首进 adopt 输出的 🖼️ 行在会话切换后已丢,简报尾部补可发现面;置于 digestLines 后=绝对
1146
+ // 尾部,长知识注入段也顶不掉——评审 P3-2 清偿)。无原型零输出。
1147
+ ...(() => {
1148
+ const protos = changeArtifactPaths(changeDir).filter((p) => /\.html$/i.test(p))
1149
+ return protos.length > 0
1150
+ ? [`- 变更原型在场(UI 实现前必看,界面/交互/流程按原型对齐):`, ...protos.map((p) => ` - ${p}`)]
1151
+ : []
1152
+ })(),
882
1153
  ].join('\n'))
883
1154
  }
884
1155
 
@@ -900,7 +1171,7 @@ export function remainingSubstepsAtFail({ snapshotSubsteps = {}, doneList = [],
900
1171
  return SUBSTEPS.filter((k) => k !== failed && snapshotSubsteps?.[k] !== 'done' && !doneNow.has(k))
901
1172
  }
902
1173
 
903
- export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null, confirmArchive = true, freezeDirty = false, acceptDirtyGap = false, acceptOrphanFace = false, allowBatchTick = false, acceptUnmapped = false }) {
1174
+ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null, confirmArchive = true, freezeDirty = false, acceptDirtyGap = false, acceptOrphanFace = false, allowBatchTick = false, acceptUnmapped = false, acceptEmptyFace = false }) {
904
1175
  const changeDir = join(specBase, 'changes', change)
905
1176
  const st = readFlowState(changeDir)
906
1177
  if (!st) {
@@ -911,6 +1182,9 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
911
1182
  console.error('❌ 本 change 已混跑回退 legacy(跑过 run <stage>)——剩余流程按厚档走 run <stage>,flow done 不再裁决')
912
1183
  process.exit(2)
913
1184
  }
1185
+ // 极速模式读态(2026-10-14-thin-turbo-mode FR-02/D-007):turbo && tier!=='thick' 双条件——
1186
+ // 升厚(distill 异态/verify 失败/--thick 起步)后 turbo 残留不短路厚档仪式,重入即恢复全量校验
1187
+ const turbo = st.turbo === true && st.tier !== 'thick'
914
1188
  const runtimeRoot = resolveRuntimeRoot(runtimeRootOpt ? { runtimeRoot: runtimeRootOpt } : {}, specBase)
915
1189
  // 归属收窄清单(2026-09-25-thin-dogfood-fixes 修复②):baseline..HEAD 不分作者——并行会话在
916
1190
  // start..done 之间的提交会混入本变更实测面与 FR 域路由。复用 verify 对账同款切分器:他侧显式
@@ -968,6 +1242,9 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
968
1242
  const r = verifyFlowDrafts({ changeDir, change, runtimeRoot, skipDesign: adoptedDesign })
969
1243
  if (r.applicable && r.schema === 2) {
970
1244
  // v2 纯 markdown 轨(2026-10-04-thin-docs-v2):violations 拒收 → 漂移 advisory → spec 断点机器门
1245
+ // 问题行机器回填留痕(2026-10-13-design-question-autorepair):先于拒收渲染(评审 P3 清偿:
1246
+ // 混合拒收场景回填已落盘,若渲染在 exit(1) 之后则留痕不可达且幂等重跑后永久丢失)。
1247
+ for (const p of r.repairs || []) console.log(`🔧 [design 问题行机器回填] ${p}`)
971
1248
  if (r.violations.length > 0) {
972
1249
  console.error(`❌ 工件校验拒收(v2 文档↔锚对比):`)
973
1250
  for (const v of r.violations) console.error(` - ${v}`)
@@ -1041,6 +1318,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1041
1318
  // 哨兵断言(2026-09-25-sentinel-wiring):tasks.md 全勾但零完成证据(区间提交消息标题与正文
1042
1319
  // 均无 task-NN token 且无对应 review.json)→ 拒收——L0 硬门接线,两道收口同一哨兵(quick 侧同判)。
1043
1320
  let _sentinelNonMirrorTasks = null // 镜像豁免面外的任务数(null=哨兵未跑;0=纯镜像任务面——节奏 advisory 静默)
1321
+ let _sentinelEvidenceComplete = false // 哨兵证据完备(status=complete——每格已勾任务均有 token/review.json):单拍降级判据(2026-10-13-batchtick-evidence-domain-hint)
1044
1322
  let _autopilotTicked = 0 // 机器代勾格数(>0 时勾选节奏门降 advisory——代勾是单拍多格机械写,非 agent 纪律面)
1045
1323
  // 证据面=整条提交消息(2026-09-25-thin-done-gate-calibration 坑2:%s 只取标题行,正文里的
1046
1324
  // token 被判零证据,与文案「提交带 task-NN」口径漂移)——%B%x1e 按提交切记录,advisory 的
@@ -1107,6 +1385,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1107
1385
  }
1108
1386
  _sentinelNonMirrorTasks = sent.claimTotal
1109
1387
  if (sent.status === 'complete') {
1388
+ _sentinelEvidenceComplete = true
1110
1389
  console.log(`🛡️ 哨兵:全勾 ${sent.checked}/${sent.claimTotal} 证据齐(提交 token/review.json)`)
1111
1390
  // 勾选时点判定(2026-09-26-tick-loop-nudge,R19 行为发现):tasks.md 的 git 首次提交
1112
1391
  // == 收口窗口最后提交(一把勾模式)→ 放行但打行为提醒(不阻断——token 证据已验,
@@ -1156,16 +1435,24 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1156
1435
  // 单源(resolveBatchTickAction)四态与豁免优先序见其 JSDoc——代勾须解释整跳才降级
1157
1436
  const _persistedAuto = Number(st?.autopilot_ticked) || 0
1158
1437
  const _autoN = Math.max(_autopilotTicked, _persistedAuto)
1159
- const v = resolveBatchTickAction({ batchTick: batch, nonMirrorCount: _sentinelNonMirrorTasks, allowBatchTick, autopilotTicked: _autoN })
1438
+ const v = resolveBatchTickAction({ batchTick: batch, nonMirrorCount: _sentinelNonMirrorTasks, allowBatchTick, autopilotTicked: _autoN, evidenceComplete: _sentinelEvidenceComplete })
1160
1439
  const at = Number.isFinite(batch.ts) ? new Date(batch.ts).toLocaleTimeString() : '未知时刻'
1161
1440
  if (v.action === 'bypass') {
1162
1441
  try { writeFlowState(changeDir, { allow_batch_tick: true }) } catch { /* 留痕 best-effort */ }
1163
1442
  console.warn(`⚠️ --allow-batch-tick:单拍多格勾选(${batch.detail},${at})硬门显式旁路——留痕 flow-state 与平台时间线`)
1164
1443
  } else if (v.action === 'advisory') {
1165
- const why = v.reason === 'autopilot-ticked'
1166
- ? `机器代勾 ${_autoN} 格(governance-autopilot 单拍机械写,非 agent 纪律面;本拍 ${_autopilotTicked}/持久化 ${_persistedAuto})`
1167
- : '哨兵任务面未知(fail-open 防误拒)'
1168
- console.warn(`⚠️ 勾选节奏:单拍多格勾选(${batch.detail},${at})——${why},降级提醒不拒`)
1444
+ // 证据完备降级(2026-10-13-batchtick-evidence-domain-hint):同形态 git 时点检查(一把勾
1445
+ // 模式)因 token 证据齐放行——事件流门对齐该语义;损害只剩 per-task 时间戳信号面,advisory
1446
+ // + flow-state 留痕与损害对齐。证据缺失形态不走此态(哨兵先拒 / 决策函数维持 reject)。
1447
+ if (v.reason === 'evidence-complete') {
1448
+ try { writeFlowState(changeDir, { batch_ticked: true }) } catch { /* 留痕 best-effort */ }
1449
+ console.warn(`⚠️ 勾选节奏:单拍多格勾选(${batch.detail},${at})——哨兵证据完备(每格已勾任务均有 token 提交/review.json),降级提醒不拒(batch_ticked 已留痕;下次完成单元即勾)`)
1450
+ } else {
1451
+ const why = v.reason === 'autopilot-ticked'
1452
+ ? `机器代勾 ${_autoN} 格(governance-autopilot 单拍机械写,非 agent 纪律面;本拍 ${_autopilotTicked}/持久化 ${_persistedAuto})`
1453
+ : '哨兵任务面未知(fail-open 防误拒)'
1454
+ console.warn(`⚠️ 勾选节奏:单拍多格勾选(${batch.detail},${at})——${why},降级提醒不拒`)
1455
+ }
1169
1456
  } else if (v.action === 'reject') {
1170
1457
  console.error(`🚫 单拍勾选拒收:事件流观测到一拍勾选 ${batch.detail}(${at})——勾选纪律要求逐格(做一件→勾一格→下一个),勾选任务 ${_sentinelNonMirrorTasks} 格`)
1171
1458
  console.error(' 出口:①节奏违例已既成——认知后重跑带 --allow-batch-tick 显式留痕过门(平台时间线可见旁路);②疑观测误判→sillyspec doctor 核对事件流')
@@ -1175,7 +1462,14 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1175
1462
  }
1176
1463
  }
1177
1464
  } catch { /* 节奏门 best-effort:读流失败静默(fail-open——观测缺席不阻断) */ }
1178
- const gate = await runQuickTestLintGate({ cwd, specBase, changedFiles, changeName: change, skipSentinel: true /* flow 侧哨兵带 baseline 区间,quick 侧区间不可靠——单判不双判 */ })
1465
+ // 极速模式实测门跳过(2026-10-14-thin-turbo-mode FR-02/D-002):turbo 在场不调
1466
+ // runQuickTestLintGate,合成 gate 同形状 skipped 对象(带 status:'skipped' 完整形态——
1467
+ // 下方 fmt 对 null 显示 '—',带形态才输出「skipped — 跳过原因」,审计可见;
1468
+ // 对齐 quick-audit env 逃生门 skip 形态先例 src/run/quick-audit.js:482)。
1469
+ // 失败升厚分支(下方 action==='fail')turbo 下天然不触达——升厚语义不变。
1470
+ const gate = turbo
1471
+ ? { 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 } }
1472
+ : await runQuickTestLintGate({ cwd, specBase, changedFiles, changeName: change, skipSentinel: true /* flow 侧哨兵带 baseline 区间,quick 侧区间不可靠——单判不双判 */ })
1179
1473
  if (gate && gate.action === 'fail') {
1180
1474
  console.error(`❌ 测试门 FAIL(整单 FAIL——实测失败/超时=失败,不继续 distill/归档):`)
1181
1475
  console.error(` ${gate.reason || gate.message || JSON.stringify(gate)}`)
@@ -1219,6 +1513,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1219
1513
  } catch { /* lint 件套 best-effort */ }
1220
1514
  // 失败触发升级(R7 切片四 / FR-10 / 护栏#4:不依赖 agent 主动)——剩余流程按厚档走
1221
1515
  writeFlowState(changeDir, { tier: 'thick', upgrade_reason: `verify 实测失败(${gate.reason || 'test fail'})——失败自动升厚` })
1516
+ resetThinStageRowOnUpgrade(cwd, change, specBase)
1222
1517
  console.error(' ⬆️ 已自动升厚档(tier=thick):重入修复后剩余流程按厚档语义(归档不跳过 plan.md 校验)')
1223
1518
  reportMidFail('ledger')
1224
1519
  process.exit(1)
@@ -1230,7 +1525,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1230
1525
  const fmt = (r) => r
1231
1526
  ? `${r.status}${r.command ? ` ← ${r.command}` : ''}${typeof r.durationMs === 'number' ? `(${(r.durationMs / 1000).toFixed(1)}s)` : ''}${r.resultPath ? ` 结果:${r.resultPath}` : ''}${r.status === 'skipped' && r.reason ? ` — 跳过原因:${String(r.reason).split('。')[0]}。` : ''}`
1232
1527
  : '—'
1233
- gateSummaryText = `test: ${fmt(gate && gate.test)}|lint: ${fmt(gate && gate.lint)}|门文件 ${Array.isArray(changedFiles) ? changedFiles.length : '?'} 个`
1528
+ gateSummaryText = `${turbo ? '⚡极速模式|' : ''}test: ${fmt(gate && gate.test)}|lint: ${fmt(gate && gate.lint)}|门文件 ${Array.isArray(changedFiles) ? changedFiles.length : '?'} 个`
1234
1529
  console.log(`🧾 实测面对账 — ${gateSummaryText}`)
1235
1530
  try { writeFlowState(changeDir, { gate_summary: gateSummaryText }) } catch { /* 存档 best-effort */ }
1236
1531
  // FR 腐烂 suspect(2026-09-25-thin-fr-inject-parity):quick-done 钩子迁轻量道(quick 退役后
@@ -1314,62 +1609,87 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1314
1609
  // 提交时,幂等跳过会把 change.patch/review.json 停在处置前时点(归档审计件缺处置面,
1315
1610
  // 2026-10-05-flowdone-lintfail-output 收编活体复现)。漂移 → 自动重冻结(等价 --refreeze)
1316
1611
  // + review 已有结论则隔离旧 review.json(结论对着旧冻结面不作数)并重置标记强制重评。
1317
- try {
1318
- const patchMeta = JSON.parse(readFileSync(join(changeDir, 'change-patch.json'), 'utf8'))
1319
- const { detectPatchDrift } = await import('./flow-parity.js')
1320
- const drift = detectPatchDrift({ cwd, change, freezeHead: patchMeta.head })
1321
- if (drift.drifted) {
1322
- console.warn(`⚠️ 审计时点漂移:patch 冻结(${String(patchMeta.head || '').slice(0, 8)})后另有 ${drift.ownCommits.length} 个本变更交付提交(${drift.ownCommits.slice(0, 3).join(';')}${drift.ownCommits.length > 3 ? ' 等' : ''})——幂等跳过会把审计件停在处置前时点`)
1323
- // 隔离先行(评审处置 P3:时间戳槽位防跨代覆盖——单槽 .superseded 在 Windows renameSync
1324
- // 同名碰撞会抛错;隔离失败则整体退回现状幂等跳过(fail-safe:漂移处理要么完整交付要么
1325
- // 不动,不留半套状态),指引人工处置后重跑)
1326
- const reviewPath = join(changeDir, 'review.json')
1327
- let quarantineOk = true
1328
- if (existsSync(reviewPath)) {
1329
- // 锚定保留判定(2026-10-06-review-anchor-and-negation 实测竞态:复审对着当前 HEAD
1330
- // 做的 PASS 结论落盘 26 秒后被「重冻结时刻在场=过期」口径误隔离):reviewedAgainst
1331
- // 命中当前 HEAD = 评审对象即最新交付面,结论有效——保留不隔离、review 标记不重置。
1332
- // 缺省/读失败/格式非法按未锚定处理,隔离行为不变(fail-safe 宁可多评)。
1333
- let anchoredToHead = false
1612
+ //
1613
+ // 冻结件存在性复查(2026-10-14-patch-refreeze-missing-artifact):子步标记 done 而冻结件
1614
+ // 缺席(手工误删/上次写盘中断)时,旧逻辑 readFileSync 抛错被下方 catch 吞掉 → 幂等跳过
1615
+ // → 归档无审计冻结件(multi-agent-platform 2026-10-14 实证:verify-result 记「无冻结件」,
1616
+ // 只能按 git 手工重建)。存在性复查优先于子步状态:缺件按漂移同款自动重冻结(重冻面=
1617
+ // 当前 HEAD);review 处置沿用漂移面同一隔离规则——缺基面无法判 governance-only,
1618
+ // 未锚定一律隔离(fail-safe 宁可多评)。
1619
+ const patchJsonPath = join(changeDir, 'change-patch.json')
1620
+ const patchTextPath = join(changeDir, 'change.patch')
1621
+ const missingFreeze = [
1622
+ !existsSync(patchJsonPath) ? 'change-patch.json' : null,
1623
+ !existsSync(patchTextPath) ? 'change.patch' : null,
1624
+ ].filter(Boolean)
1625
+ let drifted = false
1626
+ let drift = null
1627
+ if (missingFreeze.length > 0) {
1628
+ drifted = true
1629
+ console.warn(`⚠️ 冻结件缺失:patch 子步标记 done 但 ${missingFreeze.join('、')} 不在——存在性复查优先于子步状态,按漂移同款自动重冻结(2026-10-14-patch-refreeze-missing-artifact)`)
1630
+ } else {
1631
+ try {
1632
+ const patchMeta = JSON.parse(readFileSync(patchJsonPath, 'utf8'))
1633
+ const { detectPatchDrift } = await import('./flow-parity.js')
1634
+ drift = detectPatchDrift({ cwd, change, freezeHead: patchMeta.head })
1635
+ if (drift.drifted) {
1636
+ drifted = true
1637
+ console.warn(`⚠️ 审计时点漂移:patch 冻结(${String(patchMeta.head || '').slice(0, 8)})后另有 ${drift.ownCommits.length} 个本变更交付提交(${drift.ownCommits.slice(0, 3).join(';')}${drift.ownCommits.length > 3 ? ' 等' : ''})——幂等跳过会把审计件停在处置前时点`)
1638
+ }
1639
+ } catch { /* 漂移检测失败按现状幂等跳过(fail-safe 不新造阻断面) */ }
1640
+ }
1641
+ if (drifted) {
1642
+ // 隔离先行(评审处置 P3:时间戳槽位防跨代覆盖——单槽 .superseded 在 Windows renameSync
1643
+ // 同名碰撞会抛错;隔离失败则整体退回现状幂等跳过(fail-safe:漂移处理要么完整交付要么
1644
+ // 不动,不留半套状态),指引人工处置后重跑)
1645
+ const reviewPath = join(changeDir, 'review.json')
1646
+ let quarantineOk = true
1647
+ if (existsSync(reviewPath)) {
1648
+ // 重冻结目标 HEAD:漂移面取 detectPatchDrift.head;缺件重冻面取当前 HEAD(重冻结将锚定该面)。
1649
+ // 锚定保留判定(2026-10-06-review-anchor-and-negation 实测竞态:复审对着当前 HEAD
1650
+ // 做的 PASS 结论落盘 26 秒后被「重冻结时刻在场=过期」口径误隔离):reviewedAgainst
1651
+ // 命中重冻结目标 HEAD = 评审对象即最新交付面,结论有效——保留不隔离、review 标记不重置。
1652
+ // 缺省/读失败/格式非法按未锚定处理,隔离行为不变(fail-safe 宁可多评)。
1653
+ const refreezeHead = drift ? drift.head : String(gitQuiet(cwd, ['rev-parse', 'HEAD']) || '').trim()
1654
+ let anchoredToHead = false
1655
+ try {
1656
+ const { reviewAnchoredToHead } = await import('./flow-review.js')
1657
+ anchoredToHead = reviewAnchoredToHead(reviewPath, refreezeHead)
1658
+ } catch { /* 锚定判定失败按未锚定处理 */ }
1659
+ if (anchoredToHead) {
1660
+ console.log(` review.json 已锚定当前 HEAD(reviewedAgainst 命中 ${String(refreezeHead || '').slice(0, 8)})——评审对象即最新交付面,结论保留不隔离`)
1661
+ } else if (drift && drift.governanceOnly && !drift.touchedPromiseFace) {
1662
+ // 治理面等价保留(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop
1663
+ // 用户实证):评审结论的对象是交付面——窗口内本变更提交只碰 .sillyspec/** 治理文件且
1664
+ // 未触本变更 requirements/design(承诺面也是评审对象)时,结论对最新交付面逐字节等价,
1665
+ // 保留不隔离不重置标记;change.patch 仍走下方 driftRefreeze 自动重冻结(FR-01 后治理
1666
+ // 工件不入冻结面——治理修复的留档真相=归档目录本体文件,重冻结保交付面时点对齐)
1667
+ // (审计件覆盖最新提交面不变量不破)。此前任何后缀提交一律隔离重评——评审后修一个
1668
+ // P2 文档措辞也付整轮子代理评审费(循环,被迫手工重排执行顺序自赎)。
1669
+ console.log(` review.json 保留不隔离——窗口内本变更提交仅触及治理文件(${drift.ownFiles.length} 个,未触 requirements/design),评审对象(交付面)未变,结论对最新面等价;change.patch 自动重冻结(治理工件不入冻结面,治理修复随归档目录本体留档)`)
1670
+ } else {
1671
+ const ts = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, '')
1334
1672
  try {
1335
- const { reviewAnchoredToHead } = await import('./flow-review.js')
1336
- anchoredToHead = reviewAnchoredToHead(reviewPath, drift.head)
1337
- } catch { /* 锚定判定失败按未锚定处理 */ }
1338
- if (anchoredToHead) {
1339
- console.log(` review.json 已锚定当前 HEAD(reviewedAgainst 命中 ${String(drift.head || '').slice(0, 8)})——评审对象即最新交付面,结论保留不隔离`)
1340
- } else if (drift.governanceOnly && !drift.touchedPromiseFace) {
1341
- // 治理面等价保留(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop
1342
- // 用户实证):评审结论的对象是交付面——窗口内本变更提交只碰 .sillyspec/** 治理文件且
1343
- // 未触本变更 requirements/design(承诺面也是评审对象)时,结论对最新交付面逐字节等价,
1344
- // 保留不隔离不重置标记;change.patch 仍走下方 driftRefreeze 自动重冻结(FR-01 后治理
1345
- // 工件不入冻结面——治理修复的留档真相=归档目录本体文件,重冻结保交付面时点对齐)
1346
- // (审计件覆盖最新提交面不变量不破)。此前任何后缀提交一律隔离重评——评审后修一个
1347
- // P2 文档措辞也付整轮子代理评审费(循环,被迫手工重排执行顺序自赎)。
1348
- console.log(` review.json 保留不隔离——窗口内本变更提交仅触及治理文件(${drift.ownFiles.length} 个,未触 requirements/design),评审对象(交付面)未变,结论对最新面等价;change.patch 自动重冻结(治理工件不入冻结面,治理修复随归档目录本体留档)`)
1349
- } else {
1350
- const ts = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, '')
1351
- try {
1352
- renameSync(reviewPath, `${reviewPath}.superseded-${ts}`)
1353
- console.warn(` 旧 review.json 已隔离为 review.json.superseded-${ts}(评审结论对着旧冻结面)——重新执行评审任务书后再收口`)
1354
- } catch (e) {
1355
- quarantineOk = false
1356
- console.error(` ⚠️ 旧 review.json 隔离失败(${e.message})——本次保持现状幂等跳过,手动删除/改名 ${reviewPath} 后重跑(将自动重冻结+重评)`)
1357
- }
1673
+ renameSync(reviewPath, `${reviewPath}.superseded-${ts}`)
1674
+ console.warn(` 旧 review.json 已隔离为 review.json.superseded-${ts}(评审结论对着旧冻结面)——重新执行评审任务书后再收口`)
1675
+ } catch (e) {
1676
+ quarantineOk = false
1677
+ console.error(` ⚠️ 旧 review.json 隔离失败(${e.message})——本次保持现状幂等跳过,手动删除/改名 ${reviewPath} 后重跑(将自动重冻结+重评)`)
1358
1678
  }
1359
1679
  }
1360
- if (quarantineOk) {
1361
- driftRefreeze = true
1362
- if (st.substeps?.review === 'done' && !existsSync(reviewPath)) {
1363
- // 双故障边界(评审 P3 处置披露):writeFlowState 盘写失败时内存重置保本运行正确;
1364
- // 极端双故障(隔离成功+盘写失败)下轮残留 done 且 review.json 缺席 → review 子步
1365
- // backfill 按 exempt 误读——该窄路径未测(需 I/O 故障注入,design 边界披露)
1366
- try { writeFlowState(changeDir, { substeps: { review: null } }) } catch { /* 见上边界注释 */ }
1367
- if (st.substeps) st.substeps.review = null
1368
- if (!existsSync(reviewPath)) console.warn(' review 豁免口径对着旧冻结面——review 子步标记已重置,本次重新定档')
1369
- }
1680
+ }
1681
+ if (quarantineOk) {
1682
+ driftRefreeze = true
1683
+ if (st.substeps?.review === 'done' && !existsSync(reviewPath)) {
1684
+ // 双故障边界(评审 P3 处置披露):writeFlowState 盘写失败时内存重置保本运行正确;
1685
+ // 极端双故障(隔离成功+盘写失败)下轮残留 done 且 review.json 缺席 → review 子步
1686
+ // backfill 按 exempt 误读——该窄路径未测(需 I/O 故障注入,design 边界披露)
1687
+ try { writeFlowState(changeDir, { substeps: { review: null } }) } catch { /* 见上边界注释 */ }
1688
+ if (st.substeps) st.substeps.review = null
1689
+ if (!existsSync(reviewPath)) console.warn(' review 豁免口径对着旧冻结面——review 子步标记已重置,本次重新定档')
1370
1690
  }
1371
1691
  }
1372
- } catch { /* 漂移检测失败按现状幂等跳过(fail-safe 不新造阻断面) */ }
1692
+ }
1373
1693
  if (!driftRefreeze) skip('patch')
1374
1694
  }
1375
1695
  if (st.substeps?.patch !== 'done' || driftRefreeze) {
@@ -1472,7 +1792,12 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1472
1792
  // ——旧行为警告后继续归档,autopilot 单跑到底把三选一变成不可达选项(归档即拒收重入)。
1473
1793
  // 缺口在场且未显式处置 → 停在 patch 子步标记之前(半态可重入,与 review 中断同款
1474
1794
  // exit 语义):处置后重跑,patch 全量重建自动并入已提交 src。
1475
- if (!freezeDirty && !acceptDirtyGap) {
1795
+ // 极速模式(2026-10-14-thin-turbo-mode FR-02/D-002):缺口三选一阻断降 advisory——
1796
+ // 警告+缺口数留痕后放行,不 exit;非 turbo 路径逐字不变。
1797
+ if (turbo && !freezeDirty && !acceptDirtyGap) {
1798
+ console.warn(`⚡ 极速模式:未提交交付缺口未显式处置——降 advisory 放行(缺口数 ${freeze.dirtyWarned.length} 随收口输出留痕,审计自负)`)
1799
+ }
1800
+ if (!freezeDirty && !acceptDirtyGap && !turbo) {
1476
1801
  console.error('⛔ 未提交交付缺口未显式处置——收口阻断(不归档;处置后重跑同一条命令从断点续):')
1477
1802
  console.error(` ① git commit 这些文件(信息尾缀带 (${change}) 或 (${change}),重跑本命令——冻结面自动并入`)
1478
1803
  console.error(` ② 全部归属本变更:flow done --change ${change} --freeze-dirty`)
@@ -1521,6 +1846,76 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1521
1846
  }
1522
1847
  } catch { /* 守卫异常不阻断(fail-safe 不新造阻断面) */ }
1523
1848
  }
1849
+ // ── 声明面自证前置(2026-10-13-thin-empty-face-crosscheck):planEntries 解析与跨仓防呆
1850
+ // 告警提到 ownFiles 判空之前——空面也要跑。fire-ledger-form-strict 实证(multi-agent-platform
1851
+ // docs/sillyspec/thin-done-empty-face-no-declared-crosscheck.md 37c7d146b):跨仓交付撞空面时
1852
+ // 本段整挂在非空 else 分支内从未执行,归档件永久缺 patch 且零告警;空面恰是高危形态。
1853
+ let planEntries = []
1854
+ try {
1855
+ const { parseFileChangeListDetailed } = await import('./change-list.js')
1856
+ // repoKeys(2026-10-10-cross-repo-patch-freeze D-002@v1):注册仓清单传入——跨仓
1857
+ // 子段/cross-repo: 前缀条目才带 repo 标注(不传则跨仓声明全落主仓口径, thin 跨仓
1858
+ // 对账无从谈起)
1859
+ const { loadRegisteredRepoKeys } = await import('./scope-audit.js')
1860
+ planEntries = parseFileChangeListDetailed(join(changeDir, 'design.md'), { keepSillyspecDocs: true, repoKeys: loadRegisteredRepoKeys(specBase) })
1861
+ } catch { /* 清单缺失/不可解析 → 实际侧 only 快照行(无 verdict,与主链路降级同语义) */ }
1862
+ // 跨仓声明防呆(坑 design-filelist-crossrepo-prefix-silent-main):嫌疑判定从「首段命中
1863
+ // 注册仓」放宽为「声明路径主仓不存在即点名」(仓根相对路径形态首段不命中是盲区);
1864
+ // 首段命中注册仓的条目额外标注 key,点名+三种正确语法指引。
1865
+ const _declared = []
1866
+ for (const e of planEntries) {
1867
+ const pe = String((e && e.path) || '').split('\\').join('/')
1868
+ if (!pe || pe.startsWith('.sillyspec/')) continue
1869
+ if (e.repo) continue
1870
+ _declared.push({ path: pe })
1871
+ }
1872
+ let _repoAliases = null
1873
+ try {
1874
+ const _yamlText = existsSync(join(specBase, 'local.yaml')) ? readFileSync(join(specBase, 'local.yaml'), 'utf8') : ''
1875
+ if (_yamlText) {
1876
+ const { parseRepoRegistry } = await import('./stages/plan-postcheck.js')
1877
+ const _repos = parseRepoRegistry(_yamlText)
1878
+ if (_repos && _repos.size > 0) {
1879
+ _repoAliases = new Map()
1880
+ for (const [k, v] of _repos) {
1881
+ _repoAliases.set(k, k)
1882
+ const norm = String(v || '').split('\\').join('/')
1883
+ const bn = norm.replace(/\/+$/, '').split('/').pop()
1884
+ if (bn) _repoAliases.set(bn, k)
1885
+ }
1886
+ }
1887
+ }
1888
+ } catch { /* 注册表读取失败 → 别名标注缺席,存在性判定不受影响 */ }
1889
+ const _suspects = []
1890
+ for (const d of _declared) {
1891
+ if (existsSync(join(cwd, d.path))) continue
1892
+ _suspects.push({ path: d.path, key: (_repoAliases && _repoAliases.get(d.path.split('/')[0])) || null })
1893
+ }
1894
+ if (_suspects.length > 0) {
1895
+ console.warn(`⚠️ 疑似跨仓声明未按规范标注(${_suspects.length} 条已按主仓路径对账):${_suspects.slice(0, 5).map((x) => `${x.path}(主仓不存在${x.key ? `,首段命中注册仓 ${x.key}` : ''})`).join('、')}${_suspects.length > 5 ? ' 等' : ''}`)
1896
+ console.warn(' 主仓无这些路径 → 冻结面将标「计划未动」、跨仓对账与 patch 冻结不触发。正确写法三选一:① 段头「## <注册key> 仓变更」段内写仓根相对路径;② 子段「### <注册key>」;③ 条目前缀 cross-repo:<注册key>:<仓根相对路径>——key 以 local.yaml repos 注册为准')
1897
+ }
1898
+ // ── 空面门:空冻结面 + 声明清单有未标注交付条目 → 阻断(三处置一次给全;fire-ledger-form-strict 形态补位)──
1899
+ if (ownFiles.length === 0 && _declared.length > 0 && !acceptEmptyFace && !st.accept_empty_face) {
1900
+ console.error(`❌ 冻结面为空但 design 声明清单有 ${_declared.length} 条未标注交付条目(实现不在冻结窗口——跨仓交付撞空面的实证形态)——收口阻断(不归档;处置后重跑同一条命令从断点续):`)
1901
+ for (const x of _suspects.slice(0, 5)) console.error(` - ${x.path}${x.key ? `(首段命中注册仓 ${x.key})` : '(主仓不存在)'}`)
1902
+ console.error(' ① 实现在主仓:先提交后重跑')
1903
+ console.error(' ② 跨仓交付:清单按规范标注后重跑——正确写法三选一:段头「## <注册key> 仓变更」段内写仓根相对路径;子段「### <注册key>」;条目前缀 cross-repo:<注册key>:<仓根相对路径>(key 以 local.yaml repos 注册为准)')
1904
+ console.error(` ③ 纯文档变更/清单误列:同步裁剪 design 清单后重跑;或显式签收:flow done --change ${change} --accept-empty-face(留痕 flow-state,重跑免重拦)`)
1905
+ reportMidFail('patch')
1906
+ process.exit(1)
1907
+ }
1908
+ if (ownFiles.length === 0 && _declared.length > 0 && (acceptEmptyFace || st.accept_empty_face)) {
1909
+ if (acceptEmptyFace && !st.accept_empty_face) {
1910
+ writeFlowState(changeDir, { accept_empty_face: { at: new Date().toISOString(), declaredCount: _declared.length, by: process.env.SILLYSPEC_SESSION_ID || null } })
1911
+ st.accept_empty_face = true
1912
+ }
1913
+ console.warn(`⚠️ --accept-empty-face:显式签收空冻结面与声明面矛盾(${_declared.length} 条声明交付不进冻结面,留痕 flow-state accept_empty_face)`)
1914
+ }
1915
+ // 纯跨仓规范标注空面:合规书写不拦,advisory 点名 thin 空面分支不做跨仓冻结的已知缺口
1916
+ if (ownFiles.length === 0 && _declared.length === 0 && planEntries.some((e) => e && e.repo)) {
1917
+ console.warn('⚠️ 声明清单全部为跨仓规范标注且主仓冻结面为空——thin 空面分支不做跨仓冻结与 patch 留档(缺口已知:归档件将无 patch 两件;如需冻结请主仓侧交付或待后续版本支持空面跨仓冻结)')
1918
+ }
1524
1919
  if (ownFiles.length === 0) {
1525
1920
  console.log('📦 patch 留档跳过:本变更可归属文件面为空(无交付面改动——治理工件不入冻结面;若实现代码应在此,先提交后重跑)')
1526
1921
  patchOk = true
@@ -1586,15 +1981,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1586
1981
  // 收口留痕单套写(2026-10-09-close-trace-single-set):共用 writeCloseTraceArtifacts
1587
1982
  // 只写 change.patch + change-patch.json 两件——对账面数据并入 JSON 的 scopeAudit
1588
1983
  // 子对象(thin 通道同样有对账面),旧名 scope-audit.* 停写、读侧兼容兜存量归档。
1589
- let planEntries = []
1590
- try {
1591
- const { parseFileChangeListDetailed } = await import('./change-list.js')
1592
- // repoKeys(2026-10-10-cross-repo-patch-freeze D-002@v1):注册仓清单传入——跨仓
1593
- // 子段/cross-repo: 前缀条目才带 repo 标注(不传则跨仓声明全落主仓口径, thin 跨仓
1594
- // 对账无从谈起)
1595
- const { loadRegisteredRepoKeys } = await import('./scope-audit.js')
1596
- planEntries = parseFileChangeListDetailed(join(changeDir, 'design.md'), { keepSillyspecDocs: true, repoKeys: loadRegisteredRepoKeys(specBase) })
1597
- } catch { /* 清单缺失/不可解析 → 实际侧 only 快照行(无 verdict,与主链路降级同语义) */ }
1984
+ // planEntries 解析与跨仓防呆告警已前移至 ownFiles 判空之前(2026-10-13-thin-empty-face-crosscheck)。
1598
1985
  // 跨仓声明在场 → 共享集成层分仓对账 + patch 冻结(D-001/D-002@v1):与 heavy 通道
1599
1986
  // (execute --done)同源 reconcileCrossRepoPlan,repos[](锚点/计数/patch 正文)
1600
1987
  // 随 snapObj 进 change-patch.json;任何异常 fail-soft 退 ⊘ 补行现行为(不阻断收口)
@@ -1609,6 +1996,9 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1609
1996
  changeName: change,
1610
1997
  planEntries,
1611
1998
  collectPatch: true,
1999
+ // 跨仓基线锚透传(2026-10-13-cross-repo-baseline-anchor):flow start 记录的
2000
+ // cross_baselines → collectRepoActual 3c 档(基线..工作树窗口,token 归因主面)
2001
+ crossBaselines: (st && st.cross_baselines && typeof st.cross_baselines === 'object') ? st.cross_baselines : null,
1612
2002
  })
1613
2003
  crossRepoRows = recon.rows
1614
2004
  crossRepos = recon.repos
@@ -1700,6 +2090,13 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1700
2090
  reviewOutcome = { required: false, sampled: false, verdict: 'exempt', backfilled: true }
1701
2091
  }
1702
2092
  } catch { reviewOutcome = { required: null, verdict: 'done', backfilled: true } }
2093
+ } else if (turbo) {
2094
+ // 极速模式评审豁免(2026-10-14-thin-turbo-mode FR-02/D-001/D-002):用户显式极速声明=
2095
+ // 风险自担留痕,跳过 classifyReviewNeed 强制豁免(telemetry src/flow.js 整对象 JSON
2096
+ // 序列化,turboExempt 字段无破坏——Grill X-003 探针已核)。
2097
+ console.log('⚡ 极速模式:评审子步豁免(用户显式极速声明=风险自担留痕)')
2098
+ reviewOutcome = { required: false, sampled: false, verdict: 'exempt', turboExempt: true }
2099
+ mark('review')
1703
2100
  } else {
1704
2101
  const { classifyReviewNeed, renderReviewerTaskbook, validateReviewJson } = await import('./flow-review.js')
1705
2102
  let patchText = null
@@ -1790,6 +2187,10 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1790
2187
  // 视觉降级无用户裁决留痕恒拦;ui_visual_gate=error 时缺 visual-evidence.md 亦拦;
1791
2188
  // 默认 warn 档只警告不拦。与 run/gates.js verify 收尾同一检测单点,不二算)。
1792
2189
  try {
2190
+ if (turbo) {
2191
+ // 极速模式(2026-10-14-thin-turbo-mode FR-02/D-002):ui 视觉探针跳过留痕
2192
+ console.log('⚡ 极速模式:UI 视觉证据探针跳过(审计留痕)')
2193
+ } else {
1793
2194
  const uiProbe = runUiVisualProbe({ changeDir, gate: readUiVisualGate(specBase) })
1794
2195
  if (uiProbe.level === 'error') {
1795
2196
  console.error('\n❌ UI 视觉证据门(探针 12 error 档)阻断收口:')
@@ -1800,6 +2201,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1800
2201
  } else if (uiProbe.level === 'warn') {
1801
2202
  console.warn(`⚠️ UI 视觉证据(探针 12 warn 档):${uiProbe.notes.join(';')}`)
1802
2203
  }
2204
+ }
1803
2205
  } catch (e) {
1804
2206
  console.warn(`⚠️ UI 视觉证据探针异常(fail-soft 降级放行):${(e && e.message) || e}`)
1805
2207
  }
@@ -1808,7 +2210,9 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1808
2210
  // 竞争、在途残留;hunk_gate warn 默认(警告)/error(阻断)/off。与「提交面夹带嫌疑
1809
2211
  // advisory」互补:advisory 在冻结面采集处看文件名,本门在 probes 执法看 hunk 与竞争面。
1810
2212
  try {
1811
- const gate = readHunkGate(specBase)
2213
+ // 极速模式(2026-10-14-thin-turbo-mode FR-02/D-002):hunk 门强制降 warn 档——
2214
+ // 判定链路照跑(三类信号照常渲染),仅 error 阻断档解除(降 advisory 留痕不阻断)。
2215
+ const gate = turbo ? 'warn' : readHunkGate(specBase)
1812
2216
  let committedForGate = []
1813
2217
  try {
1814
2218
  committedForGate = String(gitQuiet(cwd, ['diff', '--name-only', `${st.baseline_commit}..HEAD`]) || '')
@@ -1885,6 +2289,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1885
2289
  const abnormal = res && (res.rejected > 0 || res.needsWait > 0)
1886
2290
  if (abnormal) {
1887
2291
  writeFlowState(changeDir, { tier: 'thick', upgrade_reason: `distill 异态(rejected=${res.rejected} needsWait=${res.needsWait})——升厚留人工裁决` })
2292
+ resetThinStageRowOnUpgrade(cwd, change, specBase)
1888
2293
  console.warn(`⚠️ distill 异态 → 已升厚档(tier=thick)。剩余流程按厚档走 run <stage> 完整仪式;归档子步仍将执行(厚档语义不跳过 plan.md 校验)。`)
1889
2294
  }
1890
2295
  } catch (e) {
@@ -2211,6 +2616,8 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2211
2616
  withTasks: hasFlag('--with-tasks'),
2212
2617
  reviewForce: hasFlag('--review') ? true : hasFlag('--no-review') ? false : null,
2213
2618
  autopilot: hasFlag('--autopilot'),
2619
+ prestageAck: hasFlag('--no-prestage'),
2620
+ turbo: hasFlag('--turbo'),
2214
2621
  cwd, specBase,
2215
2622
  json: hasFlag('--json') || jsonGlobal,
2216
2623
  })
@@ -2236,6 +2643,15 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2236
2643
  await reportAgentLog(change, sub)
2237
2644
  return cmdFlowReject({ change, cwd, specBase, reason: getFlag('--reason') })
2238
2645
  }
2646
+ if (sub === 'diff') {
2647
+ // 只读查询面(2026-10-13-flow-diff-cmd):change 粒度文件统计双态口——与 status 同级自愿面
2648
+ const change = getFlag('--change')
2649
+ if (!change) { console.error('❌ flow diff 需 --change <名>'); process.exit(2) }
2650
+ validateChangeName(change)
2651
+ await reportAgentLog(change, sub)
2652
+ await cmdFlowDiff({ change, cwd, specBase, json: jsonGlobal || hasFlag('--json') })
2653
+ return
2654
+ }
2239
2655
  if (sub === 'status') {
2240
2656
  // 三断点配套(2026-09-25-flow-checkpoints):随时可查当前变更阶段/槽位/子步进度
2241
2657
  const change = getFlag('--change')
@@ -2380,7 +2796,7 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2380
2796
  console.log('🔄 --refreeze:patch 子步标记已重置,本次 done 将重新冻结(change.patch 按 baseline..HEAD 最新面重建)')
2381
2797
  }
2382
2798
  }
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') })
2799
+ 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') })
2384
2800
  }
2385
2801
  if (sub === 'amend-draft') {
2386
2802
  // 机器稿唯一留痕修改通道(R7 切片三 / FR-08):重锚哈希 + ledger amendment 审计;
@@ -2413,12 +2829,13 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2413
2829
  console.log(`✅ flow amend-draft 留痕重锚:${r.reanchored.join('、')}——ledger amendment 审计在案;editRatio=${maxRatio}(段级最大;基准=首版原文,AGENT 槽不计入)`)
2414
2830
  return r
2415
2831
  }
2416
- console.error('用法: sillyspec flow start --change <名> --input "<动机与背景+独立一行『成功标准:』+每行一条『- 可验证标准』>" [--thick|--with-tasks] | sillyspec flow done --change <名> | sillyspec flow status --change <名> [--json]')
2832
+ console.error('用法: sillyspec flow start --change <名> --input "<动机与背景+独立一行『成功标准:』+每行一条『- 可验证标准』>" [--thick|--with-tasks] [--no-prestage(hindsight 标记在场时声明需求已含决策,留痕放行)] | sillyspec flow done --change <名> | sillyspec flow status --change <名> [--json] | sillyspec flow diff --change <名> [--json](只读文件统计)')
2417
2833
  console.error(' --input 可照抄形态(引号内换行合法):')
2418
2834
  console.error(' sillyspec flow start --change <名> --input "<动机与背景>')
2419
2835
  console.error('')
2420
2836
  console.error(' 成功标准:')
2421
2837
  console.error(' - <可验证标准>"')
2838
+ console.error(' 预段同意门被拦时(上个轻量变更被 route-hindsight 标记「疑似该走预段未走」):确认需求已含决策加 --no-prestage;不清晰则先 run brainstorm --change <名>,完成后回来 flow start 收编(标记自动消费)')
2422
2839
  process.exit(2)
2423
2840
  }
2424
2841