sillyspec 3.29.5 → 3.30.0

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 (110) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +54 -54
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
  3. package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
  4. package/.claude/skills/sillyspec-flow/SKILL.md +57 -0
  5. package/.claude/skills/sillyspec-quick/SKILL.md +18 -116
  6. package/AGENTS.md +38 -20
  7. package/SKILL.md +9 -3
  8. package/assets/command-cards/flow.md +32 -0
  9. package/assets/command-cards/run-brainstorm.md +1 -1
  10. package/assets/command-cards/run-quick.md +8 -25
  11. package/package.json +2 -1
  12. package/packages/dashboard/server/executor.js +2 -1
  13. package/packages/dashboard/server/parser.js +7 -7
  14. package/r7-test-env/README-R7-TEST.md +93 -0
  15. package/r7-test-env/demo-project/lib/calc.js +6 -0
  16. package/r7-test-env/demo-project/package.json +1 -0
  17. package/r7-test-env/demo-project/test.js +10 -0
  18. package/r7-test-env/prompt-R7-SMOKE.md +41 -0
  19. package/r7-test-env/sillyspec-r7.cmd +3 -0
  20. package/round4/account.mjs +18 -5
  21. package/round4/b1-analyze-task08.mjs +40 -0
  22. package/round4/b1-b1-target-validation.md +32 -0
  23. package/round4/b3a-prompt-audit-result.md +66 -0
  24. package/round4/b3a-prompt-audit.mjs +89 -0
  25. package/round4/optimization-plan.md +165 -0
  26. package/round4/r4-final-report.html +347 -0
  27. package/round4/r5-scenario-matrix.md +46 -0
  28. package/src/change-list.js +9 -0
  29. package/src/classify-change.js +6 -0
  30. package/src/command-cards.js +2 -1
  31. package/src/commit-guard.js +2 -0
  32. package/src/config-schema.js +64 -12
  33. package/src/db.js +35 -4
  34. package/src/decision-distill.js +9 -3
  35. package/src/docs-check.js +1 -1
  36. package/src/doctor-diagnostics.js +44 -2
  37. package/src/flow-draft.js +748 -0
  38. package/src/flow-parity.js +236 -0
  39. package/src/flow-review.js +184 -0
  40. package/src/flow.js +1492 -0
  41. package/src/foreign-declared.js +17 -1
  42. package/src/fr-index.js +396 -22
  43. package/src/fs-atomic.js +5 -1
  44. package/src/git-helper.js +2 -2
  45. package/src/greenfield-bootstrap.js +98 -0
  46. package/src/handoff.js +120 -7
  47. package/src/hunk-attribution.js +191 -0
  48. package/src/index.js +570 -16
  49. package/src/knowledge-classify.js +47 -1
  50. package/src/knowledge-digest.js +162 -0
  51. package/src/knowledge-quicklog.js +246 -0
  52. package/src/machine-draft.js +128 -0
  53. package/src/machine-interface.js +107 -5
  54. package/src/mcp-server.js +1 -0
  55. package/src/module-resolve.js +3 -1
  56. package/src/preview-progress.js +160 -0
  57. package/src/progress/change-registry.js +11 -3
  58. package/src/progress/shared.js +3 -2
  59. package/src/progress.js +56 -13
  60. package/src/quicklog.js +44 -6
  61. package/src/redomain.js +147 -0
  62. package/src/review-material-pack.js +204 -1
  63. package/src/run/archive-distill.js +4 -2
  64. package/src/run/bg-sync.js +67 -0
  65. package/src/run/command.js +123 -10
  66. package/src/run/complete-handlers.js +327 -72
  67. package/src/run/complete.js +350 -71
  68. package/src/run/gate-snapshot-ledger.js +213 -0
  69. package/src/run/gate-snapshot.js +250 -34
  70. package/src/run/gates.js +370 -233
  71. package/src/run/green-cache.js +123 -0
  72. package/src/run/junction-rm.js +70 -0
  73. package/src/run/next.js +14 -0
  74. package/src/run/prompt.js +185 -55
  75. package/src/run/quick-audit.js +185 -12
  76. package/src/run/shared.js +120 -3
  77. package/src/run/stage.js +182 -216
  78. package/src/run/test-ledger.js +264 -0
  79. package/src/run/verify-quality-scan.js +84 -7
  80. package/src/scope-audit.js +6 -1
  81. package/src/sentinel-assertions.js +114 -0
  82. package/src/setup.js +3 -3
  83. package/src/spec-sync.js +41 -1
  84. package/src/stage-contract.js +26 -8
  85. package/src/stage-review-checklist.js +2 -0
  86. package/src/stage-review.js +7 -2
  87. package/src/stage-templates.js +157 -0
  88. package/src/stages/archive.js +2 -0
  89. package/src/stages/brainstorm.js +24 -106
  90. package/src/stages/execute.js +221 -84
  91. package/src/stages/knowledge.js +74 -1
  92. package/src/stages/plan-postcheck.js +352 -1
  93. package/src/stages/plan.js +23 -1
  94. package/src/stages/quick.js +10 -2
  95. package/src/stages/verify.js +4 -2
  96. package/src/sync.js +81 -23
  97. package/src/task-done.js +181 -0
  98. package/src/task-review.js +8 -55
  99. package/src/test-bindings.js +549 -0
  100. package/src/timeline.js +218 -0
  101. package/src/ui-visual.js +175 -0
  102. package/src/verify-draft.js +214 -0
  103. package/src/verify-postcheck.js +860 -158
  104. package/src/verify-probes.js +59 -3
  105. package/src/watcher.js +1031 -0
  106. package/src/worktree-apply.js +44 -4
  107. package/src/worktree-deps.js +22 -8
  108. package/src/worktree-policy.js +42 -0
  109. package/src/worktree.js +102 -15
  110. package/templates/agents-instruction.md +37 -18
package/src/index.js CHANGED
@@ -9,7 +9,7 @@
9
9
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, unlinkSync } from 'fs';
10
10
  import { writeAtomicSync } from './fs-atomic.js';
11
11
  import { basename, dirname, extname, join, resolve, isAbsolute, sep, relative } from 'path';
12
- import { safeGit, git } from './git-helper.js';
12
+ import { safeGit, git, gitQuiet } from './git-helper.js';
13
13
  import { getVersion } from './version.js';
14
14
 
15
15
  // E22(性能#2):progress.js(拖 db.js→node:sqlite 链 ~48ms)与 run/shared.js(拖 stages 全家
@@ -53,11 +53,7 @@ SillySpec CLI — 规范驱动开发工具包
53
53
  --json 输出 JSON(程序化读取)
54
54
 
55
55
  阶段特有参数:
56
- quick: --linked-changes none|a,b 显式关联变更(取代 --change,推荐)
57
- --files a.js,b.js 显式声明允许修改的文件
58
- --allow-new 允许新增文件(默认禁止)
59
- --force-baseline 允许覆盖 baseline 受保护文件
60
- --confirm 完成时确认接受变更审计
56
+ quick: (通道已退役 v3.30.0——仅收尾升级前在途会话:--done/--cancel/--change <会话ID>;新工作走 flow start)
61
57
  scan: --quick | --standard | --deep 显式选择 profile(优先于规模自动判定;三档互斥)
62
58
  --force-rescan 覆盖已有 scan 文档保护
63
59
  --diff [--base <commit>] [--full] [--report] scan 文档 vs 源码漂移清单(纯只读)
@@ -95,6 +91,10 @@ SillySpec CLI — 规范驱动开发工具包
95
91
  sillyspec config [schema] [--json] 打印 local.yaml 全部已知键 + 生效状态 + 读取点(堵外部 agent 配置发现缺口)
96
92
  sillyspec config cat [--json] 读取 local.yaml 实际内容(自动定位真实路径,worktree 内解析到主仓)
97
93
  sillyspec runtime list [--json] 枚举 .sillyspec/.runtime/ 运行时产物(只读,看手上有哪些证据/状态文件)
94
+ sillyspec watcher alerts --change <名> [--all] [--follow] [--runtime-root <路径>]
95
+ 哨兵告警查询(watcher 事件流只读可视化;--all 全事件、--follow 2s 轮询持续打印)
96
+ sillyspec watcher timeline --change <名> [--runtime-root <路径>]
97
+ 变更合成时间线(事件流 × tasks.md × git 提交锚三源真实留痕渲染;活跃/归档变更均可)
98
98
  sillyspec dispatch <probe | hint> SillyHub 派发能力探测 + 策略生成(agent 调用桥,仅渲染不执行 tool)
