sillyspec 3.33.0 → 3.33.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/.claude/skills/sillyspec-flow/SKILL.md +18 -10
  2. package/.github/workflows/ci.yml +11 -3
  3. package/AGENTS.md +3 -3
  4. package/README.md +2 -2
  5. package/assets/command-cards/flow.md +9 -5
  6. package/package.json +1 -1
  7. package/src/archive-delta.js +4 -3
  8. package/src/change-delete.js +6 -5
  9. package/src/contract-matrix.js +2 -1
  10. package/src/cross-placement.js +5 -1
  11. package/src/cross-repo-reconcile.js +4 -1
  12. package/src/db.js +8 -2
  13. package/src/decision-distill.js +22 -8
  14. package/src/decision-values.js +34 -0
  15. package/src/decisions-io.js +5 -2
  16. package/src/deleted-ledger.js +2 -1
  17. package/src/doctor-diagnostics.js +8 -3
  18. package/src/flow-draft.js +17 -10
  19. package/src/flow-parity.js +93 -5
  20. package/src/flow-review-pack.js +183 -0
  21. package/src/flow-review.js +45 -3
  22. package/src/flow.js +365 -109
  23. package/src/friction-ledger.js +2 -1
  24. package/src/friction-tally.js +8 -4
  25. package/src/handoff.js +2 -1
  26. package/src/hooks/worktree-guard.js +5 -5
  27. package/src/index.js +62 -21
  28. package/src/init.js +7 -30
  29. package/src/knowledge-hits.js +2 -1
  30. package/src/knowledge-vector.js +2 -1
  31. package/src/progress/consistency-doctor.js +5 -2
  32. package/src/progress/stage-machine.js +6 -2
  33. package/src/progress.js +19 -8
  34. package/src/quicklog.js +194 -9
  35. package/src/review-dispatch.js +4 -3
  36. package/src/review-tier.js +2 -1
  37. package/src/route-hindsight.js +4 -3
  38. package/src/run/bg-sync.js +10 -3
  39. package/src/run/command.js +14 -12
  40. package/src/run/complete-handlers.js +40 -37
  41. package/src/run/complete.js +12 -8
  42. package/src/run/gate-snapshot-ledger.js +2 -1
  43. package/src/run/gate-snapshot.js +102 -23
  44. package/src/run/gates.js +32 -8
  45. package/src/run/prompt.js +6 -5
  46. package/src/run/scan-profile.js +2 -1
  47. package/src/run/shared.js +109 -5
  48. package/src/run/stage.js +5 -4
  49. package/src/run/test-ledger.js +2 -1
  50. package/src/run/verify-quality-scan.js +11 -5
  51. package/src/runtime-paths.js +189 -0
  52. package/src/scan-refresh.js +4 -3
  53. package/src/scope-audit.js +13 -7
  54. package/src/sentinel-assertions.js +37 -0
  55. package/src/spec-sync.js +38 -22
  56. package/src/stage-contract.js +24 -2
  57. package/src/stage-review.js +3 -2
  58. package/src/stage-templates.js +3 -2
  59. package/src/stages/brainstorm.js +6 -5
  60. package/src/sync-noise.js +3 -2
  61. package/src/sync.js +71 -15
  62. package/src/task-done.js +2 -1
  63. package/src/task-review.js +7 -5
  64. package/src/task-tick.js +45 -1
  65. package/src/validate-artifacts.js +2 -1
  66. package/src/verify-draft.js +2 -1
  67. package/src/verify-postcheck.js +11 -10
  68. package/src/verify-probes.js +157 -52
  69. package/src/watcher.js +15 -9
  70. package/src/worktree-apply.js +2 -1
  71. package/src/write-audit.js +2 -1
  72. package/templates/agents-instruction.md +3 -3
package/src/flow.js CHANGED
@@ -25,13 +25,14 @@
25
25
  * 六子步完成标记/legacy_fallback/route_hint)——fs-atomic 原子写,缺文件=未参与 thin。
26
26
  * 幂等循 task-done 先例:子步各查自身完成标记,中断半态重入断点续,中段失败精确报告。
27
27
  */
28
- import { existsSync, readFileSync, writeFileSync, readdirSync, appendFileSync, renameSync, statSync } from 'node:fs'
28
+ import { existsSync, readFileSync, writeFileSync, readdirSync, appendFileSync, renameSync, statSync, unlinkSync } from 'node:fs'
29
+ import { runtimeStatePath, runtimeTempPath } from './runtime-paths.js'
29
30
  import { join, relative, dirname } from 'node:path'
30
31
  import { DatabaseSync } from 'node:sqlite'
31
32
  import yaml from 'js-yaml'
32
33
  import { git, gitQuiet } from './git-helper.js'
33
34
  import { writeAtomicSync } from './fs-atomic.js'
34
- import { resolveRuntimeRoot, triggerSync, assertDatedChangeName } from './run/shared.js'
35
+ import { resolveRuntimeRoot, triggerSync, triggerSyncTerminalStrict, assertDatedChangeName } from './run/shared.js'
35
36
  import { detectUiTouch, buildUiGuidanceLines, runUiVisualProbe, readUiVisualGate, UI_EVIDENCE_FILENAME } from './ui-visual.js'
36
37
  import { runHunkAttributionGate, readHunkGate, renderHunkAttributionLines } from './hunk-attribution.js'
37
38
  // 2026-10-13-thin-sync-stage:FLOW_STATE_FILE/readFlowState 单一源上移叶子模块 flow-state.js
@@ -673,7 +674,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
673
674
  const r = redraftMissingArtifacts({ changeDir, change, input: null, runtimeRoot })
674
675
  if (r.drafted.length > 0) console.log(`📌 收编补生成缺失机器稿:${r.drafted.join('、')}(brainstorm 未产的工件机器补齐,criteria 从 proposal 成功标准回提;已存在文件未动)`)
675
676
  const b = ensureBindingSlots({ changeDir })
676
- if (b.appended) console.log(`📌 收编追加测试绑定槽 ${b.slots} 枚(brainstorm requirements 无绑定面——干活时作答,flow done 校验)`)
677
+ if (b.appended) console.log(`📌 收编追加测试绑定槽 ${b.slots} 枚(brainstorm requirements 无绑定面——动码前作答,flow done 校验)`)
677
678
  } catch (e) { console.warn(`⚠️ 收编补件失败(best-effort): ${(e && e.message) || e}`) }
678
679
  // route-hindsight 首版快照(收编时点=redraftMissingArtifacts 首次落盘后):adopt 的
679
680
  // design 是头脑风暴产物、机器只补缺件——快照取收编时点内容,此后 agent 改写计入指标。
@@ -686,18 +687,24 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
686
687
  try {
687
688
  const { spawnWatcher } = await import('./watcher.js')
688
689
  const w = await spawnWatcher(cwd, change, { specBase })
689
- if (w.status === 'spawned') console.log(`🔄 [watcher] 观测旁路已拉起:事件流 .sillyspec/.runtime/watcher-events-${change}.jsonl(恒带 provisional:true)`)
690
+ if (w.status === 'spawned') console.log(`🔄 [watcher] 观测旁路已拉起:事件流 .sillyspec/.runtime/state/watcher-events-${change}.jsonl(恒带 provisional:true)`)
690
691
  } catch { /* 观测旁路 best-effort */ }
691
- // adopt 标题(用户需求 2026-09-29):头脑风暴 proposal 的 H1 是人写语义标题,
692
- // deriveTitleFromLinkedChange 提取;--title 优先;兜底变更名。adopt 路径此前完全不
693
- // 注册 changes 行——initChange 补插(幂等),既有行 title 空时回填。
692
+ // adopt 标题(2026-09-29;2026-10-16-short-change-title 提炼;2026-10-16-title-range-gate
693
+ // 硬区间 10-30):头脑风暴 proposal H1 人写语义标题(纯模板 H1 回退动机首句)→ --title
694
+ // 优先(合区间校验,超界 exit 2)→ --input 首行提炼 → 兜底变更名。收编是既成事实的
695
+ // 延续面,非 flag 的超界不硬断(收编半态不可恢复)打 ⚠️ 强提示。
694
696
  try {
695
- const { deriveTitleFromLinkedChange, deriveChangeTitle } = await import('./quicklog.js')
696
- const adoptTitle = titleFlag || deriveTitleFromLinkedChange(specBase, change) || deriveChangeTitle(input) || change
697
+ const { deriveTitleFromLinkedChange, deriveShortChangeTitle, isValidChangeTitle, shortTitleRejectMessage, shortTitleRangeWarnLine } = await import('./quicklog.js')
698
+ if (titleFlag && !isValidChangeTitle(titleFlag)) {
699
+ console.error(shortTitleRejectMessage(titleFlag))
700
+ process.exit(2)
701
+ }
702
+ const adoptTitle = titleFlag || deriveTitleFromLinkedChange(specBase, change) || deriveShortChangeTitle(input) || change
697
703
  pm.initChange(cwd, change, { title: adoptTitle })
698
704
  const _arow = pm._ensureDB(cwd).getDb().prepare('SELECT title FROM changes WHERE name = ?').get(change)
699
705
  if (_arow && !_arow.title) pm.updateChangeMeta(cwd, change, { title: adoptTitle })
700
706
  console.log(`🏷️ 变更标题:${adoptTitle}(收编自头脑风暴产物;重入 --title 可改)`)
707
+ if (!isValidChangeTitle(adoptTitle)) console.log(shortTitleRangeWarnLine(adoptTitle))
701
708
  } catch { /* adopt 标题 best-effort */ }
702
709
  const materials = materialPaths(specBase, change, changeDir)
703
710
  const artifacts = changeArtifactPaths(changeDir)
@@ -716,10 +723,11 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
716
723
  ? `🖼️ 原型在场(${prototypePaths.length} 个 HTML):实现前必看,界面/交互/流程按原型对齐;有出入以 design.md 承诺为准并在回复中说明。`
717
724
  : null,
718
725
  `【你要做的】① spec 阶段先定稿任务面:把 tasks.md 改写为工作分解(每行=可独立完成并当场验证的一步:做什么+怎么验证;全 \`- [ ]\`——代码开动前工作队列先存在);`,