99
99
  sillyspec agent-log [--detect] [--json] 本地 agent 会话日志查询/现场探测(run 命令自动上报平台 POST /api/agent-logs 并本地留底)
100
100
 
@@ -145,7 +145,8 @@ SillySpec CLI — 规范驱动开发工具包
145
145
  sillyspec knowledge <search --query "..." --limit N
146
146
  | inspect --id "..."
147
147
  | validate | refresh
148
- | propose --title "..." --category <name>>
148
+ | propose --title "..." --category <name>
149
+ | inbox [--json] (知识收件箱:uncategorized 待审清单+基线态)>
149
150
  sillyspec platform <connect <url> [--token <t>]
150
151
  | disconnect
151
152
  | sync [--change <name>] | sync-docs [--change <name>]
@@ -167,11 +168,10 @@ SillySpec CLI — 规范驱动开发工具包
167
168
  示例:
168
169
  sillyspec init
169
170
  sillyspec run brainstorm --change 2026-07-03-add-login
170
- sillyspec run quick --linked-changes none --done --output "修复手机号校验"
171
+ sillyspec flow start --change my-fix --input "<动机与背景+独立一行『成功标准:』+每行一条『- 可验证标准』>" # 轻量变更(默认快道,2 次调用)
171
172
  sillyspec run verify --done --output "验证通过,测试全绿"
172
173
  sillyspec run archive --done --confirm --output "归档完成"
173
174
  sillyspec run plan --reopen --from-step 2 # 修订 plan,从第 2 步重做
174
- sillyspec run quick --non-interactive --done --output "CI 内的快修" # 脚本/CI
175
175
  sillyspec progress show
176
176
  sillyspec progress show --all # 多变更汇总时无信号变更全量单行列出(默认折叠超 8 个)
177
177
  sillyspec progress show --json # 全局总览 envelope(面板/脚本消费)
@@ -196,6 +196,36 @@ async function withJsonOutput(json, fn) {
196
196
  }
197
197
  }
198
198
 
199
+ // quick 通道直接退役(2026-09-25-quick-channel-retire):CLI 入口预门——拦「新会话启动形态」的
200
+ // quick 调用。必须在 process entry 层拒绝:run/command.js 对新会话的 id 生成 / owner.json 落盘 /
201
+ // 「📌 已建立」公告都发生在 runStage 之前,晚于此处拒绝会留幻影会话残留(违背零副作用承诺)。
202
+ // 放行面(在途收尾与校验路径,全部先于 command.js 会话建立、零副作用):
203
+ // ① --done/--cancel/--status/--skip/--reset/--reopen/--help——收尾/只读/帮助
204
+ // ② --change quick-<8hex>——在途续跑(指向不存在会话的由 stage.js 第二道网拒绝)
205
+ // ③ 含「新启合法集」之外旗标的形态——未知旗标由 command.js 未知参数校验 exit 2(该校验
206
+ // 2026-09-19 已上移至会话建立之前),放行不产生副作用;漏放行的合法收尾形态同理被
207
+ // stage.js 兜底网拒绝。进程内直调 runCommand 的入口不经此门,亦由 stage.js 兜底。
208
+ const QUICK_FRESH_START_FLAGS = new Set([
209
+ '--input', '--linked-changes', '--files', '--file-notes',
210
+ '--allow-new', '--allow-delete', '--force-baseline',
211
+ '--non-interactive', '--interactive', '--session',
212
+ '--json', '--dir', '--spec-dir', '--spec-root', '--runtime-root', '--workspace-id',
213
+ '--skip-approval', '--no-docs', '--confirm-mode',
214
+ ])
215
+ function refuseRetiredQuickFreshStart(stageArgs) {
216
+ const flags = stageArgs.slice(1).filter((a) => String(a).startsWith('-'))
217
+ const passThrough = flags.some((f) => !QUICK_FRESH_START_FLAGS.has(f))
218
+ || (() => {
219
+ const i = stageArgs.indexOf('--change')
220
+ return i !== -1 && /^quick-[0-9a-f]{8}$/.test(String(stageArgs[i + 1] || ''))
221
+ })()
222
+ if (passThrough) return false
223
+ console.error('❌ quick 通道已退役,不再接受新会话(v3.30.0 起)。')
224
+ console.error(' 新工作请走轻量变更:sillyspec flow start --change <名> --input "<动机与背景;随后独立一行『成功标准:』;再每行一条『- <可验证标准>』>"')
225
+ console.error(' 升级前进行中的 quick 会话仍可收尾:sillyspec run quick --change <会话ID> 续跑 / --done 收口 / --cancel 取消。')
226
+ process.exit(1)
227
+ }
228
+
199
229
  async function main() {
200
230
  const args = process.argv.slice(2);
201
231
 
@@ -390,6 +420,22 @@ async function main() {
390
420
  break;
391
421
  }
392
422
  pm.show(progDir, progChangeName, { all: filteredArgs.includes('--all') });
423
+ // 预览出口(2026-09-23-watcher-preview-progress task-03 / FR-04):--preview 在默认
424
+ // 输出后追加 watcher 预览段(带「预览」徽标与证据引用);缺省零行为变化(保险丝外显式通道)。
425
+ if (filteredArgs.includes('--preview') && progChangeName) {
426
+ const { readPreviewProgress } = await import('./progress.js');
427
+ const pv = readPreviewProgress(resolvePlatformSpecDir(dir, specDir) || join(progDir, '.sillyspec'), progChangeName);
428
+ console.log('');
429
+ if (!pv.exists || pv.stages.length === 0) {
430
+ console.log('👀 预览:无 watcher 预览行(变更未注册/库缺失/尚无投影)');
431
+ } else {
432
+ console.log(`👀 预览(watcher 推断,authority='watcher'——非流程真相,CLI --done 才是):${pv.stages.length} 行`);
433
+ for (const r of pv.stages) {
434
+ const files = r.evidence && Array.isArray(r.evidence.files) ? r.evidence.files.join('/') : '';
435
+ console.log(` [预览] ${r.stage} → ${r.status}${files ? `(依据:${files}${r.evidence && r.evidence.changed ? ' 本拍有变化' : ''})` : ''}`);
436
+ }
437
+ }
438
+ }
393
439
  break;
394
440
  }