726
+ `🛑 断点①(机器门):spec 面(任务面+测试绑定槽)定稿后把摘要给用户看 → flow approve --change ${change}(或用户显式 --autopilot)——批准后才动码。design 四节槽收编豁免(以 brainstorm design 为准)。`,
719
727
  `② 执行走任务循环:对 tasks.md 每个 pending 行——展示「Working on task N/M: <任务>」→ 做 → 测试绿后当场勾一格(\`- [ ]\`→\`- [x]\`:sillyspec task tick --change ${change} --task task-NN 或直接 Edit)→ 下一行;全勾后 flow done(收口硬门拒单拍多格勾选)。design/decisions 是本变更的承诺锚(flow done 豁免 design 四节槽,以其为准)。`,
720
- `requirements 测试绑定槽(收编追加)每条 FR 至少一行作答;写码前后顺手填。`,
728
+ `requirements 测试绑定槽(收编追加)每条 FR 至少一行作答;写码前作答(动码前,与任务面定稿同批)。`,
721
729
  `✅ 任务面归你(工作分解契约):tasks.md 是你的工作队列(机器种子可增删改,保持 \`- [ ] task-NN:\` 行形态)——CLI 以 checkbox 为进度状态机(task tick/flow status 实时读它),TodoWrite 类工具不替代;`,
722
- ` 勾选是收口证据面(逐格核提交 token/review.json):做一件 → 勾一格(task tick 即时回显进度与下一任务)→ 继续下一条,勿攒一把勾;`,
730
+ ` 勾选是收口证据面(逐格核提交 token/review.json):做一件 → 勾一格(task tick 即时回显进度与下一任务)→ 继续下一条,勿攒一把勾;勾选修改与任务交付同一提交入库(pathspec 带上本变更 tasks.md——task tick 回显给现成命令);`,
723
731
  ` 任务拟法:收口/推送/发布类动作不单列 task(协议自身承担,成功标准照写 requirements);确需单列的,勾选前先有消息预埋 task-NN token 的交付提交(回执类证据写进提交消息)。`,
724
732
  ` flow status --change ${change} 为自愿查看/恢复面(非协议必需,D-007 中间零必需交互;恢复时给下一任务指针与进度)。`,
725
733
  `⚠️ 交付纪律:收口前交付代码显式 pathspec 提交——冻结件范围=baseline..HEAD,未提交不进审计件。`,
@@ -780,17 +788,24 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
780
788
  writeFlowState(changeDir, { autopilot: true })
781
789
  console.log(`🤖 已声明 autopilot:spec 断点人审豁免(用户显式声明留痕)`)
782
790
  }
783
- // 变更标题补写(用户需求 2026-09-29:title=中文概括 ≤50 字,建议 ~20 字)——存量在途
784
- // 变更 title 空/缺失时的回填通道:显式 --title 恒生效(重入改标题);否则从 --input 首
785
- // 行推导、仅当库内 title 为空才写(不覆盖已有语义标题)。best-effort 不阻断恢复简报。
791
+ // 变更标题补写(2026-09-29 title=中文概括;2026-10-16-short-change-title 提炼 ≤30;
792
+ // 2026-10-16-title-range-gate 硬区间 10-30)——存量在途变更 title 空/缺失时的回填通道:
793
+ // 显式 --title 恒生效(重入改标题)且必须合区间(超界 exit 2,参数面最易修正);提炼补写
794
+ // 仅当库内 title 为空才写(首写者胜),超界不阻断(resume 不可断)打 ⚠️ 强提示。
786
795
  try {
787
- const { deriveChangeTitle } = await import('./quicklog.js')
796
+ const { deriveShortChangeTitleEx, isValidChangeTitle, shortTitleRejectMessage, shortTitleRangeWarnLine, SHORT_TITLE_HARDCUT_HINT } = await import('./quicklog.js')
797
+ if (titleFlag && !isValidChangeTitle(titleFlag)) {
798
+ console.error(shortTitleRejectMessage(titleFlag))
799
+ process.exit(2)
800
+ }
788
801
  const _row = pm._ensureDB(cwd).getDb().prepare('SELECT title FROM changes WHERE name = ?').get(change)
789
802
  const _cur = _row ? (_row.title || '') : ''
790
- const _derived = titleFlag || deriveChangeTitle(input)
791
- if (_derived && (titleFlag || !_cur)) {
792
- pm.updateChangeMeta(cwd, change, { title: _derived })
793
- console.log(`🏷️ 变更标题${titleFlag ? '已更新' : '已补写'}:${_derived}(面板显示用;建议 ~20 字中文概括,重入 --title 可改)`)
803
+ const _derived = titleFlag ? { title: titleFlag, hardCut: false } : deriveShortChangeTitleEx(input)
804
+ if (_derived && _derived.title && (titleFlag || !_cur)) {
805
+ pm.updateChangeMeta(cwd, change, { title: _derived.title })
806
+ console.log(`🏷️ 变更标题${titleFlag ? '已更新' : '已补写'}:${_derived.title}(面板显示用;须 10-30 字、建议 ~20 字,重入 --title 可改)`)
807
+ if (_derived.hardCut) console.log(SHORT_TITLE_HARDCUT_HINT)
808
+ if (!titleFlag && !isValidChangeTitle(_derived.title)) console.log(shortTitleRangeWarnLine(_derived.title))
794
809
  }
795
810
  } catch { /* 标题补写 best-effort */ }
796
811
  // 知识注入(2026-09-25-thin-fr-inject-parity):resume 路径域路由走基线以来文件面——
@@ -893,16 +908,29 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
893
908
  // 形态信号:清晰度门管「需求说不清楚」(预段收编),升厚只留用户决策(--upgrade-thick 同意门)
894
909
  // 与运行时证据(实测失败升档/edit_ratio/评审——风险面在收口时点按承诺词/diff 原语/盲维判定)。
895
910
 
896
- // 变更标题(用户需求 2026-09-29:title=中文概括 ≤50 字、建议 ~20 字,agent 总结):
897
- // --title 显式指定优先;否则 --input 首行推导(agent 在 input 首行写一句中文概括即为标题);
898
- // 两者皆缺用变更名兜底(可辨识但不达意——横幅提示下次带 --title)。
899
- let changeTitle = null
911
+ // 变更标题硬区间门(2026-10-16-title-range-gate:10-30 字必须、建议 ~20、强制 agent 命名;
912
+ // 提炼口径见 2026-10-16-short-change-title):--title 显式 > --input 首行提炼 > 变更名兜底。
913
+ // agent 命名时刻强制:不在区间/无 --input 兜底 → 拒绝启动(exit 2,在任何状态写入前——
914
+ // 零半态);平台 writer 预建空目录(自动派发,无 agent 对话面)豁免硬门仅 ⚠️ 留痕。
915
+ let _st = null
916
+ let _rejectMsg = null
900
917
  try {
901
- const { deriveChangeTitle } = await import('./quicklog.js')
902
- changeTitle = titleFlag || deriveChangeTitle(input) || change
903
- } catch { changeTitle = titleFlag || change }
918
+ const _q = await import('./quicklog.js')
919
+ _st = _q.resolveStartTitle({ titleFlag, input, change, enforce: !proceedFreshEmptyDir })
920
+ if (!_st.ok) _rejectMsg = _q.shortTitleRejectMessage(_st.kind === 'fallback' ? _st.rejectedTitle : _st.title)
921
+ } catch { /* quicklog 不可用(实践不可达):降级保持旧行为兜底 */ _st = { title: titleFlag || change, ok: true, kind: titleFlag ? 'flag' : 'fallback', hardCut: false } }
922
+ if (_rejectMsg) {
923
+ console.error(_rejectMsg)
924
+ process.exit(2)
925
+ }
926
+ const changeTitle = _st.title
904
927
  pm.initChange(cwd, change, { title: changeTitle })