395
441
  case 'check': {
@@ -524,6 +570,9 @@ async function main() {
524
570
  // 只传 cwd 致 --spec-dir 对 gate verify 无效。不传 --spec-dir 时 specBase 走默认
525
571
  // resolveSpecDir(cwd),向后兼容。
526
572
  const gateOpts = { cwd: dir };
573
+ // P1 --full 只读预检档(batch3 task-03):覆盖 --done 独有检查面(reconcile/stage review)——
574
+ // 预检过=必过(同因),省一轮 gate 失败重跑的整 agent 回合
575
+ if (args.includes('--full')) gateOpts.full = true;
527
576
  // A4:平台指针 / 显式 --spec-dir / 本地 fallback 三合一解析 specBase——平台模式(指针指向外部
528
577
  // specDir)下 gate 也能核验(原只传 cwd 致 runGate 的 resolveSpecDir 指向本地孤儿库,恒
529
578
  // 「无法核验」exit 2)。resolvePlatformSpecDir 指针失效时抛 PointerUnreachableError,由顶层 catch 优雅 fail-closed。
@@ -781,9 +830,12 @@ async function main() {
781
830
  if (!taskSub || ['help', '--help', '-h'].includes(taskSub)) {
782
831
  console.log(`用法:
783
832
  sillyspec task start --change <名> --task task-NN [--note "一句话在做什么"]
833
+ sillyspec task done --change <名> --task task-NN --verdict <pass|fail|cannot_verify> [--notes "评审备注"]
834
+ [--evidence "..."] [--commit -m "提交信息"] [-- <精确路径...>] [--force]
784
835
  sillyspec task finish --change <名> --task task-NN
785
836
  sillyspec task list --change <名>
786
- task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标红 >2h 中断遗留(接管审查点名用)`);
837
+ task done 四合一(r5l 方案1):review write(落 review.json+自动勾选)→ finish(清标记)→ 可选 wt-commit,
838
+ 子步幂等可断点重入;task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标红 >2h 中断遗留`);
787
839
  break;
788
840
  }
789
841
  const tChangeIdx = filteredArgs.indexOf('--change');
@@ -805,6 +857,76 @@ task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标
805
857
  task: tTask, change: tChange, note: tNote || '', startedAt: new Date().toISOString(),
806
858
  }, null, 2) + '\n');
807
859
  console.log(`📌 ${tTask} 已标记进行中${tNote ? '(' + tNote + ')' : ''}——完工时 sillyspec task finish --change ${tChange} --task ${tTask}`);
860
+ // ── 受影响测试族前移注入(2026-09-21 R5 接线 ql-20260921-007,②)──
861
+ // deps(auto) 同源计算(verify-postcheck discoverModuleDependentTests)从 quick 收尾门
862
+ // 前移到任务开工:30 秒定向定位替代 100 秒全量轰炸+混合归因(W1 实证全量当首验多跑
863
+ // 2 轮+triage 三轮)。按 task 卡 allowed_paths 声明面计算;best-effort:卡片缺失/
864
+ // 解析失败/零命中零输出零阻断。
865
+ try {
866
+ const tCardPath = join(tSpecBase, 'changes', tChange, 'tasks', `${tTask}.md`);
867
+ if (existsSync(tCardPath)) {
868
+ const { parseAllowedPaths } = await import('./stages/plan-postcheck.js');
869
+ const { discoverModuleDependentTests } = await import('./verify-postcheck.js');
870
+ const tAllowed = parseAllowedPaths(readFileSync(tCardPath, 'utf8'));
871
+ if (tAllowed.length > 0) {
872
+ const tDeps = discoverModuleDependentTests({ cwd: dir, changedFiles: tAllowed, coveredCommands: [] });
873
+ if (tDeps.length > 0) {
874
+ console.log(`🎯 受影响测试族(deps(auto) 同源 ${tDeps.length} 个)——写完先跑定向再全量(30s 定位 < 100s 全量+归因):`);
875
+ console.log(` node --test ${tDeps.slice(0, 10).join(' ')}${tDeps.length > 10 ? ` …等共 ${tDeps.length} 个` : ''}`);
876
+ }
877
+ }
878
+ }
879
+ } catch { /* 注入 best-effort */ }
880
+ } else if (taskSub === 'done') {
881
+ // ── task done 四合一(r5l-forensic-verdict 方案 1 / 评审护栏#3):13 任务 × 4 连收尾
882
+ // 往返(review write+勾选 / finish / wt-commit)合并为单进程串行 + 合并输出——每次省
883
+ // 一次 ~300K 全量上下文重发。原子性:子步幂等跳过 / 中断精确报告已完成子步 / 重入断点
884
+ // 续;ownership 与拒覆盖语义继承 writeTaskReview / runWtCommit 既有判定(编排零旁路)。──
885
+ const tdVal = (flag) => {
886
+ const i = filteredArgs.indexOf(flag);
887
+ return i >= 0 && filteredArgs[i + 1] && !String(filteredArgs[i + 1]).startsWith("--") ? filteredArgs[i + 1] : null;
888
+ };
889
+ const tdVerdict = tdVal('--verdict');
890
+ const tdNotes = tdVal('--notes');
891
+ const tdEvidence = tdVal('--evidence');
892
+ const tdCommit = filteredArgs.includes('--commit');
893
+ const tdMessage = tdVal('-m') || tdVal('--message');
894
+ const tdBase = tdVal('--base');
895
+ const tdHead = tdVal('--head');
896
+ const tdCfIdx = filteredArgs.indexOf('--changed-files');
897
+ // 存在即显式覆盖(含空串=纯验证任务零 diff 声明)——与 review write 同语义
898
+ const tdFilesOverride = tdCfIdx >= 0
899
+ ? (filteredArgs[tdCfIdx + 1] || '').split(',').map((x) => x.trim()).filter(Boolean)
900
+ : null;
901
+ const tdPsIdx = filteredArgs.indexOf('--pathspec-from-file');
902
+ const tdPsFile = tdPsIdx >= 0 && filteredArgs[tdPsIdx + 1] ? filteredArgs[tdPsIdx + 1] : null;
903
+ const tdDd = filteredArgs.indexOf('--');
904
+ const tdPaths = tdDd === -1 ? [] : filteredArgs.slice(tdDd + 1);
905
+ if (!tdVerdict || (tdCommit && !tdMessage)) {
906
+ console.error('用法: sillyspec task done --change <名> --task task-NN --verdict <pass|fail|cannot_verify> [--notes "评审备注"] [--evidence "cannot_verify 证据"]\n [--commit -m "提交信息"] [--pathspec-from-file <f>] [-- <精确路径...>] [--force]\n 四合一收尾:review write(落 review.json+自动勾选)→ task finish(清进行中标记)→ 可选 wt-commit;\n 子步幂等可断点重入;verdict 改判已存在 review 需 --force(与 review write 拒覆盖同款)');
907
+ process.exit(2);
908
+ }
909
+ if (tdCommit && tdPaths.length === 0 && !tdPsFile) {
910
+ console.error('❌ --commit 需要提交路径:`--` 后逐个给出本 task 的精确路径(或 --pathspec-from-file <f>)——wt-commit 刻意不支持空 pathspec(同 Wave 兄弟 WIP 防卷入)');
911
+ process.exit(2);
912
+ }
913
+ const tdPlatformOpts = {};
914
+ const tdResolved = resolvePlatformOpts(dir, specDir);
915
+ if (tdResolved) {
916
+ tdPlatformOpts.specRoot = tdResolved.specRoot;
917
+ if (tdResolved.runtimeRoot) tdPlatformOpts.runtimeRoot = tdResolved.runtimeRoot;
918
+ }
919
+ const { runTaskDone } = await import('./task-done.js');
920
+ const tdResult = await runTaskDone({
921
+ changeName: tChange, cwd: dir, taskId: tTask, verdict: tdVerdict,
922
+ notes: tdNotes || '', evidence: tdEvidence,
923
+ commitMessage: tdCommit ? tdMessage : null,
924
+ pathspecs: tdPaths, pathspecFile: tdPsFile,
925
+ markerDir: tDir, platformOpts: tdPlatformOpts,
926
+ baseOverride: tdBase, headOverride: tdHead, changedFilesOverride: tdFilesOverride,
927
+ force: filteredArgs.includes('--force'),
928
+ });
929
+ if (!tdResult.ok) process.exit(1);
808
930
  } else if (taskSub === 'finish') {
809
931
  try { unlinkSync(join(tDir, `${tTask}.json`)); console.log(`✅ ${tTask} 进行中标记已清除(完工)`); }
810
932
  catch { console.log(`ℹ️ ${tTask} 无进行中标记(已完工或从未 start)`); }
@@ -825,7 +947,7 @@ task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标
825
947
  }
826
948
  if (stale > 0) console.log(` 共 ${stale} 个疑似中断遗留——主代理接管时逐一审查(半成品无主的痛点出口)`);
827
949
  } else {
828
- console.error(`❌ 未知子命令「${taskSub}」(start | finish | list)`); process.exit(2);
950
+ console.error(`❌ 未知子命令「${taskSub}」(start | done | finish | list)`); process.exit(2);
829
951
  }
830
952
  break;
831
953
  }
@@ -1024,7 +1146,7 @@ task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标
1024
1146
  const { spawnSync } = await import('node:child_process');
1025
1147
  const { fileURLToPath } = await import('node:url');
1026
1148
  // ESM 无 __dirname(ql-20260911-029:此处曾直接引用,--apply 常态路径必 ReferenceError)
1027
- const r = spawnSync(process.execPath, [fileURLToPath(new URL('../bin/sillyspec.js', import.meta.url)), '--dir', dir, ...childArgs], { stdio: 'inherit' });
1149
+ const r = spawnSync(process.execPath, [fileURLToPath(new URL('../bin/sillyspec.js', import.meta.url)), '--dir', dir, ...childArgs], { stdio: 'inherit', windowsHide: true });
1028
1150
  process.exit(r.status === null ? 1 : r.status);
1029
1151
  }
1030
1152
  if (json) {
@@ -1138,8 +1260,14 @@ task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标
1138
1260
  const vpChange = vpChangeIdx >= 0 && args[vpChangeIdx + 1] && !String(args[vpChangeIdx + 1]).startsWith("--") ? args[vpChangeIdx + 1] : null;
1139
1261
  const vpInit = args.includes('--init');
1140
1262
  const vpForce = args.includes('--force');
1263
+ const vpDraft = args.includes('--draft');
1264
+ const vpAmendDraft = args.includes('--amend-draft');
1265
+ if (vpDraft && !vpInit) {
1266
+ console.error('❌ --draft 需与 --init 同用(draft 是骨架生成的机器起草模式)——sillyspec verify-probes --change <名> --init --draft');
1267
+ process.exit(2);
1268
+ }
1141
1269
  if (!vpChange) {
1142
- console.error('用法: sillyspec verify-probes --change <name> [--init [--force]] [--json] [--spec-dir <path>]\n 跑机械探针(TODO 标记/测试覆盖/API 对账/删除对账)输出 markdown;--init 生成 verify-result.md 骨架(探针预填,已存在不覆盖;--force 覆盖重生成全骨架——手填内容会重置,先备份)');
1270
+ console.error('用法: sillyspec verify-probes --change <name> [--init [--force]] [--draft] [--amend-draft] [--json] [--spec-dir <path>]\n 跑机械探针(TODO 标记/测试覆盖/API 契约/删除对账)输出 markdown;--init 生成 verify-result.md 骨架(探针预填,已存在不覆盖;--force 覆盖重生成全骨架——手填内容会重置,先备份);\n --init --draft 填槽模式(r5l 方案3):任务完成度/风险等级/测试结果/决策追踪四节机器预填完整句子+指纹防篡改,agent 只填三处 <!--AGENT:--> 槽(结论枚举/移交项/审查叙述);\n --amend-draft 留痕重锚:确要修改机器预填段时重算指纹并记 sidecar 审计(--done 篡改门禁的显式例外通道)');
1143
1271
  process.exit(2);
1144
1272
  }
1145
1273
  assertSafeChangeName(vpChange, '--change 变更名');
@@ -1166,6 +1294,24 @@ task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标
1166
1294
  } catch { return null }
1167
1295
  }
1168
1296
  const vpResult = runVerifyProbes({ cwd: dir, changeName: vpChange, specDir: vpSpecBase });
1297
+ // ── --amend-draft(r5l 方案3/护栏#2:机器段修改的唯一留痕通道)── 对 md 中现存
1298
+ // MACHINE-DRAFT 标记段按当前内容重锚哈希 + sidecar amendment 审计,不跑探针不写骨架。
1299
+ if (vpAmendDraft) {
1300
+ const { resolveRuntimeRoot } = await import('./run/shared.js');
1301
+ const { amendDraftMarkers } = await import('./verify-draft.js');
1302
+ const vpAmRoot = resolveRuntimeRoot({}, vpSpecBase);
1303
+ const vpAmKeys = amendDraftMarkers({
1304
+ mdPath: join(vpSpecBase, 'changes', vpChange, 'verify-result.md'),
1305
+ changeName: vpChange, runtimeRoot: vpAmRoot,
1306
+ });
1307
+ if (vpAmKeys.length > 0) {
1308
+ try { await mirrorInitArtifact(join(vpSpecBase, 'changes', vpChange, 'verify-result.md'), 'verify-result.md', readFileSync(join(vpSpecBase, 'changes', vpChange, 'verify-result.md'), 'utf8')) } catch { /* 镜像失败不阻断 */ }
1309
+ console.log(`✏️ amend-draft 已重锚 ${vpAmKeys.length} 个机器段:${vpAmKeys.join('、')}(sidecar 记 amendment 审计——verify --done 篡改门禁按新指纹放行)`);
1310
+ } else {
1311
+ console.log('ℹ️ 无 MACHINE-DRAFT 标记段可重锚(报告未用 --draft 生成,或标记已全部删除——后者会被 --done 拒收,先恢复标记或重跑 --init --force --draft)');
1312
+ }
1313
+ break;
1314
+ }
1169
1315
  if (json) {
1170
1316
  console.log(JSON.stringify({ command: 'verify-probes', change: vpChange, ...vpResult }, null, 2));
1171
1317
  break;
@@ -1204,6 +1350,68 @@ task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标
1204
1350
  await mirrorInitArtifact(vpReportPath, 'verify-result.md', vpSkeleton);
1205
1351
  console.log(`\n📄 已生成 verify-result.md 骨架: ${vpReportPath}(探针已预填;结论必须写明 PASS/FAIL,留待填会被 gate 判不过)${vpPlatformNote}`);
1206
1352
  }
1353
+ // ── --draft 填槽模式(r5l 方案3/护栏#2,v2 件二)── 四节机器预填完整句子 + 指纹防篡改。
1354
+ // 位置刻意在 backfill/ensure/inject 三 enricher 之前:决策追踪矩阵由本段先落(含标记),
1355
+ // injectDecisionChainDraft 随后见表格自然 no-op(幂等两不误)。材料与 verify 门对账同源:
1356
+ // 测试结果优先级 P2 账本 > 质量扫描记录(gates.js 同序);风险判级 evaluateConclusionDraft
1357
+ // 同款口径(detectChangeRisk 词表已退役 D-008)。幂等:段内非 TODO 占位(已手写/已
1358
+ // draft 化)零改动;平台模式 sidecar 随 vpSpecBase 锚定(门禁侧 not-applicable 即降级放行,
1359
+ // 不产生假红)。
1360
+ if (vpDraft && existsSync(vpReportPath)) {
1361
+ try {
1362
+ const { resolveRuntimeRoot } = await import('./run/shared.js');
1363
+ const { summarizeTaskCompletion } = await import('./task-review.js');
1364
+ const { buildDecisionChainMatrix } = await import('./verify-probes.js');
1365
+ const { parseFileChangeListDetailed } = await import('./change-list.js');
1366
+ const { loadBlastDeclarationsAllProjects } = await import('./blast-surface.js');
1367
+ const { resolveChangeRisk, extractExplicitRiskLevel } = await import('./change-risk-profile.js');
1368
+ const { consultTestLedger } = await import('./run/test-ledger.js');
1369
+ const { loadReusableQualityScan } = await import('./run/verify-quality-scan.js');
1370
+ const { buildDraftSections, applyDraftMode } = await import('./verify-draft.js');
1371
+ const vpChangeDir = join(vpSpecBase, 'changes', vpChange);
1372
+ const vpRuntimeRoot = resolveRuntimeRoot({}, vpSpecBase);
1373
+ // ① 任务完成度(summarizeTaskCompletion 同源:review verdict 真源 + 降级说明句)
1374
+ const vpSummarize = summarizeTaskCompletion({ changeDir: vpChangeDir, runtimeRoot: vpRuntimeRoot, changeName: vpChange });
1375
+ // ② 风险等级(evaluateConclusionDraft 同款输入面;无清单 → null 走降级句)
1376
+ let vpRisk = null;
1377
+ try {
1378
+ const vpDesignPath = join(vpChangeDir, 'design.md');
1379
+ const vpDesignContent = existsSync(vpDesignPath) ? readFileSync(vpDesignPath, 'utf8') : '';
1380
+ const vpDeclared = parseFileChangeListDetailed(vpDesignPath, { keepSillyspecDocs: true }).map((e) => e.path);
1381
+ if (vpDeclared.length > 0) {
1382
+ vpRisk = resolveChangeRisk({
1383
+ files: vpDeclared,
1384
+ blastDeclarations: loadBlastDeclarationsAllProjects({ specBase: vpSpecBase }),
1385
+ explicitRiskLevel: vpDesignContent ? extractExplicitRiskLevel(vpDesignContent) : null,
1386
+ });
1387
+ }
1388
+ } catch { /* 风险材料不可得 → 降级句(完整句子,不空段) */ }
1389
+ // ③ 测试结果(与 verify 门对账同源优先级:P2 账本 > 扫描记录 > 无记录提示句)
1390
+ let vpTest = null;
1391
+ try {
1392
+ const vpConsult = consultTestLedger({ runtimeRoot: vpRuntimeRoot, changeName: vpChange, projectRoot: dir, testRoot: join(dir, 'test'), specBase: vpSpecBase, cwd: dir });
1393
+ if (vpConsult.reuse) vpTest = { source: 'ledger', command: vpConsult.runPlan?.[0]?.runner || 'npm test', status: 'passed', ranAt: vpConsult.result.ranAt, durationMs: vpConsult.result.durationMs, total: vpConsult.result.total ?? null };
1394
+ } catch { /* 账本咨询异常 → 下一源 */ }
1395
+ if (!vpTest) {
1396
+ try {
1397
+ const vpScan = loadReusableQualityScan({ specBase: vpSpecBase, cwd: dir, changeName: vpChange });
1398
+ if (vpScan && vpScan.testResult) vpTest = { source: 'scan', command: vpScan.testResult.command, status: vpScan.testResult.status, ranAt: vpScan.ranAt, durationMs: vpScan.testResult.durationMs, total: vpScan.testResult.total ?? null };
1399
+ } catch { /* 扫描记录不可得 → 提示句 */ }
1400
+ }
1401
+ // ④ 决策追踪(buildDecisionChainMatrix 机械半边——Evidence/状态两列留人工)
1402
+ const vpChain = buildDecisionChainMatrix(vpChangeDir);
1403
+ const vpDraftRes = applyDraftMode({
1404
+ mdPath: vpReportPath, changeName: vpChange, runtimeRoot: vpRuntimeRoot,
1405
+ sections: buildDraftSections({ summarize: vpSummarize, risk: vpRisk, testResult: vpTest, decisionChain: vpChain }),
1406
+ });
1407
+ if (vpDraftRes) {
1408
+ try { await mirrorInitArtifact(vpReportPath, 'verify-result.md', readFileSync(vpReportPath, 'utf8')) } catch { /* 镜像失败不阻断 */ }
1409
+ console.log(`\n📝 填槽模式已应用:${vpDraftRes.applied.length} 节机器预填(${vpDraftRes.applied.join('、')},MACHINE-DRAFT 指纹防篡改——整段改写会被 verify --done 拒收);你只填三处 <!--AGENT:--> 槽(结论枚举/移交项/审查叙述)${vpPlatformNote}`);
1410
+ }
1411
+ } catch (e) {
1412
+ console.warn(`⚠️ --draft 机器起草降级(fail-soft,骨架保持原态): ${e && e.message ? e.message : e}`);
1413
+ }
1414
+ }
1207
1415
  const vpFacts = writeVerifyFacts(join(vpSpecBase, 'changes', vpChange), vpResult, vpChange, {
1208
1416
  platformNote: formatPlatformPathNote(vpPlatformBase, vpChange, 'verify-facts.json'),
1209
1417
  }); // P3b:facts 底稿随 --init 刷新(骨架已存在也刷新;v2 起分段合并保留固化段)
@@ -1270,6 +1478,15 @@ task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标
1270
1478
  }
1271
1479
  } catch { /* 草稿预填失败不阻断 --init */ }
1272
1480
  }
1481
+ // ── 产物落盘即推平台(2026-09-24 fr-test-readside 断流实证修复④):--init/--force
1482
+ // 写探针报告/骨架/facts 后不触发同步,verify 期步骤与文件迟到平台直到 --done——
1483
+ // 补 triggerSync 与 stage 渲染同拍(bg 异步,不阻塞命令返回)。──
1484
+ if (vpChange && (vpInit || vpForce)) {
1485
+ try {
1486
+ const { triggerSync } = await import('./run/shared.js')
1487
+ triggerSync(dir, vpChange, { specRoot: vpSpecBase })
1488
+ } catch { /* 同步触发失败不阻断探针命令 */ }
1489
+ }
1273
1490
  break;
1274
1491
  }
1275
1492
  case 'review-dispatch': {
@@ -2508,6 +2725,9 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
2508
2725
  break
2509
2726
  }
2510
2727
  const { runCommand } = await import('./run.js')
2728
+ // quick 通道退役预门(2026-09-25-quick-channel-retire):`run quick` 新会话形态在进
2729
+ // runCommand(及其前置副作用:sid 生成/owner.json/已建立公告)之前拒绝
2730
+ if (filteredArgs[1] === 'quick') refuseRetiredQuickFreshStart(filteredArgs.slice(1))
2511
2731
  // 平台模式(--spec-dir 已指定)时,--dir 是明确的 source_root,不应被 resolveEffectiveDir 纠正
2512
2732
  const effectiveDir = specDir ? dir : resolveEffectiveDir(dir)
2513
2733
  // 下行 pull:对齐顶层 stage 别名块(task-10 / D-009 / FR-04)。修复前 case 'run' 漏接,
@@ -2784,8 +3004,11 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
2784
3004
  case 'verify':
2785
3005
  case 'auto':
2786
3006
  case 'archive': {
2787
- const { runCommand } = await import('./run.js')
2788
3007
  const stageArgs = [command, ...filteredArgs.slice(1)]
3008
+ // quick 通道退役预门(2026-09-25-quick-channel-retire):顶层 quick 别名的新会话形态
3009
+ // 在进 runCommand(及其前置副作用)之前拒绝
3010
+ if (command === 'quick') refuseRetiredQuickFreshStart(stageArgs)
3011
+ const { runCommand } = await import('./run.js')
2789
3012
  const effectiveDir = specDir ? dir : resolveEffectiveDir(dir)
2790
3013
  // 下行 pull:stage 命令(run/--done,含 archive)启动时拉一次(task-10 / D-009 / FR-04)
2791
3014
  // 不在每步 pull,仅低频边界点;未连接平台静默跳过
@@ -2796,7 +3019,182 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
2796
3019
  }
2797
3020
  case 'knowledge': {
2798
3021
  const { cmdKnowledge } = await import('./stages/knowledge.js')
2799
- await cmdKnowledge(filteredArgs.slice(1), specDir ? dir : resolveEffectiveDir(dir), { specDir })
3022
+ await cmdKnowledge(filteredArgs.slice(1), specDir ? dir : resolveEffectiveDir(dir), { specDir, json })
3023
+ break
3024
+ }
3025
+ // ── sillyspec tests(2026-09-24-fr-test-bindings task-02,fr-test-binding §3.2/§5):
3026
+ // 绑定面视图 + 唯一合法修理工。锚可解析+路径存在硬校验——修理工不得制造悬空。──
3027
+ case 'tests': {
3028
+ const {
3029
+ queryByAnchor, queryByChange, upsertFrBindings, upsertQlBindings,
3030
+ unbindFrRows, unbindQlRows, anchorResolvable, normalizeRow, testAnchorFile,
3031
+ readFrBindings, upsertFrBindingsRaw, resolveTestFileRel,
3032
+ } = await import('./test-bindings.js')
3033
+ const effDir = specDir ? dir : resolveEffectiveDir(dir)
3034
+ const specBase = specDir || join(effDir, '.sillyspec')
3035
+ const knowledgeRoot = join(specBase, 'knowledge')
3036
+ // ── repair-paths(2026-09-26-dynamic-test-inference):存量绑定路径错形修复 ──
3037
+ // 三形态混杂(根相对 ✓ / 丢子项目前缀的 cwd 相对 / 裸文件名——平台仓实证)统一归一为
3038
+ // 仓根相对。缺省干跑预览,--write 落盘(upsertFrBindingsRaw 权威重写,路径级修复不触
3039
+ // 行语义)。逐条经硬校验(resolveTestFileRel 只认磁盘在场文件),修复工不得制造悬空。
3040
+ if (filteredArgs.slice(1).includes('repair-paths')) {
3041
+ const write = filteredArgs.includes('--write')
3042
+ const frDir = join(knowledgeRoot, 'fr')
3043
+ if (!existsSync(frDir)) { console.error(`❌ knowledge/fr 不存在(${frDir})——本仓无 FR 绑定面`); process.exitCode = 1; break }
3044
+ let plannedRows = 0, plannedPaths = 0, unresolvable = []
3045
+ const filePlan = []
3046
+ for (const f of readdirSync(frDir).filter(x => x.endsWith('.md')).sort()) {
3047
+ const p = join(frDir, f)
3048
+ const lines = readFileSync(p, 'utf8').split(/\r?\n/)
3049
+ const frIds = [...new Set(lines.filter(l => /^## (FR-[\w-]+-\d+)\s/.test(l)).map(l => l.match(/^## (FR-[\w-]+-\d+)/)[1]))]
3050
+ for (const frId of frIds) {
3051
+ const rows = readFrBindings({ knowledgeRoot, frId })
3052
+ let rowChanged = 0
3053
+ const fixedRows = rows.map(r => {
3054
+ const fixedTests = (r.tests || []).map(t => {
3055
+ const filePart = testAnchorFile(String(t))
3056
+ const anchorPart = String(t).slice(filePart.length)
3057
+ if (existsSync(resolve(effDir, filePart.replace(/\\/g, '/')))) return String(t) // 已是仓根相对
3058
+ const rel = resolveTestFileRel(filePart, { projectRoot: effDir })
3059
+ if (rel) { plannedPaths++; return rel + anchorPart }
3060
+ unresolvable.push(`${frId}:${t}`)
3061
+ return String(t)
3062
+ })
3063
+ if (JSON.stringify(fixedTests) !== JSON.stringify(r.tests || [])) { rowChanged++; return { ...r, tests: fixedTests } }
3064
+ return r
3065
+ })
3066
+ if (rowChanged > 0) { plannedRows += rowChanged; filePlan.push({ file: f, frId, rows: fixedRows }) }
3067
+ }
3068
+ }
3069
+ if (plannedPaths === 0) {
3070
+ console.log(`✅ 绑定路径无需修复(fr/*.md 全部条目 tests 均可自仓根解析${unresolvable.length ? `;${unresolvable.length} 个无法定位需人工核(--write 也只保留原值):${unresolvable.slice(0, 5).join('、')}${unresolvable.length > 5 ? ' 等' : ''}` : ''})`)
3071
+ break
3072
+ }
3073
+ console.log(`🔧 绑定路径修复${write ? '' : '预览(--write 落盘)'}:${plannedRows} 行 / ${plannedPaths} 个路径归一为仓根相对,涉及 ${filePlan.length} 个条目`)
3074
+ for (const fp of filePlan.slice(0, 10)) console.log(` ${fp.file} :: ${fp.frId}`)
3075
+ if (filePlan.length > 10) console.log(` … 共 ${filePlan.length} 个条目`)
3076
+ if (unresolvable.length > 0) console.warn(`⚠️ ${unresolvable.length} 个路径无法自仓根定位(保留原值,人工核):${unresolvable.slice(0, 5).join('、')}${unresolvable.length > 5 ? ' 等' : ''}`)
3077
+ if (!write) break
3078
+ for (const fp of filePlan) upsertFrBindingsRaw({ knowledgeRoot, frId: fp.frId, rows: fp.rows })
3079
+ console.log(`✅ 已写入 ${filePlan.length} 个条目的归一路径`)
3080
+ break
3081
+ }
3082
+ const flag = (name) => { const i = filteredArgs.indexOf(name); return i !== -1 && i + 1 < filteredArgs.length ? filteredArgs[i + 1] : null }
3083
+ const has = (name) => filteredArgs.includes(name)
3084
+ const anchor = flag('--anchor')
3085
+ const change = flag('--change')
3086
+ const rowId = flag('--row-id')
3087
+ const testsArg = flag('--tests')
3088
+ const reason = flag('--reason') || 'spec'
3089
+ const fail = (msg) => { console.error(`❌ ${msg}`); process.exitCode = 1 }
3090
+ // repair-paths/--redomain 自带用法门,不走 anchor/change 前置守卫
3091
+ if (!anchor && !change && !has('--repair-paths') && !filteredArgs.slice(1).includes('repair-paths') && !has('--redomain')) { fail('用法: sillyspec tests --anchor <FR-…|ql-…> | --change <名> [--bind|--unbind] [--tests <p1,p2>] [--row-id <id>] [--reason spec|capability|regression] | repair-paths | --redomain --from <域> --to <域>'); break }
3092
+ // 变更期局部锚(FR-NN 未铸全局)只读拒绝修改——修理工仅面向提升后行
3093
+ const isLocalAnchor = anchor && /^FR-\d+$/.test(anchor)
3094
+ if ((has('--bind') || has('--unbind')) && isLocalAnchor) { fail(`变更期局部锚「${anchor}」只读——修理工仅面向提升后的全局锚(FR-<域>-NNN / ql-…)`); break }
3095
+ if (has('--bind') || has('--unbind')) {
3096
+ if (!anchor) { fail('--bind/--unbind 须配 --anchor'); break }
3097
+ if (!anchorResolvable({ specBase, knowledgeRoot, anchor })) { fail(`锚不可解析「${anchor}」(FR=活库条目 / ql=QUICKLOG 在册)——修理工不得制造悬空锚`); break }
3098
+ }
3099
+ if (has('--bind')) {
3100
+ const tests = (testsArg || '').split(',').map(s => s.trim()).filter(Boolean)
3101
+ if (tests.length === 0) { fail('--bind 须配 --tests <p1,p2>(仓根相对路径)'); break }
3102
+ const missing = tests.filter(p => !existsSync(resolve(effDir, p)))
3103
+ if (missing.length > 0) { fail(`tests 路径不存在:${missing.join('、')}(相对 ${effDir})——修理工不得制造悬空路径`); break }
3104
+ const head = (() => { try { return gitQuiet(effDir, ['rev-parse', 'HEAD']) || null } catch { return null } })()
3105
+ const row = { anchor, row_id: rowId || `manual:${Date.now().toString(36)}:${tests[0]}`, tests, reason, state: 'active', discovery: 'agent', confirmed_by: 'agent', confirmed_at: head, source_change: change || 'manual' }
3106
+ try {
3107
+ normalizeRow(row)
3108
+ if (/^FR-/.test(anchor)) upsertFrBindings({ knowledgeRoot, frId: anchor, rows: [row], projectRoot: effDir })
3109
+ else upsertQlBindings({ specBase, qlId: anchor, rows: [row] })
3110
+ console.log(`✅ 绑定写入:${anchor} ← ${tests.join(', ')}(confirmed_by=agent${head ? ` @${head.slice(0, 8)}` : ''})`)
3111
+ } catch (e) { fail(`绑定写入被拒:${e && e.message ? e.message : e}`) }
3112
+ break
3113
+ }
3114
+ if (has('--unbind')) {
3115
+ const ids = rowId ? [rowId] : (testsArg || '').split(',').map(s => s.trim()).filter(Boolean)
3116
+ if (ids.length === 0) { fail('--unbind 须配 --row-id <id> 或 --tests <p1,…>'); break }
3117
+ if (/^FR-/.test(anchor)) {
3118
+ const cur = queryByAnchor({ specBase, knowledgeRoot, anchor })
3119
+ // tests 条目可携带用例锚(2026-09-26-binding-anchor-fidelity)——给路径也能命中带锚行
3120
+ const del = cur.filter(r => ids.includes(r.row_id) || (testsArg && ids.length === 1 && r.tests.some(t => ids.includes(t) || ids.includes(testAnchorFile(t))))).map(r => r.row_id)
3121
+ if (del.length === 0) { fail(`未命中可解绑行(anchor=${anchor},给=${ids.join(',')})`); break }
3122
+ unbindFrRows({ knowledgeRoot, frId: anchor, rowIds: del })
3123
+ console.log(`✅ 解绑 ${del.length} 行(${anchor})`)
3124
+ } else {
3125
+ const cur = queryByAnchor({ specBase, knowledgeRoot, anchor })
3126
+ const del = cur.filter(r => ids.includes(r.row_id)).map(r => r.row_id)
3127
+ if (del.length === 0) { fail(`未命中可解绑行(anchor=${anchor})`); break }
3128
+ unbindQlRows({ specBase, qlId: anchor, rowIds: del })
3129
+ console.log(`✅ 解绑 ${del.length} 行(${anchor})`)
3130
+ }
3131
+ break
3132
+ }
3133
+ // ── confirm(2026-09-27-confirm-on-use 三层治理①层):消费时确认翻牌 ──
3134
+ // candidate 机器行(confirmed_by=null)批量翻 active(confirmed_by=agent)——抽查式
3135
+ // 确认的写回通道。机械防橡皮图章:--evidence 必须是可自仓根解析的真实测试路径
3136
+ // (resolveTestFileRel 单源),解析失败拒绝 exit 1。
3137
+ if (has('--confirm')) {
3138
+ if (!anchor || !/^FR-/.test(anchor)) { fail('用法: sillyspec tests --confirm --anchor FR-<域>-NNN --evidence <真实测试路径>'); break }
3139
+ const evidenceRaw = flag('--evidence')
3140
+ if (!evidenceRaw) { fail('--confirm 须配 --evidence <真实测试路径>(防橡皮图章:证据必须是盘上文件)'); break }
3141
+ const { resolveTestFileRel: _rtf2 } = await import('./test-bindings.js')
3142
+ const evRel = _rtf2(testAnchorFile(evidenceRaw), { projectRoot: effDir }) || (existsSync(resolve(effDir, evidenceRaw.replace(/\\/g, '/'))) ? evidenceRaw.replace(/\\/g, '/') : null)
3143
+ // 评审 P2-1 清偿:证据须为测试形态文件(*.test.* / *.spec.* / test_*.py / *_test.*)——
3144
+ // resolveTestFileRel 直取分支无形态检查(package.json/win.ini 实证可绕),此处收口
3145
+ const evBase = evRel ? evRel.split('/').pop() : ''
3146
+ const EV_TEST_SHAPE_RE = new RegExp('\\.(test|spec)\\.[cm]?[jt]sx?$')
3147
+ const evShaped = !!evRel && (EV_TEST_SHAPE_RE.test(evBase) || /^test_[\w.]+\.py$/.test(evBase) || /_test\.(go|py)$/.test(evBase))
3148
+ if (!evShaped) { fail(`证据必须是盘上真实测试文件(*.test.* / test_*.py 等形态):${evidenceRaw}——不收口头相符,不收非测试文件`); break }
3149
+ const rows = readFrBindings({ knowledgeRoot, frId: anchor })
3150
+ if (rows.length === 0) { fail(`${anchor} 无绑定行——确认对象不存在(先 --bind 或经归档提升)`); break }
3151
+ const head2 = (() => { try { return gitQuiet(effDir, ['rev-parse', 'HEAD']) || null } catch { return null } })()
3152
+ const confirmed = rows.map((r) => r.confirmed_by === 'agent' ? r : { ...r, state: 'active', confirmed_by: 'agent', confirmed_at: head2 })
3153
+ const flipped = confirmed.filter((r, i) => rows[i].confirmed_by !== 'agent').length
3154
+ if (flipped === 0) { console.log(`ℹ️ ${anchor} 全部绑定行已 active(幂等,无需确认)`); break }
3155
+ upsertFrBindingsRaw({ knowledgeRoot, frId: anchor, rows: confirmed, projectRoot: effDir })
3156
+ console.log(`✅ 确认翻牌:${anchor} ${flipped}/${rows.length} 行 candidate→active(confirmed_by=agent${head2 ? ` @${head2.slice(0, 8)}` : ''};证据=${evRel})`)
3157
+ break
3158
+ }
3159
+ // ── redomain(2026-09-27-redomain,三层治理 v2 ③):伪域条目机器迁移 ──
3160
+ // --from <域> --to <域> [--anchor <FR-id> 可重复];干跑缺省列条目,--write 落盘。
3161
+ // 身份铁律:迁域不换号(绑定/supersede 链靠 ID 寻址);平台伪域卡「一键迁移」的 CLI 端。
3162
+ if (has('--redomain')) {
3163
+ const fromD = flag('--from')
3164
+ const toD = flag('--to')
3165
+ if (!fromD || !toD) { fail('用法: sillyspec tests --redomain --from <源域> --to <目标域> [--anchor <FR-id>] [--write]'); break }
3166
+ // 多 --anchor 收集
3167
+ const anchorList = []
3168
+ for (let i = 0; i < filteredArgs.length - 1; i++) if (filteredArgs[i] === '--anchor') anchorList.push(filteredArgs[i + 1])
3169
+ try {
3170
+ const { planRedomain, redomainFrEntries } = await import('./redomain.js')
3171
+ if (!has('--write')) {
3172
+ const plan = planRedomain({ knowledgeRoot, from: fromD, to: toD, anchors: anchorList.length ? anchorList : null })
3173
+ console.log(`🔧 域迁移预览(--write 落盘):fr/${fromD}.md → fr/${toD}.md,${plan.entries.length} 条`)
3174
+ for (const e of plan.entries.slice(0, 10)) console.log(` ${e.id} ${e.title}`)
3175
+ if (plan.entries.length > 10) console.log(` … 共 ${plan.entries.length} 条`)
3176
+ console.log(` 目标域文件${plan.targetExists ? '在场' : '将新建'};源域${plan.sourceWillDelete ? '将删空壳' : '保留余条'};ID 不变(身份保持)`)
3177
+ break
3178
+ }
3179
+ const r = redomainFrEntries({ knowledgeRoot, from: fromD, to: toD, anchors: anchorList.length ? anchorList : null })
3180
+ console.log(`✅ 域迁移完成:${r.moved.length} 条 fr/${fromD}.md → fr/${toD}.md(ID 不变)`)
3181
+ for (const m of r.moved.slice(0, 10)) console.log(` ${m.id} ${m.title}`)
3182
+ if (r.sourceDeleted) console.log(` 源域空壳已删:fr/${fromD}.md`)
3183
+ if (r.targetCreated) console.log(` 目标域文件新建:fr/${toD}.md`)
3184
+ if (r.indexLineAdded) console.log(` INDEX 路由行已补:${toD}`)
3185
+ } catch (e) { fail(`域迁移被拒:${(e && e.message) || e}`) }
3186
+ break
3187
+ }
3188
+ // 视图
3189
+ const rows = anchor
3190
+ ? queryByAnchor({ specBase, knowledgeRoot, anchor })
3191
+ : queryByChange({ specBase, knowledgeRoot, sourceChange: change })
3192
+ if (rows.length === 0) { console.log(`(无绑定行:${anchor || change})`); break }
3193
+ console.log(`${anchor || change} — ${rows.length} 行`)
3194
+ for (const r of rows) {
3195
+ console.log(` ${r.state === 'active' ? '🟢' : '⚪'} ${r.tests.join(', ')}|reason=${r.reason}|discovery=${r.discovery}|confirmed_by=${r.confirmed_by || '-'}${r.confirmed_at ? '@' + String(r.confirmed_at).slice(0, 8) : ''}|status=${r.status}`)
3196
+ console.log(` row=${r.row_id}|source=${r.source_change || '-'}`)
3197
+ }
2800
3198
  break
2801
3199
  }
2802
3200
  case 'dashboard': {
@@ -4153,6 +4551,12 @@ checkbox 行;depends_on 自动反填行内注解 "(depends_on: task-01,02)";
4153
4551
  process.exitCode = r.ok ? 0 : 1;
4154
4552
  break;
4155
4553
  }
4554
+ case 'flow': {
4555
+ // R7 切片二:2-调用轻量协议命令族(start/done/amend-draft)——协议必需交互=2
4556
+ const flowMod = await import('./flow.js');
4557
+ await flowMod.cmdFlow(filteredArgs.slice(1), dir, specDir);
4558
+ break;
4559
+ }
4156
4560
  case 'workflow': {
4157
4561
  const wfSub = filteredArgs[1];
4158
4562
  if (!wfSub || wfSub === 'help' || wfSub === '--help') {
@@ -4663,8 +5067,158 @@ SillySpec pull — 拉取服务器 spec 快照到本地(X2 / FR-07)
4663
5067
  }
4664
5068
  break;
4665
5069
  }
5070
+ case 'watcher': {
5071
+ // 哨兵告警可视化出口(2026-09-23 quick / 1a91587c 后续件):watcher 落的
5072
+ // watcher-events-<change>.jsonl 是事件唯一真相源,此前只能人肉 grep。纯只读查询 +
5073
+ // 可选 --follow 轮询;不改 applySentinelRules、不做平台推送、不加新事件类型。
5074
+ // 子命令面:alerts;预留名(勿占):events(原始流格式化)/ tail(时间窗过滤)。
5075
+ const watcherSub = filteredArgs[1];
5076
+ if (watcherSub !== 'alerts' && watcherSub !== 'timeline') {
5077
+ const sug = didYouMean(watcherSub || '', ['alerts', 'timeline']);
5078
+ console.error(`❌ 未知子命令: watcher ${watcherSub || '(空)'}`);
5079
+ if (sug) console.error(` 你是想输入「watcher ${sug}」吗?`);
5080
+ console.error('用法: sillyspec watcher alerts --change <名> [--all] [--follow] [--runtime-root <路径>]');
5081
+ console.error(' sillyspec watcher timeline --change <名> [--runtime-root <路径>]');
5082
+ process.exit(2);
5083
+ }
5084
+ // ── timeline 子命令(2026-09-26-watcher-timeline):变更合成时间线只读出口 ──
5085
+ // 三源 join(事件流 × tasks.md × git 提交锚)在 src/timeline.js 纯函数层;此处只做
5086
+ // I/O 接线(runtimeRoot 解析与 alerts 同源;change 目录活跃>归档双路径探测)。
5087
+ if (watcherSub === 'timeline') {
5088
+ const tRest = filteredArgs.slice(2);
5089
+ let tChange = null;
5090
+ let tRuntimeRootFlag = null;
5091
+ for (let i = 0; i < tRest.length; i++) {
5092
+ if (tRest[i] === '--change' && tRest[i + 1]) tChange = tRest[++i];
5093
+ else if (tRest[i] === '--runtime-root' && tRest[i + 1]) tRuntimeRootFlag = resolve(tRest[++i]);
5094
+ }
5095
+ if (!tChange) {
5096
+ console.error('❌ watcher timeline 需要 --change <变更名>');
5097
+ console.error('用法: sillyspec watcher timeline --change <名> [--runtime-root <路径>]');
5098
+ process.exit(2);
5099
+ }
5100
+ assertSafeChangeName(tChange, '变更名');
5101
+ const { resolveRuntimeRoot } = await import('./run/shared.js');
5102
+ const { readWatcherEvents } = await import('./watcher.js');
5103
+ const { loadChangeTasks, readBirthTs, resolveCommitAnchors, parseTaskLines, renderTimeline } = await import('./timeline.js');
5104
+ const tSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
5105
+ const tPlatformOpts = resolvePlatformOpts(dir, specDir);
5106
+ const tRuntimeRoot = resolveRuntimeRoot({ runtimeRoot: tRuntimeRootFlag || (tPlatformOpts && tPlatformOpts.runtimeRoot) || null }, tSpecBase);
5107
+ let tRes = readWatcherEvents({ runtimeRoot: tRuntimeRoot, change: tChange });
5108
+ let tSourceNote = '';
5109
+ if (!tRes.exists) {
5110
+ // 归档包烤制副本回退(2026-09-28-archive-timeline-bake):本机 .runtime 无事件流(跨机
5111
+ // clone / 机本位清理)时读归档包内 watcher-events.jsonl 副本——时间线不随 .runtime 存活消失;
5112
+ // 副本也缺失时保持既有 exit 2 提示语义。
5113
+ const tBakedPath = join(tSpecBase, 'changes', 'archive', tChange, 'watcher-events.jsonl');
5114
+ const tBaked = readWatcherEvents({ path: tBakedPath });
5115
+ if (tBaked.exists) {
5116
+ tRes = tBaked;
5117
+ tSourceNote = `(事件源:归档包烤制快照 ${tBakedPath}——本机 .runtime 无事件流)`;
5118
+ }
5119
+ }
5120
+ if (!tRes.exists) {
5121
+ console.error(`📭 ${tChange} 无事件流(未跑过 watcher),无从合成时间线`);
5122
+ console.error(` 事件流路径:${join(tRuntimeRoot, `watcher-events-${tChange}.jsonl`)};活跃变更跑任意 flow/run 命令会拉起 watcher`);
5123
+ process.exit(2);
5124
+ }
5125
+ const { changeDir, tasksMd } = loadChangeTasks(tSpecBase, tChange);
5126
+ const tasks = tasksMd ? parseTaskLines(tasksMd) : null;
5127
+ let tier = null;
5128
+ if (changeDir) {
5129
+ try {
5130
+ const m = /^tier:\s*(\S+)/m.exec(readFileSync(join(changeDir, 'flow-state.yaml'), 'utf8'));
5131
+ if (m) tier = m[1];
5132
+ } catch { /* 缺 flow-state(头脑风暴预段等)按未知 */ }
5133
+ }
5134
+ const { gitQuiet } = await import('./git-helper.js');
5135
+ const anchors = resolveCommitAnchors(tRes.events, (hash) => {
5136
+ const out = gitQuiet(dir, ['log', '-1', '--format=%h%x1f%aI%x1f%B', hash]);
5137
+ if (typeof out !== 'string' || !out.trim()) return null;
5138
+ const sep = out.indexOf('\x1f');
5139
+ const rest = out.slice(sep + 1);
5140
+ const sep2 = rest.indexOf('\x1f');
5141
+ return { hash: out.slice(0, sep), dateISO: rest.slice(0, sep2), message: rest.slice(sep2 + 1) };
5142
+ });
5143
+ console.log(renderTimeline({ change: tChange, events: tRes.events, tasks, anchors, birthTs: readBirthTs(changeDir), tier }));
5144
+ if (tSourceNote) console.log(tSourceNote);
5145
+ break;
5146
+ }
5147
+ const wRest = filteredArgs.slice(2);
5148
+ let wChange = null;
5149
+ let wRuntimeRootFlag = null;
5150
+ let wAll = false;
5151
+ let wFollow = false;
5152
+ for (let i = 0; i < wRest.length; i++) {
5153
+ if (wRest[i] === '--change' && wRest[i + 1]) wChange = wRest[++i];
5154
+ else if (wRest[i] === '--runtime-root' && wRest[i + 1]) wRuntimeRootFlag = resolve(wRest[++i]);
5155
+ else if (wRest[i] === '--all') wAll = true;
5156
+ else if (wRest[i] === '--follow') wFollow = true;
5157
+ }
5158
+ if (!wChange) {
5159
+ console.error('❌ watcher alerts 需要 --change <变更名>(事件流按变更名分文件)');
5160
+ console.error('用法: sillyspec watcher alerts --change <名> [--all] [--follow] [--runtime-root <路径>]');
5161
+ process.exit(2);
5162
+ }
5163
+ assertSafeChangeName(wChange, '变更名');
5164
+ const { resolveRuntimeRoot } = await import('./run/shared.js');
5165
+ const { readWatcherEvents, watcherEventsPath } = await import('./watcher.js');
5166
+ // runtimeRoot 解析与 watcher.js spawnWatcher 同源:--runtime-root flag > 平台指针
5167
+ // runtimeRoot > specBase/.runtime(resolveRuntimeRoot 单点收敛)。
5168
+ const wSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
5169
+ const wPlatformOpts = resolvePlatformOpts(dir, specDir);
5170
+ const wRuntimeRoot = resolveRuntimeRoot({ runtimeRoot: wRuntimeRootFlag || (wPlatformOpts && wPlatformOpts.runtimeRoot) || null }, wSpecBase);
5171
+
5172
+ const ALERTS_POLL_MS = 2000;
5173
+ const fmtLocal = (ts) => {
5174
+ const d = new Date(Number(ts));
5175
+ if (Number.isNaN(d.getTime())) return String(ts);
5176
+ const p = (n) => String(n).padStart(2, '0');
5177
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
5178
+ };
5179
+ // detail 截 80:超长取前 79 列 + 省略号恰好 80 列
5180
+ const clip80 = (s) => {
5181
+ const t = String(s ?? '');
5182
+ return t.length > 80 ? t.slice(0, 79) + '…' : t;
5183
+ };
5184
+ const fmtRow = (e) => `${fmtLocal(e.ts)} | ${String(e.rule ?? e.kind ?? '').padEnd(12)} | ${clip80(e.detail)}`;
5185
+ const fmtStats = (warnN, evN, badN) => `📊 ${warnN} 条告警 / ${evN} 条事件${badN > 0 ? `(跳过 ${badN} 坏行)` : ''}`;
5186
+
5187
+ let wRes = readWatcherEvents({ runtimeRoot: wRuntimeRoot, change: wChange });
5188
+ if (!wRes.exists) {
5189
+ console.log(`📭 ${wChange} 无事件流(未跑过 watcher): ${watcherEventsPath(wRuntimeRoot, wChange)}`);
5190
+ break;
5191
+ }
5192
+ console.log(`🕒 watcher alerts — ${wChange}(${wRuntimeRoot})`);
5193
+ console.log(`${'本地时间'.padEnd(19)} | ${wAll ? 'rule/kind' : 'rule'.padEnd(12)} | detail`);
5194
+ const shown = wAll ? wRes.events : wRes.warnings;
5195
+ for (const e of shown) console.log(fmtRow(e));
5196
+ console.log(fmtStats(wRes.warnings.length, wRes.events.length, wRes.badLines));
5197
+ if (!wFollow) break;
5198
+
5199
+ // --follow:2s 轮询增量打印(jsonl 单写者 append-only,按 events 序号单调锚定已打印
5200
+ // 水位;坏行不产事件不干扰锚——残行下一轮解析成功后自然补打)。Ctrl-C 打印末次统计退出。
5201
+ console.log('\n⏳ 持续监听中(2s 轮询,Ctrl-C 退出)');
5202
+ const printNew = (from) => {
5203
+ const rows = (wAll ? wRes.events : wRes.warnings).slice(from);
5204
+ for (const e of rows) console.log(fmtRow(e));
5205
+ };
5206
+ let printed = shown.length;
5207
+ process.on('SIGINT', () => {
5208
+ console.log(`\n${fmtStats(wRes.warnings.length, wRes.events.length, wRes.badLines)}(已停止监听)`);
5209
+ process.exit(0);
5210
+ });
5211
+ // eslint-disable-next-line no-constant-condition
5212
+ while (true) {
5213
+ await new Promise((r) => setTimeout(r, ALERTS_POLL_MS));
5214
+ wRes = readWatcherEvents({ runtimeRoot: wRuntimeRoot, change: wChange });
5215
+ if (!wRes.exists) continue; // 文件中途被删:继续等(watcher 重启会重建)
5216
+ printNew(printed);
5217
+ printed = wAll ? wRes.events.length : wRes.warnings.length;
5218
+ }
5219
+ }
4666
5220
  default: {
4667
- const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'dispatch', 'agent-log', 'local', 'workflow', 'gate', 'derive', 'backfill-reviews', 'register-stage-review', 'modules', 'change-rename', 'change-delete', 'knowledge', 'platform', 'pull', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime'];
5221
+ const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'dispatch', 'agent-log', 'local', 'workflow', 'gate', 'derive', 'backfill-reviews', 'register-stage-review', 'modules', 'change-rename', 'change-delete', 'knowledge', 'platform', 'pull', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime', 'watcher'];
4668
5222
  const suggestion = didYouMean(command, topCommands);
4669
5223
  console.error(`❌ 未知命令: ${command}`);
4670
5224
  if (command === '--status') {