905
- console.log(`🏷️ 变更标题:${changeTitle}${changeTitle === change ? '(变更名兜底——下次带 --title "<≤20 字中文概括>" 更达意)' : '(面板显示用;建议 ~20 字中文概括,重入 flow start --title 可改)'}`)
928
+ console.log(`🏷️ 变更标题:${changeTitle}(面板显示用;须 10-30 字、建议 ~20 字,重入 flow start --title 可改)`)
929
+ try {
930
+ const { isValidChangeTitle, shortTitleRangeWarnLine, SHORT_TITLE_HARDCUT_HINT } = await import('./quicklog.js')
931
+ if (_st.hardCut) console.log(SHORT_TITLE_HARDCUT_HINT)
932
+ if (proceedFreshEmptyDir && !isValidChangeTitle(changeTitle)) console.log(shortTitleRangeWarnLine(changeTitle))
933
+ } catch { /* 提示行 best-effort */ }
906
934
  try {
907
935
  const { resolveSessionIdentity } = await import('./progress.js')
908
936
  const { session } = resolveSessionIdentity({ flagSession: null, cwd })
@@ -915,7 +943,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
915
943
  try {
916
944
  const { spawnWatcher } = await import('./watcher.js')
917
945
  const r = await spawnWatcher(cwd, change, { specBase })
918
- if (r.status === 'spawned') console.log(`🔄 [watcher] 观测旁路已拉起:事件流 .sillyspec/.runtime/watcher-events-${change}.jsonl(恒带 provisional:true)`)
946
+ if (r.status === 'spawned') console.log(`🔄 [watcher] 观测旁路已拉起:事件流 .sillyspec/.runtime/state/watcher-events-${change}.jsonl(恒带 provisional:true)`)
919
947
  } catch { /* 观测旁路 best-effort,绝不阻断协议面 */ }
920
948
 
921
949
  const head = gitQuiet(cwd, ['rev-parse', 'HEAD'])
@@ -1089,17 +1117,20 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
1089
1117
  ``,
1090
1118
  `【协议调用 1/2(本次)】change 已建 + 基线锚定(baseline_commit=${baseline ? baseline.slice(0, 10) : '(无 git 历史)'})${withTasks ? ' + 任务卡模式(--with-tasks:中间自愿用 task done,收尾仍 flow done)' : ''}`,
1091
1119
  ``,
1092
- `【你要做的】直接干活:改代码、写测试。治理工件不用你写——flow done 机器做(协议记账单位=change 级)。`,
1120
+ // 顺序正典(2026-10-19-thin-order-canon):行动句必须是顺序句——实证(artifact 时间戳
1121
+ // 还原+跨仓 14 例抽样)agent 跟从【你要做的】首义行动,旧句「直接干活」把动码提前、
1122
+ // 文档与批准沦为收口仪式;正典=先答 spec 面 → 批准 → 才动码(读代码探究不算动码)。
1123
+ `【你要做的】先答 spec 面,批准后才动码:① 答 spec——requirements FR 行为句+测试绑定行、design.md 四节作答(动码前;读代码探究不算动码);② 断点①——摘要给用户看 → flow approve --change ${change}(或用户显式 --autopilot);③ 才干活——写测试、改代码。治理工件骨架 CLI 已起草、收口机器做(协议记账单位=change 级)。`,
1093
1124
  // 收口拒收红线前置(2026-10-11-flow-start-redline-frontload):钉死/空槽/绑定三面都是文档
1094
1125
  // 先行校验,实证第一次最容易踩(四问文本被改写、区间 task-NN 写法零证据被拒)——横幅
1095
1126
  // 头部先行告知,收口才暴露的规则前置到动笔前。
1096
- `🚫 收口拒收红线(文档先行校验,动笔前先读——第一次最容易踩的就是这三条):`,
1127
+ `🚫 收口拒收红线(文档须先于动码——动笔前先读,第一次最容易踩的就是这三条):`,
1097
1128
  ` ① design 四问文本与 FR 标题锚逐字钉死勿改写——问题原文保持原样、答案写在问题下方,改写/删除即收口拒收(文档↔锚逐字对比);`,
1098
- ` ② design.md 四节(做法/接口契约/边界并发四问/风险)每节至少一行——空节拒收(写「不适用:<理由>」也算答);`,
1099
- ` ③ requirements 每条 FR 须有带强度词的行为句(必须/禁止/SHOULD)+ 测试绑定行——缺任一即拒收。`,
1129
+ ` ② design.md 四节(做法/接口契约/边界并发四问/风险)每节至少一行——空节拒收(写「不适用:<理由>」也算答;动码前作答);`,
1130
+ ` ③ requirements 每条 FR 须有带强度词的行为句(必须/禁止/SHOULD)+ 测试绑定行——缺任一即拒收(动码前作答)。`,
1100
1131
  `书写面(纯 markdown 起草):requirements FR 正文/design 四节作答/测试绑定行都直接写正文——`,
1101
1132
  ` 文档内零指纹标记、零槽注释、零书写指导(规则以本横幅与命令卡为唯一源);成功标准漂移只出 advisory 不拒收。`,
1102
- `⚠️ design.md 四节动码前后顺手作答;列改动文件用独立「## 文件变更清单」节+表格(| 操作 | 路径 | 说明 |)`,
1133
+ `⚠️ design.md 四节动码前作答(它是断点① 批准的输入——后补属超期补写、留痕可审计);列改动文件用独立「## 文件变更清单」节+表格(| 操作 | 路径 | 说明 |)`,
1103
1134
  ` ——勿写在「接口契约」节内(收口按节名解析声明面)。`,
1104
1135
  ...crossRepoHintLines,
1105
1136
  `📜 requirements 每条 FR = 标题锚 + 行为句 + 按需场景块(#### 场景:名 + Given/When/Then);`,
@@ -1112,30 +1143,31 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
1112
1143
  ` 也有 1/4 抽查采样。要强制/豁免可重启时带 --review / --no-review${reviewForce === true ? '(本变更已声明 --review)' : reviewForce === false ? '(本变更已声明 --no-review)' : ''}。`,
1113
1144
  `⚠️ 交付纪律:收口前先把交付代码用显式 pathspec 提交(git add -- <文件> && git commit)——`,
1114
1145
  ` patch 冻结件范围=baseline..HEAD 提交面,未提交的代码不进审计件(R16 评审 P2 实证);`,
1115
- ` tasks.md 一并显式 pathspec 提交(勾选证据进 git 历史,勿 untracked 直至归档——R19 实证)。`,
1146
+ ` tasks.md 一并显式 pathspec 提交(勾选证据进 git 历史,勿 untracked 直至归档——R19 实证;task tick 回显会给现成命令)。`,
1116
1147
  `✅ 任务面归你(工作分解契约,2026-10-07-thin-tasks-v3):tasks.md 是你的工作队列——机器种子按真实实现路径改写`,
1117
1148
  ` (每行=可独立完成并当场验证的一步:做什么+怎么验证;增删改随意,保持 \`- [ ] task-NN:\` 行形态);`,
1118
1149
  ` 执行循环:对每个未勾行 Working on task N/M → 做一件 → 测试绿后当场勾这一格 → 下一行——CLI 以 checkbox 为进度状态机`,
1119
1150
  ` (sillyspec task tick --change ${change} --task task-NN 即时回显进度 N/M 与下一任务指针,flow status 同源;或直接 Edit 翻格 \`- [ ]\`→\`- [x]\`),watcher 实时上平台。`,
1120
1151
  ` ⚠️ harness 的 TodoWrite 类工具是会话内便利面,不替代 tasks.md——平台进度/收口证据只读它(0/12 事故实证:todo 全 completed 而 tasks.md 0 勾);`,
1121
- ` 勾选是收口证据面:每格勾选要有对应提交(消息带 task-NN——逐个列出或 /、,连写如 task-01/02/03,「task-01..task-05」区间写法不认)或 review.json,全勾但零证据会被拒收;勿攒一把勾(收口硬门拒单拍多格);`,
1152
+ ` 勾选是收口证据面:每格勾选要有对应提交(消息带 task-NN——逐个列出或 /、,连写如 task-01/02/03,「task-01..task-05」区间写法不认;pathspec 带上本变更 tasks.md——勾选修改与任务交付同一提交入库,task tick 回显会给现成命令)或 review.json,全勾但零证据会被拒收;勿攒一把勾(收口硬门拒单拍多格);`,
1122
1153
  // 任务拟法指引(2026-10-12-closeout-task-guidance,三次「哨兵抓收口型任务零证据」复盘):
1123
1154
  // 成功标准≠工作分解——收口/推送/发布是协议自身承担的动作;单列则证据(token 提交)必须先于勾选存在。
1124
1155
  ` 任务拟法:收口/推送/发布类动作不单列 task(提交、归档、推送是 flow start→done 协议自身承担的动作——成功标准照写 requirements,勿逐字搬进任务清单);确需单列的收口/操作型任务,勾选前先做消息预埋 task-NN token 的交付提交——回执类证据(push/publish 回执、构建日志路径)写进提交消息,勿依赖勾选后补;`,
1125
1156
  ` flow status --change ${change} 为自愿查看/恢复面(恢复时给下一任务指针与进度)。`,
1157
+ `💡 执行期用户插入决策方向(有实现影响、约束后续变更的取舍/否决/定调)→ 当场 sillyspec decisions add --change ${change} --id D-001 --title "…" 落盘(只留会话=收口档案不可见、跨变更复潮无防);改变承诺面的同步 design/tasks 对应节。`,
1126
1158
  ``,
1127
- `🛑 三断点纪律(可控性要求——用户没说「全跑完」就必须在每个断点向用户汇报并等确认):`,
1159
+ `🛑 三断点纪律(顺序纪律:①未过不动码;②③为汇报点——用户没说「全跑完」就必须在每个断点向用户汇报并等确认):`,
1128
1160
  ` ① spec 断点【机器门】:填完 FR 区和 design 四节后,把摘要给用户看(FR 条目+盲维作答+方案概述),`,
1129
1161
  ` 用户确认后请其运行 sillyspec flow approve --change ${change}(批准留痕)再动手写代码——`,
1130
1162
  ` agent 代跑时如实声明通道:--user-confirmed(转述用户已确认)/ --self-approved(agent 自批),`,
1131
1163
  ` 无 flag 按 unattested 留痕(审计面无法区分用户确认与自批)——2026-10-12-approve-channel-attest。`,
1132
- ` 方案错了返工最贵;未批准且未声明 --autopilot 时 flow done 拒收(2026-10-04-thin-docs-v2);`,
1164
+ ` 方案错了返工最贵;批准是动码前置义务(未批准且未声明 --autopilot 时 flow done 亦拒收——2026-10-04-thin-docs-v2);`,
1133
1165
  ` 用户否掉方案 → flow reject --change ${change} --reason <理由>(拒后批准须显式通道再确认)。`,
1134
1166
  ` ② 执行断点:写完代码跑完测试后,把测试结果(过了几个/挂了什么)给用户看,`,
1135
- ` 等用户确认再跑收口。`,
1167
+ ` 等用户确认再跑收口;汇报顺带对账执行期用户决策(decisions 条目列一下,无则说明无)。`,
1136
1168
  ` ③ 归档断点:flow done 跑完后(无论过/拒),把结果(归档成功/被什么拦了)给用户看。`,
1137
- ` 用户明确说「直接跑完/不用问我」→ 重跑 flow start --change ${change} --autopilot 声明豁免(留痕),`,
1138
- ` ②③断点 agent 自主跳过。`,
1169
+ ` 用户明确说「直接跑完/不用问我」→ 重跑 flow start --change ${change} --autopilot 声明豁免(用户显式授权、留痕——非 agent 自选补救通道),`,
1170
+ ` ①②③人审全免(agent 自主跑到底)。`,
1139
1171
  ` 随时可查进度:sillyspec flow status --change ${change}`,
1140
1172
  ...(detectUiTouch(input)
1141
1173
  ? [...buildUiGuidanceLines(specBase), '']
@@ -1177,18 +1209,21 @@ function printRecoveryBriefing({ cwd, specBase, change, changeDir, runtimeRoot,
1177
1209
  ? (gitQuiet(cwd, ['rev-list', '--count', `${st.baseline_commit}..HEAD`]) || '0').toString().trim()
1178
1210
  : '?'
1179
1211
  const dirty = changedFilesSinceBaseline(cwd, st.baseline_commit).length
1180
- const hasLedger = existsSync(join(runtimeRoot, `verify-quality-scan-${change}.json`))
1212
+ const hasLedger = existsSync(runtimeStatePath(runtimeRoot, `verify-quality-scan-${change}.json`))
1181
1213
  console.log([
1182
1214
  `🔁 flow start 恢复简报(重入): ${change}(tier=${st.tier}${st.legacy_fallback ? ',legacy_fallback=true 混跑回退中' : ''})`,
1183
1215
  `══════════════════════════════════════════════════════`,
1184
1216
  ...(title ? [`- 标题:${title}`] : []),
1185
1217
  `- 做到哪:任务勾选 ${checked}/${total};基线以来提交 ${commits} 个;变更/dirty 文件 ${dirty} 个;P2 质量扫描记录 ${hasLedger ? '在场' : '无'}。`,
1186
1218
  `- 六子步标记:${done.length ? `已完成 ${done.join('/')}` : '(无)'}${left.length ? `;待办 ${left.join('/')}` : ';全部完成'}`,
1219
+ // 顺序正典(2026-10-19-thin-order-canon):恢复面带批准状态——重入时 agent 只看简报,
1220
+ // 未批准时「下一步」必须指向补批而非「继续干活」(防先码后批在恢复路径复发)。
1221
+ `- spec 批准:${st.spec_approved ? `✅ 已批准(channel=${approveChannelOf(st)})` : st.autopilot ? '🔑 autopilot 豁免(用户显式授权)' : '⬜ 未批准——动码前先过断点①(摘要给用户 → flow approve)'}`,
1187
1222
  // 红线重申(2026-10-11-flow-start-redline-frontload):重入不出首进横幅——钉死规则与
1188
1223
  // task-NN token 形态随简报补位,断点续跑生命周期内规则不丢。
1189
1224
  `- 红线重申:design 四问文本/FR 标题锚逐字钉死勿改写(改写即拒收);勾选证据提交消息逐个带 task-NN 或 /、,连写(「task-01..task-05」区间写法不认)。`,
1190
1225
  `- 剩什么:${left.length === 0 && dirty === 0 ? '活已干完' : dirty > 0 ? '活未干完(继续改代码)' : '收尾待裁决'}`,
1191
- `- 下一步:${left.length === 0 ? `sillyspec flow done --change ${change}` : `继续干活;干完跑 flow done(断点续)`}`,
1226
+ `- 下一步:${left.length === 0 ? `sillyspec flow done --change ${change}` : !st.spec_approved && !st.autopilot ? `未批准先补批(摘要给用户 → sillyspec flow approve --change ${change});随后继续干活,干完跑 flow done(断点续)` : `继续干活;干完跑 flow done(断点续)`}`,
1192
1227
  ...(digestLines.length > 0 ? ['', ...digestLines] : []),
1193
1228
  // 尾部原型锚(2026-10-14-guide-anchor-adopt-proto):收编/含原型变更重入时原型指针不隐身
1194
1229
  //(首进 adopt 输出的 🖼️ 行在会话切换后已丢,简报尾部补可发现面;置于 digestLines 后=绝对
@@ -1265,7 +1300,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1265
1300
  // 不许在失败面丢失;成功收口走函数末尾同款记录,失败面只多不少)
1266
1301
  const appendTelemetry = (review) => {
1267
1302
  try {
1268
- appendFileSync(join(runtimeRoot, 'flow-telemetry.jsonl'), JSON.stringify({
1303
+ appendFileSync(runtimeTempPath(runtimeRoot, 'flow-telemetry.jsonl'), JSON.stringify({
1269
1304
  ts: new Date().toISOString(), change, protocolCalls: 2,
1270
1305
  draftAmendments: st.edit_ratio != null ? 1 : 0, editRatio: st.edit_ratio ?? null,
1271
1306
  routeHint: st.route_hint ?? null, tier: st.tier, upgraded: st.upgrade_reason ?? null,
@@ -1305,10 +1340,10 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1305
1340
  // spec 断点机器门(FR-06/FR-07,护栏#2 零 prompt 劝说——advisory 断点在 0/12 勾选类事故
1306
1341
  // 实证下无牙;v2 起批准是收口硬前提,autopilot 是用户显式豁免通道)
1307
1342
  if (!st.autopilot && !st.spec_approved) {
1308
- console.error(`❌ spec 断点未批准:本变更(v2 起草)收口需要方案确认留痕`)
1309
- console.error(` 修复:把 FR 条目+design 四节摘要给用户看,用户确认后由用户运行:`)
1343
+ console.error(`❌ spec 断点未批准:批准是动码前置义务(不是收口补救动作)——本变更缺方案确认留痕`)
1344
+ console.error(` 补批通道(留痕可审计):把 FR 条目+design 四节摘要给用户看,用户确认后由用户运行:`)
1310
1345
  console.error(` sillyspec flow approve --change ${change} [--user-confirmed|--self-approved]`)
1311
- console.error(` 或用户显式豁免(自主跑到底):重跑 flow start --change ${change} --autopilot(声明留痕)`)
1346
+ console.error(` 或用户显式豁免(须用户授权,非 agent 自选):重跑 flow start --change ${change} --autopilot(声明留痕)`)
1312
1347
  reportMidFail('artifacts')
1313
1348
  process.exit(1)
1314
1349
  }
@@ -1369,6 +1404,22 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1369
1404
  let _sentinelNonMirrorTasks = null // 镜像豁免面外的任务数(null=哨兵未跑;0=纯镜像任务面——节奏 advisory 静默)
1370
1405
  let _sentinelEvidenceComplete = false // 哨兵证据完备(status=complete——每格已勾任务均有 token/review.json):单拍降级判据(2026-10-13-batchtick-evidence-domain-hint)
1371
1406
  let _autopilotTicked = 0 // 机器代勾格数(>0 时勾选节奏门降 advisory——代勾是单拍多格机械写,非 agent 纪律面)
1407
+ // 勾选观测共享采集(2026-10-17-tick-timing-source):watcher 事件流 + task-ticks.jsonl 侧车
1408
+ // 一次读取——时点判定与勾选节奏门同源消费(此前节奏门单独读,现两门一份数据;检测逻辑
1409
+ // 与优先序不变:事件流先判、空结果回退侧车突发)。侧车=CLI tick 逐格毫秒时戳事实源(随
1410
+ // 归档进 git、不受观测盲窗影响)。任一源缺失按空(fail-open),绝不阻断收口。
1411
+ const _tickObs = await (async () => {
1412
+ try {
1413
+ const { readWatcherEvents } = await import('./watcher.js')
1414
+ const { detectBatchCheckCadence, detectTickBurst } = await import('./sentinel-assertions.js')
1415
+ const stream = readWatcherEvents({ runtimeRoot, change })
1416
+ let ticks = []
1417
+ try { ticks = (await import('./timeline.js')).parseTickLog(readFileSync(join(changeDir, 'task-ticks.jsonl'), 'utf8')) } catch { /* 侧车缺失/不可读按空(fail-open) */ }
1418
+ let batch = stream.exists ? detectBatchCheckCadence(stream.events) : null
1419
+ if (!batch) batch = detectTickBurst(ticks)
1420
+ return { stream, ticks, batch }
1421
+ } catch { return { stream: { exists: false, events: [] }, ticks: [], batch: null } }
1422
+ })()
1372
1423
  // 证据面=整条提交消息(2026-09-25-thin-done-gate-calibration 坑2:%s 只取标题行,正文里的
1373
1424
  // token 被判零证据,与文案「提交带 task-NN」口径漂移)——%B%x1e 按提交切记录,advisory 的
1374
1425
  // 提交计数不因多行正文失真。
@@ -1436,20 +1487,31 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1436
1487
  if (sent.status === 'complete') {
1437
1488
  _sentinelEvidenceComplete = true
1438
1489
  console.log(`🛡️ 哨兵:全勾 ${sent.checked}/${sent.claimTotal} 证据齐(提交 token/review.json)`)
1439
- // 勾选时点判定(2026-09-26-tick-loop-nudge,R19 行为发现):tasks.md 的 git 首次提交
1440
- // == 收口窗口最后提交(一把勾模式)→ 放行但打行为提醒(不阻断——token 证据已验,
1441
- // 这是提醒边干边勾纪律,非假勾)。tasks.md 全程 untracked(未随交付提交)时同款提醒。
1442
- // 首提锚定(2026-10-07-tick-timing-first-commit):--reverse 全 history 首行=首次提交——
1443
- // 旧实现 -n 1 取最近一次提交,每任务随交付提交 tasks.md 的渐进形态(首提早于 HEAD)
1444
- // 被误报一把勾(完整实测实证:历史 0→1→2 渐进勾选仍出警告)。
1490
+ // 勾选时点判定(2026-10-17-tick-timing-source 换源):判据从「tasks.md git 首次提交
1491
+ // == 收口最后提交」整体退役——git 历史形态被官方允许的 reset --soft 压扁无声摧毁
1492
+ // (tick-timing-squash-blindspot 坑:并行会话压扁归档把边干边勾的渐进历史抹平,警告
1493
+ // 照出,结构性误报;且 tasks.md 不进交付 pathspec 的纪律下「untracked 直至归档」恒
1494
+ // 复现)。换源=task-ticks.jsonl 侧车墙钟时戳(detectTickTiming 纯函数:跨度超 120s
1495
+ // =边干边勾确认;挤 ≤120s 短窗且变更存活 ≥10×跨度=末尾集中勾选)——墙钟时戳是文件
1496
+ // 事实,压扁/rebase 不可改写,与「审计真相锚 change.patch sha256、不依赖 git 形态」
1497
+ // 口径对齐。节奏门已检出单拍跳/突发时让位(防双重警告——其判定覆盖同一批格子);
1498
+ // 无侧车观测输出一行 task tick 教学(不再作结构性警告);advisory 不阻断语义不变。
1445
1499
  try {
1446
- const _firstRaw = gitQuiet(cwd, ['log', '--reverse', '--format=%h', '--', tasksPath])
1447
- const tasksIn = _firstRaw ? (String(_firstRaw).split(/\r?\n/).map((x) => x.trim()).filter(Boolean)[0] || null) : null
1448
- const lastIn = gitQuiet(cwd, ['log', '--format=%h', '-n', '1'])
1449
- if (!tasksIn) {
1450
- console.warn(`⚠️ 勾选时点:tasks.md 未随交付提交(untracked 直至归档)——勾选证据链靠归档兜底,git 历史不可回溯;下次收口前 tasks.md 一并显式 pathspec 提交`)
1451
- } else if (String(tasksIn).trim() === String(lastIn).trim()) {
1452
- console.warn(`⚠️ 勾选时点:tasks.md 的首次 git 提交 == 收口最后提交(一把勾模式)——token 证据齐放行,但边干边勾是进度锚(OS Guardrails 同款纪律),下次完成单元即勾`)
1500
+ if (_tickObs.batch) {
1501
+ /* 节奏门将报单拍多格/突发——时点判定让位 */
1502
+ } else {
1503
+ const _baseRaw = st && st.baseline_commit ? gitQuiet(cwd, ['show', '-s', '--format=%ct', String(st.baseline_commit)]) : null
1504
+ const _baseTs = Number(String(_baseRaw ?? '').trim())
1505
+ const { detectTickTiming } = await import('./sentinel-assertions.js')
1506
+ const t = detectTickTiming({ ticks: _tickObs.ticks, lifespanMs: Number.isFinite(_baseTs) && _baseTs > 0 ? Date.now() - _baseTs * 1000 : NaN })
1507
+ if (t.status === 'concentrated') {
1508
+ console.warn(`⚠️ 勾选时点:${t.count} 格勾选时戳集中在 ${Math.max(1, Math.round(t.spanMs / 1000))}s 短窗内(末尾集中勾选形态)——token 证据齐放行,但边干边勾是进度锚,下次完成单元即勾(task tick 回显含现成提交命令)`)
1509
+ } else if (t.status === 'spread') {
1510
+ console.log(`✅ 勾选时点:${t.count} 格勾选时戳跨度 ${Math.max(1, Math.round(t.spanMs / 60000))} 分钟——边干边勾确认(判据=task-ticks 侧车墙钟时戳,不受 git 压扁影响)`)
1511
+ } else if (t.status === 'no-ticks') {
1512
+ console.log(`ℹ️ 勾选时点:无 tick 时戳观测(未走 task tick)——下次勾选建议 sillyspec task tick --change ${change} --task task-NN(即时回显进度与现成提交命令)`)
1513
+ }
1514
+ /* 'single'/'no-anchor' 静默:单格无节奏可言;缺存活锚 fail-open 不判 */
1453
1515
  }
1454
1516
  } catch { /* 时点判定 fail-soft */ }
1455
1517
  }
@@ -1459,7 +1521,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1459
1521
  // 任务面与实际不符可先改写 tasks.md(工作分解归 agent)再逐格勾。token 代勾(governance-
1460
1522
  // autopilot)已在上方先跑:有证据未勾的格子已代勾,此处拒的纯属零证据未完成面。
1461
1523
  if (sent.claimTotal > 0 && sent.checked < sent.claimTotal) {
1462
- console.error(`🚫 任务未全勾(${sent.checked}/${sent.claimTotal})——tasks.md 是完成状态机:完成一件勾一格(每格勾选需对应提交带 task-NN token 或 review.json),全勾后重跑 flow done`)
1524
+ console.error(`🚫 任务未全勾(${sent.checked}/${sent.claimTotal})——tasks.md 是完成状态机:完成一件勾一格(每格勾选需对应提交带 task-NN token 或 review.json;提交 pathspec 带上本变更 tasks.md——勾选修改与任务交付同一提交入库),全勾后重跑 flow done`)
1463
1525
  console.error(' 任务面与实际实现路径不符时,先改写 tasks.md(工作分解归你,增删改随意)再逐格勾;查下一任务:sillyspec task tick --change ' + `${change}` + ' --task task-NN(回显进度与指针)')
1464
1526
  appendTelemetry({ sentinel: 'incomplete-tasks', checked: sent.checked, total: sent.claimTotal })
1465
1527
  reportMidFail('ledger')
@@ -1477,19 +1539,12 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1477
1539
  // 侧车突发回退(2026-10-14-tick-burst-sidecar-gate):CLI 收口端快连点对事件流不可见(单格
1478
1540
  // 精确跳),空结果时回退 task-ticks.jsonl 毫秒时戳判突发——CLI 与直接 Edit 两条路径同判。
1479
1541
  try {
1480
- const { readWatcherEvents } = await import('./watcher.js')
1481
- const { detectBatchCheckCadence, detectTickBurst, resolveBatchTickAction } = await import('./sentinel-assertions.js')
1482
- const stream = readWatcherEvents({ runtimeRoot, change })
1483
- let batch = stream.exists ? detectBatchCheckCadence(stream.events) : null
1484
- if (!batch) {
1485
- // 侧车突发回退(2026-10-14-tick-burst-sidecar-gate):事件流空结果时读 change 目录
1486
- // task-ticks.jsonl(CLI tick 毫秒时戳事实源,随归档进 git、不受观测盲窗影响)判快连点;
1487
- // 事件流结果优先不双重报告;侧车缺失/坏文件按无证据静默(fail-open)。
1488
- try {
1489
- const { parseTickLog } = await import('./timeline.js')
1490
- batch = detectTickBurst(parseTickLog(readFileSync(join(changeDir, 'task-ticks.jsonl'), 'utf8')))
1491
- } catch { /* 侧车缺失/不可读 → 无侧车证据(fail-open) */ }
1492
- }
1542
+ const { resolveBatchTickAction } = await import('./sentinel-assertions.js')
1543
+ // 观测消费共享采集(2026-10-17-tick-timing-source):事件流/侧车/突发判定已在前文哨兵段
1544
+ // 前置采集为 _tickObs(时点判定与节奏门同源一份数据;读取仅前移,检测逻辑与优先序不变
1545
+ // ——事件流先判、空结果回退侧车突发,侧车缺失按空 fail-open)。
1546
+ const stream = _tickObs.stream
1547
+ const batch = _tickObs.batch
1493
1548
  if (batch) {
1494
1549
  // 代勾计数取「本拍实时代勾 ∨ 持久化代勾」较大者(二轮 P2-B:重入漂移防护);决策纯函数
1495
1550
  // 单源(resolveBatchTickAction)四态与豁免优先序见其 JSDoc——代勾须解释整跳才降级
@@ -1665,6 +1720,9 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1665
1720
  // untracked 自拼 hunk),基=baseline,时点=done 冻结。fail-soft:失败只告警不阻断归档、
1666
1721
  // 不标 done(重入 done 重试)。
1667
1722
  let driftRefreeze = false
1723
+ // JSON-only 冻结形态(无主仓 change.patch,跨仓正文在 repos[])——收口指引文案按它
1724
+ // 切换审计锚表述(2026-10-19-thin-empty-face-cross-repo-freeze)。
1725
+ let jsonOnlyFreeze = false
1668
1726
  if (st.substeps?.patch === 'done') {
1669
1727
  // 处置漂移检测(2026-10-05-disposition-refreeze-drift,主清单 P1):冻结后又有本变更交付
1670
1728
  // 提交时,幂等跳过会把 change.patch/review.json 停在处置前时点(归档审计件缺处置面,
@@ -1678,11 +1736,11 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1678
1736
  // 当前 HEAD);review 处置沿用漂移面同一隔离规则——缺基面无法判 governance-only,
1679
1737
  // 未锚定一律隔离(fail-safe 宁可多评)。
1680
1738
  const patchJsonPath = join(changeDir, 'change-patch.json')
1681
- const patchTextPath = join(changeDir, 'change.patch')
1682
- const missingFreeze = [
1683
- !existsSync(patchJsonPath) ? 'change-patch.json' : null,
1684
- !existsSync(patchTextPath) ? 'change.patch' : null,
1685
- ].filter(Boolean)
1739
+ // 空面跨仓冻结(2026-10-19-thin-empty-face-cross-repo-freeze):JSON-only 留档
1740
+ // (无 change.patch、repos[] 承载跨仓正文)是完整冻结,存在性复查不得判缺件。
1741
+ // 完整性以探针的 complete 为准(坏 JSON 即使 change.patch 在场也不可信)。
1742
+ const { missingFreezeLabels } = await import('./flow-parity.js')
1743
+ const missingFreeze = missingFreezeLabels(changeDir)
1686
1744
  let drifted = false
1687
1745
  let drift = null
1688
1746
  if (missingFreeze.length > 0) {
@@ -1973,11 +2031,119 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1973
2031
  }
1974
2032
  console.warn(`⚠️ --accept-empty-face:显式签收空冻结面与声明面矛盾(${_declared.length} 条声明交付不进冻结面,留痕 flow-state accept_empty_face)`)
1975
2033
  }
1976
- // 纯跨仓规范标注空面:合规书写不拦,advisory 点名 thin 空面分支不做跨仓冻结的已知缺口
1977
- if (ownFiles.length === 0 && _declared.length === 0 && planEntries.some((e) => e && e.repo)) {
1978
- console.warn('⚠️ 声明清单全部为跨仓规范标注且主仓冻结面为空——thin 空面分支不做跨仓冻结与 patch 留档(缺口已知:归档件将无 patch 两件;如需冻结请主仓侧交付或待后续版本支持空面跨仓冻结)')
2034
+ // ── 未标注跨仓交付窗口探测(2026-10-19-undeclared-cross-repo-probe)──
2035
+ // 生产实证(消防工作区 4e9eb05b,2026-10-12..10-19 共 14 例):design 无「文件
2036
+ // 变更清单」段、或清单裸写仓目录前缀时,主仓冻结面为空或只剩附件/治理模板,
2037
+ // 真实代码在注册跨仓窗口内却零告警。既有空面门只拦「清单有未标注条目」形态;
2038
+ // 本探测以真实跨仓 diff 为证据补位:无 repo 标注 + 注册仓非空 → 逐仓看
2039
+ // cross_baselines..工作树窗口,有交付改动才点名(advisory 不阻断)。纯文档变更/
2040
+ // 单仓工作区/窗口零改动 → 零输出。未提交面可能含并行会话改动,故文案用「疑似」。
2041
+ if (!planEntries.some((e) => e && e.repo)) {
2042
+ try {
2043
+ const { loadRegisteredRepoKeys } = await import('./scope-audit.js')
2044
+ const { collectRepoActual } = await import('./cross-repo-reconcile.js')
2045
+ const { filterDeliverableFiles } = await import('./worktree-apply.js')
2046
+ const { isToolGovernancePath } = await import('./flow-parity.js')
2047
+ const registeredKeys = loadRegisteredRepoKeys(specBase)
2048
+ const probeHits = []
2049
+ for (const repoKey of registeredKeys) {
2050
+ try {
2051
+ const actual = collectRepoActual({
2052
+ repoKey, specBase, cwd,
2053
+ runtimeRoot: resolveRuntimeRoot(runtimeRootOpt ? { runtimeRoot: runtimeRootOpt } : {}, specBase),
2054
+ changeName: change,
2055
+ crossBaselines: (st && st.cross_baselines && typeof st.cross_baselines === 'object') ? st.cross_baselines : null,
2056
+ })
2057
+ if (!actual || actual.degradedReason) continue
2058
+ const files = filterDeliverableFiles(actual.files || [])
2059
+ .filter((f) => !isToolGovernancePath(f))
2060
+ if (files.length > 0) probeHits.push({ key: repoKey, count: files.length })
2061
+ } catch { /* 单仓探测失败不阻断(fail-soft) */ }
2062
+ }
2063
+ if (probeHits.length > 0) {
2064
+ console.warn(`⚠️ 注册跨仓窗口内有交付改动,但 design 清单未按规范标注(主仓冻结面为空/仅治理件时归档 patch 将缺失这些交付面):${probeHits.map((h) => `${h.key}(${h.count} 文件)`).join('、')}`)
2065
+ console.warn(' 疑似跨仓交付(未提交面可能含并行会话改动,请人工确认归属)。正确写法三选一:① 段头「## <注册key> 仓变更」段内写仓根相对路径;② 子段「### <注册key>」;③ 条目前缀 cross-repo:<注册key>:<仓根相对路径>——key 以 local.yaml repos 注册为准')
2066
+ }
2067
+ } catch { /* 探测整体失败不阻断(fail-soft 不新造阻断面) */ }
2068
+ }
2069
+ // 空面跨仓冻结(2026-10-19-thin-empty-face-cross-repo-freeze):主仓冻结面为空且
2070
+ // 声明清单含规范跨仓标注时,跨仓交付面仍可留档——JSON 路线只写 change-patch.json,
2071
+ // scopeAudit.repos[] 承载各仓 patch 正文/sha256,不写 change.patch(主仓面确为空,
2072
+ // 混入跨仓正文会让按主仓 cwd 解析 hunk 的读方错位)。各仓窗口内无可归属交付时
2073
+ // 维持空面跳过(无清单段形态同此路,留给第 2 步的窗口探测点名)。
2074
+ let emptyFaceCrossRepos = []
2075
+ let emptyFaceCrossRows = undefined
2076
+ if (ownFiles.length === 0 && planEntries.some((e) => e && e.repo)) {
2077
+ try {
2078
+ const { reconcileCrossRepoPlan } = await import('./scope-audit.js')
2079
+ const recon = reconcileCrossRepoPlan({
2080
+ cwd, specBase,
2081
+ runtimeRoot: resolveRuntimeRoot(runtimeRootOpt ? { runtimeRoot: runtimeRootOpt } : {}, specBase),
2082
+ changeName: change,
2083
+ planEntries,
2084
+ collectPatch: true,
2085
+ crossBaselines: (st && st.cross_baselines && typeof st.cross_baselines === 'object') ? st.cross_baselines : null,
2086
+ })
2087
+ emptyFaceCrossRows = recon.rows
2088
+ // 「有交付」判定按对账三态行,不按 totals.files——后者含声明未动的 untouched
2089
+ // 占位行(空仓也会得到 files>0,导致空面误落 JSON-only 留档)。判定只决定
2090
+ // 落不落件;落件时 repos[] 原样携带全部仓(含降级仓与 degradedReason——
2091
+ // 与非空主仓分支同口径,读侧可感知采集失败面)。
2092
+ const deliveredRepoKeys = new Set((Array.isArray(recon.rows) ? recon.rows : [])
2093
+ .filter((r) => r && r.crossRepo && r.verdict !== 'untouched')
2094
+ .map((r) => r.crossRepo))
2095
+ const hasDelivered = deliveredRepoKeys.size > 0
2096
+ || (recon.repos || []).some((r) => r && typeof r.patch === 'string' && r.patch.length > 0)
2097
+ emptyFaceCrossRepos = hasDelivered ? (recon.repos || []) : []
2098
+ for (const n of recon.notes) console.log(` 📎 ${n}`)
2099
+ } catch { /* 跨仓对账失败 → 退回空面跳过(fail-soft 不新造阻断面) */ }
1979
2100
  }
1980
- if (ownFiles.length === 0) {
2101
+ if (ownFiles.length === 0 && emptyFaceCrossRepos.length > 0) {
2102
+ const { buildThinSnapshotRows, writeCloseTraceArtifacts } = await import('./flow-parity.js')
2103
+ const { rows: snapRows, totals: snapTotals } = buildThinSnapshotRows({ ownFiles: [], stats: new Map(), planEntries, crossRepoRows: emptyFaceCrossRows })
2104
+ const headCommit = String(gitQuiet(cwd, ['rev-parse', 'HEAD']) || '').trim() || null
2105
+ // 先清旧 change.patch 再写 JSON:JSON-only 语义下残留旧正文与新 JSON(无顶级
2106
+ // patchSha256)互相矛盾。清理失败直接抛出(外层 catch → patch 子步不标 done,
2107
+ // 重入重试),不留「双件并存且复查判完整」的自愈盲区。
2108
+ const stalePatchPath = join(changeDir, 'change.patch')
2109
+ if (existsSync(stalePatchPath)) {
2110
+ try { unlinkSync(stalePatchPath) } catch (e) {
2111
+ throw new Error(`旧 change.patch 清理失败(${e.message})——保持半态可重入,修复文件权限后重跑`)
2112
+ }
2113
+ }
2114
+ const closeTrace = writeCloseTraceArtifacts({
2115
+ changeDir,
2116
+ change,
2117
+ baseline: st.baseline_commit,
2118
+ head: headCommit,
2119
+ files: [],
2120
+ metaTotals: { files: 0, additions: 0, deletions: 0 },
2121
+ patchText: null,
2122
+ savedAt: new Date().toISOString(),
2123
+ patchStatus: 'ok',
2124
+ snapObj: {
2125
+ mode: 'full-flow', ok: true, degradedReason: null,
2126
+ baseAnchor: st.baseline_commit || null,
2127
+ totals: snapTotals,
2128
+ rows: snapRows,
2129
+ excluded: { foreignDeclared: [] },
2130
+ note: 'flow done 时点冻结(主仓冻结面为空;跨仓交付面见 repos[])',
2131
+ closedBy: 'flow done',
2132
+ repos: emptyFaceCrossRepos,
2133
+ },
2134
+ meta: {
2135
+ note: 'flow done 时点冻结(主仓冻结面为空;跨仓交付面见 scopeAudit.repos[]——2026-10-19-thin-empty-face-cross-repo-freeze)',
2136
+ modules: [], uncoveredDirs: [], moduleMaps: [],
2137
+ },
2138
+ })
2139
+ jsonOnlyFreeze = true
2140
+ const frozenRepos = emptyFaceCrossRepos.filter((r) => r && typeof r.patch === 'string' && r.patch.length > 0).length
2141
+ console.log(`📦 空面跨仓 patch 留档:change-patch.json(主仓面为空,跨仓 ${emptyFaceCrossRepos.length} 仓、正文 ${frozenRepos}——diff 见 scopeAudit.repos[])`)
2142
+ patchOk = closeTrace.patchStatus === 'ok'
2143
+ } else if (ownFiles.length === 0) {
2144
+ if (_declared.length === 0 && planEntries.some((e) => e && e.repo)) {
2145
+ console.warn('⚠️ 声明清单全部为跨仓规范标注且主仓冻结面为空——跨仓窗口内没有可归属交付,按空面跳过 patch 留档(如实现应在此,先提交后重跑)')
2146
+ }
1981
2147
  console.log('📦 patch 留档跳过:本变更可归属文件面为空(无交付面改动——治理工件不入冻结面;若实现代码应在此,先提交后重跑)')
1982
2148
  patchOk = true
1983
2149
  } else {
@@ -2131,6 +2297,13 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2131
2297
  }
2132
2298
  if (patchOk) mark('patch')
2133
2299
  }
2300
+ // 冻结形态复核(含重入 skip 路径——上轮评审 P2:断点续跑回合不执行 patch 分支,
2301
+ // jsonOnlyFreeze 会停在 false,收口指引误报 change.patch)。直接按在场冻结件判定。
2302
+ try {
2303
+ const { inspectFreezeArtifacts: inspectFinal } = await import('./flow-parity.js')
2304
+ const fsFinal = inspectFinal(changeDir)
2305
+ jsonOnlyFreeze = fsFinal.jsonOk && !fsFinal.hasText && fsFinal.repoPatchCount > 0
2306
+ } catch { /* best-effort */ }
2134
2307
 
2135
2308
  // ②c review:危险证据定档的独立评审(2026-09-25-thin-review-slice)。定档=证据累积制
2136
2309
  // (承诺词一票/盲维实质作答/diff 原语/决策密度/声明一票),文件数出局;缺省要评审、豁免要
@@ -2162,6 +2335,20 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2162
2335
  const { classifyReviewNeed, renderReviewerTaskbook, validateReviewJson } = await import('./flow-review.js')
2163
2336
  let patchText = null
2164
2337
  try { patchText = readFileSync(join(changeDir, 'change.patch'), 'utf8') } catch { /* patch 子步 skip/失败面 */ }
2338
+ // JSON-only 跨仓冻结(2026-10-19-thin-empty-face-cross-repo-freeze):无主仓
2339
+ // change.patch 时交付 diff 证据在 change-patch.json 的 scopeAudit.repos[].patch——
2340
+ // 不接上的话 classifyReviewNeed 恒判「交付 diff 不可得」,评审定档失真。
2341
+ if (patchText === null) {
2342
+ try {
2343
+ const meta = JSON.parse(readFileSync(join(changeDir, 'change-patch.json'), 'utf8'))
2344
+ const repos = meta && meta.scopeAudit && Array.isArray(meta.scopeAudit.repos) ? meta.scopeAudit.repos : []
2345
+ const repoDiff = repos
2346
+ .filter((r) => r && typeof r.patch === 'string' && r.patch.length > 0)
2347
+ .map((r) => r.patch)
2348
+ .join('\n')
2349
+ if (repoDiff) patchText = repoDiff
2350
+ } catch { /* 无/坏 JSON → 保持 null(既有判定) */ }
2351
+ }
2165
2352
  const tier = classifyReviewNeed({ changeDir, input: null, patchText, editRatio: st.edit_ratio ?? null, reviewForce: st.review_force ?? null, change })
2166
2353
  if (!tier.required) {
2167
2354
  console.log(`⚖️ 评审豁免(低风险证据齐全):${tier.exemptEvidence.join(';')}`)
@@ -2173,7 +2360,9 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2173
2360
  // 不再被漂移重冻结误隔离(实测竞态修复)。rev-parse 失败传 null(任务书退引导形态)。
2174
2361
  let taskbookHead = null
2175
2362
  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 */ }
2176
- console.log(`⚖️ 本变更需要独立评审(${tier.reasons.join(';')})——评审任务书如下,起一个干净上下文的子代理执行后重跑本命令:\n`)
2363
+ // 派发纪律(2026-10-19-thin-review-dispatch-verbatim):横幅是主会话派发时最先读到的通道,
2364
+ // 明确「原样复制」——实测手写缩编会丢材料包(厚道 stage-review.js:803 同款指令的薄道对齐)。
2365
+ console.log(`⚖️ 本变更需要独立评审(${tier.reasons.join(';')})——评审任务书如下,任务书原样复制给子代理(勿自行改写增删)执行后重跑本命令:\n`)
2177
2366
  // 项目评审探针装配(2026-10-12-review-probe-injection):local.yaml 数据驱动的项目特有
2178
2367
  // 必查面注入任务书(未配置零注入;装载告警/帽留痕 best-effort 提示,不阻断渲染)。
2179
2368
  let _probeLines = null
@@ -2183,7 +2372,34 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2183
2372
  for (const _w of _assembled.warnings) console.warn(`⚠️ review_probes:${_w}`)
2184
2373
  if (_assembled.lines.length > 0) _probeLines = _assembled.lines
2185
2374
  } catch { /* 探针面 fail-soft:装配失败零注入 */ }
2186
- console.log(renderReviewerTaskbook({ change, changeDir, head: taskbookHead, probeLines: _probeLines }))
2375
+ // 预消化材料包装配(2026-10-19-thin-review-material-pack):机械抽取 FR↔测试绑定表 +
2376
+ // 冻结 patch 文件级摘要注入任务书——砍零上下文评审员的定位类读与大 patch 整吞 prefill;
2377
+ // 摘要只从冻结件本身算(不走活 git,防与评审员手里冻结面漂移);fail-soft 零注入。
2378
+ // 归属切分(2026-10-19-thin-review-dispatch-verbatim):三单源——design 变更清单
2379
+ // (parseFileChangeListDetailed)、他侧归因(splitOwnVsForeignDiffFiles,与 patch 子步
2380
+ // 同源)、冻结面文件集(材料包同源解析)——砍评审员逐文件 git 考古(实测 22 次调用)。
2381
+ // 声明清单缺位/冻结面为空 → attribution=null(该子段零注入,FR 表/摘要不受影响)。
2382
+ let _attribution = null
2383
+ try {
2384
+ const { parseFrozenPatchSummary, buildFreezeAttribution } = await import('./flow-review-pack.js')
2385
+ const _freezeFiles = parseFrozenPatchSummary(patchText).map((f) => f.path)
2386
+ if (_freezeFiles.length > 0) {
2387
+ const { parseFileChangeListDetailed } = await import('./change-list.js')
2388
+ const _declared = parseFileChangeListDetailed(join(changeDir, 'design.md')).map((e) => e.path)
2389
+ if (_declared.length > 0) {
2390
+ const { splitOwnVsForeignDiffFiles } = await import('./foreign-declared.js')
2391
+ const _split = splitOwnVsForeignDiffFiles(cwd, change, _freezeFiles, { specBase })
2392
+ _attribution = buildFreezeAttribution({ declaredFiles: _declared, freezeFiles: _freezeFiles, foreign: _split && _split.foreign })
2393
+ }
2394
+ }
2395
+ } catch { /* 归属面 fail-soft:装配失败零注入该子段 */ }
2396
+ let _packLines = null
2397
+ try {
2398
+ const { assembleReviewPack } = await import('./flow-review-pack.js')
2399
+ const _pack = assembleReviewPack({ changeDir, patchText, attribution: _attribution })
2400
+ if (_pack && Array.isArray(_pack.lines) && _pack.lines.length > 0) _packLines = _pack.lines
2401
+ } catch { /* 材料包面 fail-soft:装配失败零注入 */ }
2402
+ console.log(renderReviewerTaskbook({ change, changeDir, head: taskbookHead, probeLines: _probeLines, packLines: _packLines }))
2187
2403
  reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'missing' }
2188
2404
  appendTelemetry(reviewOutcome) // 修评审 P2①:失败面先落账再 exit(校准信号不丢)
2189
2405
  reportMidFail('review')
@@ -2315,7 +2531,8 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2315
2531
  }
2316
2532
  } catch { /* 回读失败 → 空面(回写零命中静默) */ }
2317
2533
  }
2318
- // ④ distill:决策提炼(rejected/needsWait 异态 → 升厚留人工裁决,不静默吞)+ FR 索引提炼
2534
+ // ④ distill:决策提炼(needsWait 异态 → 升厚留人工裁决,不静默吞——判定 isDistillAbnormal
2535
+ // 单一源在 decision-distill.js,2026-10-20-distill-abnormal-upgrade-fix 修复幽灵触发器)+ FR 索引提炼
2319
2536
  // (2026-09-22-thin-fr-distill-sync:轻量变更此前只蒸馏 decisions 不调 indexRequirements——
2320
2537
  // requirements 永不进 knowledge/fr,知识复利在新默认道断流;轻量变更无 design.md,域路由
2321
2538
  // 以基线以来交付 diff 供 deliverableFiles,伪域回退同口径)
@@ -2345,13 +2562,13 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2345
2562
  }
2346
2563
  } catch (e) { console.warn(`⚠️ 变更索引回写失败(fail-soft 不阻断归档):${(e && e.message) || e}`) }
2347
2564
  try {
2348
- const { distillIntoKnowledge } = await import('./decision-distill.js')
2565
+ const { distillIntoKnowledge, isDistillAbnormal } = await import('./decision-distill.js')
2349
2566
  const knowledgeRoot = join(specBase, 'knowledge')
2350
2567
  const head = gitQuiet(cwd, ['rev-parse', 'HEAD'])
2351
2568
  const res = distillIntoKnowledge(changeDir, knowledgeRoot, typeof head === 'string' ? head.trim() : '', null)
2352
- const abnormal = res && (res.rejected > 0 || res.needsWait > 0)
2569
+ const abnormal = isDistillAbnormal(res)
2353
2570
  if (abnormal) {
2354
- writeFlowState(changeDir, { tier: 'thick', upgrade_reason: `distill 异态(rejected=${res.rejected} needsWait=${res.needsWait})——升厚留人工裁决` })
2571
+ writeFlowState(changeDir, { tier: 'thick', upgrade_reason: `distill 异态(needsWait=${res.needsWait})——升厚留人工裁决` })
2355
2572
  resetThinStageRowOnUpgrade(cwd, change, specBase)
2356
2573
  console.warn(`⚠️ distill 异态 → 已升厚档(tier=thick)。剩余流程按厚档走 run <stage> 完整仪式;归档子步仍将执行(厚档语义不跳过 plan.md 校验)。`)
2357
2574
  }
@@ -2449,7 +2666,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2449
2666
  // ⑥ events:事件收口(watcher 观测旁路 best-effort 读回,非真相源)
2450
2667
  if (st.substeps?.events === 'done') { skip('events') } else {
2451
2668
  try {
2452
- const eventsPath = join(runtimeRoot, `watcher-events-${change}.jsonl`)
2669
+ const eventsPath = runtimeStatePath(runtimeRoot, `watcher-events-${change}.jsonl`)
2453
2670
  if (existsSync(eventsPath)) {
2454
2671
  const n = readFileSync(eventsPath, 'utf8').split('\n').filter(Boolean).length
2455
2672
  console.log(`📊 watcher 事件收口:${n} 条 provisional 事件在案(.runtime 留档,平台展示面)`)
@@ -2479,12 +2696,14 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2479
2696
  const finalHead = gitQuiet(cwd, ['rev-parse', 'HEAD'])
2480
2697
  if (st.baseline_commit && typeof finalHead === 'string' && finalHead.trim() && finalHead.trim() !== st.baseline_commit) {
2481
2698
  console.log(`🧹 Git 历史可自由整理:多轮中间提交可压扁为单提交——git reset --soft ${st.baseline_commit.slice(0, 10)} && git commit -m "<最终提交信息>"`)
2482
- console.log(` 审计真相已冻结在归档件 change.patch(sha256 锚定)+ verify-result + review.json,不依赖 git 历史;`)
2699
+ console.log(` 审计真相已冻结在归档件 ${jsonOnlyFreeze ? 'change-patch.json(跨仓 repos[] 正文+sha256 锚定)' : 'change.patch(sha256 锚定)'} + verify-result + review.json,不依赖 git 历史;`)
2483
2700
  console.log(` 注:knowledge「最近确认」hash 为溯源快照,压扁后成孤儿引用属预期(非活引用)。`)
2484
2701
  }
2485
2702
  } catch { /* 指引 best-effort */ }
2486
- // 平台同步(同 flow start 尾部接线——归档后的 docs/knowledge/FR 面随本轮回推平台)
2487
- try { await triggerSync(cwd, change) } catch { /* 同步绝不阻断协议面 */ }
2703
+ // 平台同步(同 flow start 尾部接线——归档后的 docs/knowledge/FR 面随本轮回推平台)。
2704
+ // 终态强校验(2026-10-17-archive-half-state-location-stuck-active):flow done 到此 =
2705
+ // 变更已归档,文件树推送失败不再静默半态(🚨 告警 + 自救命令 + exit 1)
2706
+ try { await triggerSyncTerminalStrict(cwd, change) } catch { /* 同步绝不阻断协议面 */ }
2488
2707
  return { change, substeps: doneList }
2489
2708
  }
2490
2709
 
@@ -2519,6 +2738,39 @@ async function cmdFlowApprove({ change, cwd, specBase, by = null, channelFlag =
2519
2738
  console.error(` (拒绝后派生授权须显式再确认——2026-10-12-approve-channel-attest)`)
2520
2739
  process.exit(2)
2521
2740
  }
2741
+ // 批准前文档完整性校验(2026-10-19-approve-gate-tick-nudge FR-01/02):批准=有内容的机器
2742
+ // 事实(批时文档必齐)。复用 done 门同源校验(verifyFlowDrafts 双轨);repair:false——批准
2743
+ // 面只读,不做 design 问题行自动回填(写盘修复属 done 面)。诚实边界:本门证明「答在批前」,
2744
+ // 不证明「答在码前」——时序证据路线已实证不可行(批量收口下 git 时间戳说谎,thin-order-canon
2745
+ // 四路调查),不做假装能拦的检查。
2746
+ {
2747
+ const runtimeRoot = resolveRuntimeRoot({}, specBase)
2748
+ const { verifyFlowDrafts, verifyDesignRecordFilled } = await import('./flow-draft.js')
2749
+ const adoptedDesign = st.adopted_from === 'brainstorm' && existsSync(join(changeDir, 'design.md'))
2750
+ const r = verifyFlowDrafts({ changeDir, change, runtimeRoot, skipDesign: adoptedDesign, repair: false })
2751
+ for (const a of r.advisories || []) console.warn(`⚠️ [门柱漂移 advisory] ${a}(不阻断批准)`)
2752
+ if (r.applicable && r.violations.length > 0) {
2753
+ console.error(`❌ spec 断点未批准——文档未齐(批准是动码前置,先答 spec 面再批):`)
2754
+ for (const v of r.violations) console.error(` - ${v}`)
2755
+ console.error(` 作答齐备后重跑:sillyspec flow approve --change ${change} [--user-confirmed|--self-approved]`)
2756
+ process.exit(1)
2757
+ }
2758
+ if (!r.applicable) {
2759
+ // draft-ledger 机本位缺失(跨机批准/存量变更):fail-open 降级——仅 v1(AGENT 槽形态)跑
2760
+ // 空槽兜底;v2 纯 markdown 无 ledger 无法对锚校验,如实降级放行(不假装能查)。
2761
+ let v1Text = null
2762
+ try { v1Text = readFileSync(join(changeDir, 'design.md'), 'utf8') } catch { /* 无 design */ }
2763
+ if (v1Text && /<!--AGENT:槽\d+/.test(v1Text)) {
2764
+ const dr = verifyDesignRecordFilled({ changeDir })
2765
+ if (dr.applicable && dr.emptySlots.length > 0) {
2766
+ console.error(`❌ spec 断点未批准——design.md 有 ${dr.emptySlots.length} 个空 AGENT 槽(${dr.emptySlots.join('、')}):每节至少一行(「不适用:<理由>」也算答)`)
2767
+ console.error(` 作答齐备后重跑:sillyspec flow approve --change ${change} [--user-confirmed|--self-approved]`)
2768
+ process.exit(1)
2769
+ }
2770
+ }
2771
+ console.log(`ℹ️ 批准校验降级放行:draft-ledger 不在此机(.runtime 机本位/存量变更)——空槽兜底校验通过`)
2772
+ }
2773
+ }
2522
2774
  let approvedBy = by
2523
2775
  if (!approvedBy) {
2524
2776
  try {
@@ -2548,7 +2800,7 @@ async function cmdFlowApprove({ change, cwd, specBase, by = null, channelFlag =
2548
2800
  console.warn(`⚠️ agent 自批留痕:如用户实际未确认,请撤销——sillyspec flow reject --change ${change} --reason <理由>`)
2549
2801
  }
2550
2802
  if (patch.spec_rejected_superseded_at) console.log(` 已翻越既有拒绝记录(superseded_at=${patch.spec_rejected_superseded_at},拒绝理由保留于 spec_rejected_reason)`)
2551
- console.log(` flow done 的断点机器门已满足;agent 可继续执行/收口。撤销:sillyspec flow reject --change ${change} --reason <理由>`)
2803
+ console.log(` 批准是动码前置义务:若尚未动码,现在可以开始写码;若已动码,本次属超期补批(留痕可审计)。flow done 断点门已满足。撤销:sillyspec flow reject --change ${change} --reason <理由>`)
2552
2804
  }
2553
2805
 
2554
2806
  /**
@@ -2663,7 +2915,7 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2663
2915
  console.error(`❌ ${e.message}`)
2664
2916
  // 可照抄实例(2026-10-05-input-teach-copyable):引号内换行合法,行级 trim 容忍缩进
2665
2917
  console.error(' 重试(--input 引号内换行合法,可照抄):')
2666
- console.error(' sillyspec flow start --change <YYYY-MM-DD-简短描述> --input "<一句中文概括(首行即变更标题);随后动机与背景')
2918
+ console.error(' sillyspec flow start --change <YYYY-MM-DD-简短描述> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字);随后动机与背景')
2667
2919
  console.error('')
2668
2920
  console.error(' 成功标准:')
2669
2921
  console.error(' - <可验证标准>"')
@@ -2759,17 +3011,10 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2759
3011
  let designFilled = false, frFilled = false, bindingsFilled = 0, bindingsTotal = 0
2760
3012
  try {
2761
3013
  const dText = readFileSync(join(changeDir, 'design.md'), 'utf8')
2762
- // 双格式:v1 AGENT 槽计数;v2 纯 markdown 按节「有实质作答」判(复用 flow-review 的
2763
- // v2 节读取——问题原文行剥离;仅标题在场不算已填,防 spec 相位误跳「②执行」)
2764
- if (/<!--AGENT:槽\d+/.test(dText)) {
2765
- designFilled = Array.from(dText.matchAll(/<!--AGENT:槽\d+[^\n]*-->\n(\S)/g)).length >= 3
2766
- } else {
2767
- try {
2768
- const { readV2SectionAnswer } = await import('./flow-review.js')
2769
- const { DESIGN_QUESTIONS } = await import('./flow-draft.js')
2770
- designFilled = DESIGN_QUESTIONS.sections.filter((s) => readV2SectionAnswer(dText, s.heading, DESIGN_QUESTIONS)).length >= 3
2771
- } catch { designFilled = false }
2772
- }
3014
+ // 双格式判定单一源(2026-10-19-approve-gate-tick-nudge FR-03 上移 flow-review.js;
3015
+ // 判据与原内联逻辑逐字一致——status/tick 同源,行为零变化)
3016
+ const { designSectionsFilled } = await import('./flow-review.js')
3017
+ designFilled = designSectionsFilled(dText)
2773
3018
  } catch { /* 无 design */ }
2774
3019
  try {
2775
3020
  const rText = readFileSync(join(changeDir, 'requirements.md'), 'utf8')
@@ -2784,9 +3029,18 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2784
3029
  bindingsFilled = _rows.filter((m) => m[2].trim() && !m[2].trim().startsWith('(待填')).length
2785
3030
  }
2786
3031
  } catch { /* 无 requirements */ }
2787
- // 阶段推断
3032
+ // 阶段推断(2026-10-19-thin-order-canon:相位看批准——docs 齐但未批准仍停 ① spec 待批准,
3033
+ // 防 status 中途查询面诱导「先执行后卡点」;autopilot 是用户显式豁免、进执行段带标注)
3034
+ const specApproved = st.spec_approved === true
3035
+ const autopilotOn = st.autopilot === true
2788
3036
  let phase = '① spec(填 FR + design 槽)'
2789
- if (designFilled && frFilled && bindingsFilled >= bindingsTotal && bindingsTotal > 0) phase = '② 执行(写代码跑测试)→ ①卡点:spec 摘要给用户确认'
3037
+ if (designFilled && frFilled && bindingsFilled >= bindingsTotal && bindingsTotal > 0) {
3038
+ phase = specApproved
3039
+ ? '② 执行(已批准·写代码跑测试)'
3040
+ : autopilotOn
3041
+ ? '② 执行(autopilot 豁免·写代码跑测试)'
3042
+ : '① spec(→ 待用户批准:摘要给用户后 flow approve)'
3043
+ }
2790
3044
  if (subDone.includes('ledger')) phase = '③ 归档(flow done 收口)'
2791
3045
  // 任务心跳(2026-09-29-flow-task-heartbeat + d007-incontext 纠偏):②执行阶段把「下一个任务」
2792
3046
  // 交给机器指——当场重读 tasks.md(唯一进度源)取第一个未勾行 + 进度。进度推进本身零 CLI
@@ -2827,6 +3081,7 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2827
3081
  designFilled, frFilled, bindingsFilled, bindingsTotal,
2828
3082
  tasksChecked, tasksTotal,
2829
3083
  substeps: subDone, substepsTotal: SUBSTEPS.length,
3084
+ specApproved, specApprovedChannel: specApproved ? approveChannelOf(st) : null,
2830
3085
  ...(st.legacy_fallback ? { legacyFallback: true } : {}),
2831
3086
  }))
2832
3087
  return
@@ -2835,6 +3090,7 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2835
3090
  `📋 ${change}`,
2836
3091
  ...(changeTitle ? [` 标题:${changeTitle}`] : []),
2837
3092
  ` 阶段:${phase}`,
3093
+ ` spec 批准:${specApproved ? '✅ 已批准' : autopilotOn ? '🔑 autopilot 豁免(用户显式授权)' : '⬜ 未批准(动码前先过断点①)'}`,
2838
3094
  ` design 槽:${designFilled ? '✅ 已填' : '⬜ 未填'}|FR 区:${frFilled ? '✅ 已填' : '⬜ 未填'}|绑定槽:${bindingsFilled}/${bindingsTotal}`,
2839
3095
  hasTasks ? ` 任务勾选:${tasksChecked}/${tasksTotal}` : null,
2840
3096
  ...(heartbeat || []),
@@ -2892,9 +3148,9 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2892
3148
  console.log(`✅ flow amend-draft 留痕重锚:${r.reanchored.join('、')}——ledger amendment 审计在案;editRatio=${maxRatio}(段级最大;基准=首版原文,AGENT 槽不计入)`)
2893
3149
  return r
2894
3150
  }
2895
- 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](只读文件统计)')
3151
+ console.error('用法: sillyspec flow start --change <名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字)+动机与背景详述+独立一行『成功标准:』+每行一条『- 可验证标准』>" [--thick|--with-tasks] [--no-prestage(hindsight 标记在场时声明需求已含决策,留痕放行)] | sillyspec flow done --change <名> | sillyspec flow status --change <名> [--json] | sillyspec flow diff --change <名> [--json](只读文件统计)')
2896
3152
  console.error(' --input 可照抄形态(引号内换行合法):')
2897
- console.error(' sillyspec flow start --change <名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
3153
+ console.error(' sillyspec flow start --change <名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字);随后动机与背景')
2898
3154
  console.error('')
2899
3155
  console.error(' 成功标准:')
2900
3156
  console.error(' - <可验证标准>"')