sillyspec 3.28.3 → 3.28.5

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 (77) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +54 -54
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  3. package/.claude/skills/sillyspec-continue/SKILL.md +17 -17
  4. package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
  5. package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -40
  6. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  7. package/.claude/skills/sillyspec-propose/SKILL.md +16 -16
  8. package/.claude/skills/sillyspec-resume/SKILL.md +21 -21
  9. package/.claude/skills/sillyspec-state/SKILL.md +25 -25
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +33 -33
  11. package/AGENTS.md +25 -0
  12. package/INSTRUCTIONS.md +12 -0
  13. package/package.json +1 -1
  14. package/results-review-tier.md +18 -0
  15. package/src/agent-session-log.js +4 -3
  16. package/src/archive-delta.js +82 -0
  17. package/src/change-delete.js +2 -1
  18. package/src/change-list.js +18 -3
  19. package/src/change-risk-profile.js +77 -6
  20. package/src/commit-suggest.js +52 -1
  21. package/src/config-cat.js +17 -1
  22. package/src/constants.js +3 -1
  23. package/src/decision-distill.js +45 -5
  24. package/src/decisions-io.js +96 -0
  25. package/src/design-facts.js +3 -0
  26. package/src/dispatch/probe.js +31 -8
  27. package/src/docs-check.js +102 -3
  28. package/src/doctor-diagnostics.js +441 -5
  29. package/src/foreign-declared.js +53 -2
  30. package/src/index.js +606 -22
  31. package/src/mcp-server.js +144 -0
  32. package/src/module-impact.js +84 -12
  33. package/src/modules.js +46 -34
  34. package/src/plan-adopt-waves.js +13 -5
  35. package/src/progress/change-registry.js +34 -0
  36. package/src/progress.js +75 -2
  37. package/src/quick-recommend.js +6 -1
  38. package/src/quicklog.js +70 -8
  39. package/src/review-dispatch.js +377 -0
  40. package/src/run/archive-distill.js +48 -0
  41. package/src/run/command.js +48 -13
  42. package/src/run/complete-handlers.js +215 -9
  43. package/src/run/complete.js +184 -2
  44. package/src/run/gate-snapshot.js +88 -0
  45. package/src/run/gates.js +206 -58
  46. package/src/run/prompt.js +150 -14
  47. package/src/run/quick-audit.js +38 -13
  48. package/src/run/shared.js +183 -12
  49. package/src/run/stage.js +52 -2
  50. package/src/run/verify-quality-scan.js +336 -0
  51. package/src/runtime-hygiene.js +84 -0
  52. package/src/scope-audit.js +735 -0
  53. package/src/sillyhub-mcp/client.js +58 -3
  54. package/src/spec-sync.js +63 -8
  55. package/src/stage-contract.js +83 -3
  56. package/src/stage-review-checklist.js +44 -0
  57. package/src/stage-review.js +125 -4
  58. package/src/stages/archive.js +60 -167
  59. package/src/stages/brainstorm.js +7 -17
  60. package/src/stages/doctor.js +41 -515
  61. package/src/stages/execute.js +13 -10
  62. package/src/stages/plan-postcheck.js +191 -7
  63. package/src/stages/plan.js +7 -35
  64. package/src/stages/quick.js +11 -11
  65. package/src/stages/verify.js +24 -39
  66. package/src/sync-noise.js +158 -0
  67. package/src/sync.js +93 -27
  68. package/src/task-review.js +133 -17
  69. package/src/taskcard.js +46 -3
  70. package/src/validate-artifacts.js +163 -0
  71. package/src/verify-facts-schema.js +141 -0
  72. package/src/verify-postcheck.js +347 -29
  73. package/src/verify-probes.js +275 -24
  74. package/src/workflow.js +6 -0
  75. package/src/worktree-apply.js +167 -24
  76. package/src/worktree.js +5 -1
  77. package/templates/agents-instruction.md +1 -0
package/src/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs';
10
10
  import { writeAtomicSync } from './fs-atomic.js';
11
- import { basename, dirname, extname, join, resolve, isAbsolute, sep } from 'path';
11
+ import { basename, dirname, extname, join, resolve, isAbsolute, sep, relative } from 'path';
12
12
  import { safeGit, git } from './git-helper.js';
13
13
  import { getVersion } from './version.js';
14
14
 
@@ -98,11 +98,14 @@ SillySpec CLI — 规范驱动开发工具包
98
98
  单 task review.json 命令式写入:verdict/notes 你给,base/head/changedFiles/diffPaths CLI 代算
99
99
  sillyspec symbol-impact --change <name> 生成 symbol-impact.md 逐 task <!--TODO--> 骨架(gate 拒绝未替换占位,防骨架直接过门)
100
100
  sillyspec design-init --change <name> [--force] 从 decisions.md 生成 design.md 十三章节骨架(决策追踪表预填;已存在不覆盖)
101
+ sillyspec fourpiece-init --change <name> 生成 proposal/requirements/decisions 骨架(frontmatter+章节+模板占位;已存在不覆盖)
101
102
  sillyspec delta --change <name> [--json] 生成变更 delta.md(Before/Delta/After 三段式聚合;幂等覆盖重跑即刷新)
102
103
  sillyspec next 项目状态探测:输出当前状态 + 下一步命令 + 依据(吸收 continue/resume 手工探测表)
103
104
  sillyspec commit [--json] 智能提交建议:收集 QUICKLOG/已勾 task/阶段产出语义,生成建议 message(只建议不执行)
104
105
  sillyspec verify-probes --change <name> [--init] verify 机械探针(TODO 标记/测试覆盖/API 对账/删除对账);--init 生成 verify-result.md 骨架
105
106
  sillyspec module-impact --change <name> 生成 module-impact.md 骨架(文件×模块归属按 module-map 预填 + 未匹配清单)
107
+ sillyspec scope-audit --change <name> [--json] 变更范围对账:计划×实际三态全表 + 行数(✓ 计划内/⚠️ 计划外/⚠️ 计划未动;quick 会话传 quick-<id> 出归属表;已归档变更可查——快照记录态;advisory 只读不设门禁)
108
+ sillyspec module-docs-sync --change <name> [--note ...] diff 归属模块 → sidecar 追加变更索引行 + 卡 updated_at 戳(幂等)
106
109
  sillyspec endpoints extract --change <name> [--task task-NN | --all-tasks] [--dir <dir>|--files <a.py,b.js>] 静态扫描路由装饰器生成 endpoints.json
107
110
  sillyspec endpoints baseline --change <name> [--spec-dir <path>] [--json] 拍变更前端点基线(幂等不覆盖;worktree 内跑自动锚主仓;归档 delta 端点增删 before 侧)
108
111
  sillyspec scan-fix-headers [--project <名>] scan 文档补 author/created_at header(幂等)
@@ -117,6 +120,7 @@ SillySpec CLI — 规范驱动开发工具包
117
120
  (无子命令) 跑诊断,列出问题
118
121
  --align-execute-progress [--change <名>] [--confirm] 基于 plan.md 对齐 execute 派生戳(默认 dry-run)
119
122
  --cleanup-remnant [--confirm] 删除 0 字节空占位 db(默认 dry-run,仅 --confirm 落盘)
123
+ --gc-unstamped-runs [--confirm] 回收已归档变更留下的无 change 戳 execute-runs(默认 dry-run)
120
124
  --dump-db --path <db 路径> dump 指定 db 内容到文件(取证用)
121
125
  --json 结构化诊断 + 落盘 .sillyspec/.runtime/doctor-diagnosis.json
122
126
  sillyspec modules <rebuild | status | migrate>
@@ -625,6 +629,13 @@ async function main() {
625
629
  } else if (rwResult.ok) {
626
630
  console.log(`✅ review.json 已写入:${rwResult.reviewPath}`);
627
631
  console.log(` executeRunId: ${rwResult.executeRunId}`);
632
+ // P2-f(noai-ir-roadmap §5)task 真源归一:review 落盘即由 CLI 渲染 checkbox——
633
+ // agent 永不手勾(review.json verdict 是唯一真源,tasks.md 勾选是它的显示态)。
634
+ try {
635
+ const { autoCheckPlanFromReviews } = await import('./run/complete.js');
636
+ const ac = await autoCheckPlanFromReviews({ stageName: 'execute', changeName: rwChange, cwd: dir, platformOpts: rwPlatformOpts });
637
+ if (ac.autoChecked && ac.checkedCount > 0) console.log(` ✅ tasks.md checkbox 已按 review verdict 自动勾选(${ac.checkedCount} 个;task 真源 = review.json,CLI 唯一勾选者——勿手动勾选)`);
638
+ } catch { /* 勾选 fail-soft:--done 时 autoCheck 兜底 */ }
628
639
  for (const w of rwResult.warnings) console.log(`⚠️ ${w}`);
629
640
  } else {
630
641
  console.error('❌ review write 失败:');
@@ -814,8 +825,22 @@ async function main() {
814
825
  // 项目状态探测(2026-08-21 审计第二批 G7/G8):吸收 continue/resume 两个 skill 的
815
826
  // 手工探测表(文件存在性状态机),一条命令输出「状态 + 下一步命令 + 依据」。
816
827
  // 纯 fs 零 token;活跃进度的权威状态仍归 progress show,本命令管无活跃进度时反推。
828
+ // --apply(P2,noai-ir-roadmap §5):建议是本 CLI 命令形态时直接代跑(恢复链 noAI——
829
+ // 只建议不执行让 agent 当转述员);非 CLI 命令形态的建议(自由文本)不代跑。
817
830
  const { detectNextStep } = await import('./run/next.js');
818
831
  const nxResult = detectNextStep({ cwd: dir, specDir });
832
+ if (args.includes('--apply')) {
833
+ const m = String(nxResult.next || '').match(/^sillyspec\s+(.+)$/);
834
+ if (!m) {
835
+ console.error(`❌ next --apply:建议不是可代跑的 CLI 命令形态(实得「${nxResult.next}」)——按建议手动执行`);
836
+ process.exit(2);
837
+ }
838
+ const childArgs = m[1].trim().split(/\s+/);
839
+ console.log(`🤖 next --apply 代跑:sillyspec ${childArgs.join(' ')}\n`);
840
+ const { spawnSync } = await import('node:child_process');
841
+ const r = spawnSync(process.execPath, [join(__dirname, '..', 'bin', 'sillyspec.js'), '--dir', dir, ...childArgs], { stdio: 'inherit' });
842
+ process.exit(r.status === null ? 1 : r.status);
843
+ }
819
844
  if (json) {
820
845
  console.log(JSON.stringify({ command: 'next', ...nxResult }, null, 2));
821
846
  break;
@@ -837,7 +862,30 @@ async function main() {
837
862
  // 智能提交建议(2026-08-21 审计第二批 G1-G3):commit skill 此前让 agent 手工收集
838
863
  // 语义(LAST_COMMIT_TIME / cat QUICKLOG / 扫变更目录 / 按路径模式分类)——进度库、
839
864
  // quicklog、git 全在 CLI 手里。一条命令产出统计 + 语义来源 + conventional 建议 +
840
- // 可照抄的 git 命令。**只建议不执行**(确认权在人,对齐 skill 绝对规则「不要自动提交」)。
865
+ // 可照抄的 git 命令。**默认只建议不执行**(确认权在人,对齐 skill 绝对规则「不要自动提交」);
866
+ // --apply(P1-6,noai-ir-roadmap §4)唯一例外且只限归档语境(文件已验收 + pathspec
867
+ // 显式 + pathspec 限定 commit 不扫并行会话暂存)——agent 上下文「需人确认」是伪约束,
868
+ // 通用自动 git 在多会话仓是历史重灾区,故收窄到这一处。
869
+ if (args.includes('--apply')) {
870
+ const apChangeIdx = args.indexOf('--change');
871
+ const apChange = apChangeIdx >= 0 && args[apChangeIdx + 1] ? args[apChangeIdx + 1] : null;
872
+ if (!apChange) {
873
+ console.error('用法: sillyspec commit --apply --change <名>\n 只限归档语境(changes.status=archived 或 current_stage=archive)执行归档产物提交');
874
+ process.exit(2);
875
+ }
876
+ assertSafeChangeName(apChange, '--change 变更名');
877
+ const { applyArchiveCommit } = await import('./commit-suggest.js');
878
+ const r = applyArchiveCommit({ cwd: dir, specDir, changeName: apChange });
879
+ if (!r.ok) {
880
+ console.error(`❌ commit --apply 拒绝: ${r.reason}`);
881
+ process.exit(1);
882
+ }
883
+ console.log(`✅ 归档提交完成(pathspec 限定 ${r.paths.length} 个路径,不含并行会话暂存):`);
884
+ for (const p of r.paths) console.log(` - ${p}`);
885
+ console.log(`\n💬 ${r.subject}`);
886
+ if (r.output) console.log(r.output.split('\n').map(l => ' ' + l).join('\n'));
887
+ break;
888
+ }
841
889
  const { collectCommitContext } = await import('./commit-suggest.js');
842
890
  const cc = collectCommitContext({ cwd: dir, specDir });
843
891
  if (json) {
@@ -884,7 +932,8 @@ async function main() {
884
932
  console.log(cc.suggestion.body.split('\n').map(l => ' ' + l).join('\n'));
885
933
  }
886
934
  const bodyArgs = cc.suggestion.body ? ` -m "${cc.suggestion.body.replace(/"/g, '\\"').replace(/\n/g, ' ')}"` : '';
887
- console.log(`\n 执行:git add -A && git commit -m "${cc.suggestion.subject.replace(/"/g, '\\"')}"${bodyArgs}`);
935
+ // 规则 18(2026-09-10 目录级 git add 事故):建议命令用显式 pathspec,不再教 git add -A
936
+ console.log(`\n 执行:git add -- <本次文件…> && git commit -m "${cc.suggestion.subject.replace(/"/g, '\\"')}"${bodyArgs} -- <本次文件…>`);
888
937
  } else {
889
938
  console.log('\n💡 无语义来源匹配——请结合上方 diff stat 手写 message');
890
939
  }
@@ -905,7 +954,7 @@ async function main() {
905
954
  process.exit(2);
906
955
  }
907
956
  assertSafeChangeName(vpChange, '--change 变更名');
908
- const { runVerifyProbes, renderVerifyProbesReport, generateVerifyResultSkeleton, resolveVerifyProbesSpecBase, writeVerifyFacts } = await import('./verify-probes.js');
957
+ const { runVerifyProbes, renderVerifyProbesReport, generateVerifyResultSkeleton, resolveVerifyProbesSpecBase, writeVerifyFacts, formatPlatformPathNote } = await import('./verify-probes.js');
909
958
  // spec 根统一走漂移锚定(坑 worktree-spec-artifact-misplace):在 worktree 内跑时锚回主仓,
910
959
  // 探针读取与 --init 骨架都落主仓——与 plan/execute/verify/archive 的 command.js 守卫同口径。
911
960
  // resolvePlatformSpecDir 仍先调(保留平台接管 fail-closed 检查副作用),但仅 pointer 存在时
@@ -914,6 +963,19 @@ async function main() {
914
963
  const vpResolved = resolvePlatformSpecDir(dir, specDir);
915
964
  const vpPlatformBase = existsSync(join(dir, '.sillyspec-platform.json')) ? vpResolved : null;
916
965
  const vpSpecBase = resolveVerifyProbesSpecBase(dir, specDir, vpPlatformBase);
966
+ // 平台模式回显注记(2026-09-10 驾驭小结②):物理路径在 hub 镜像根,产物经 daemon
967
+ // spec-sync 落主仓——回显补主仓同步位置,消「回显镜像路径 / 核对主仓路径」的显示混乱
968
+ const vpPlatformNote = formatPlatformPathNote(vpPlatformBase, vpChange, 'verify-result.md');
969
+ // 平台模式双写镜像(坑 platform-init-artifact-daemon-dir-only,2026-09-10 驾驭小结第三批②):
970
+ // agent 本地(pointer 态、非显式 --spec-dir)--init 产物同时镜像主仓 changeDir,不再单点
971
+ // 依赖 spec-sync 回程(用户两次实锤产物落 daemon specs 同步目录)。
972
+ const mirrorInitArtifact = async (platformPath, file, content) => {
973
+ if (!vpPlatformBase || specDir) return null
974
+ try {
975
+ const { mirrorPlatformArtifactToMainRepo } = await import('./run/shared.js')
976
+ return mirrorPlatformArtifactToMainRepo({ cwd: dir, changeName: vpChange, file, content, platformBase: vpPlatformBase })
977
+ } catch { return null }
978
+ }
917
979
  const vpResult = runVerifyProbes({ cwd: dir, changeName: vpChange, specDir: vpSpecBase });
918
980
  if (json) {
919
981
  console.log(JSON.stringify({ command: 'verify-probes', change: vpChange, ...vpResult }, null, 2));
@@ -933,17 +995,155 @@ async function main() {
933
995
  // 指引「重跑 --init」对已存在报告是 no-op。修法:探针段是 CLI 拥有的机械段,检测缺失时
934
996
  // 尾部补注入(正文其余不动),判别子自此有子节可对账。
935
997
  const vpSection = `\n## 探针结果(CLI 机械预填,--init 补注入) [层:可复跑探针——gate 抽查防篡改]\n${renderVerifyProbesReport(vpResult)}\n`;
936
- writeFileSync(vpReportPath, vpExisting.replace(/\s*$/, '\n') + vpSection);
937
- console.log(`\n📄 存量旧格式 verify-result.md 已补注入探针预填段: ${vpReportPath}(正文其余未动;补注后按新预填段如实核对结论)`);
998
+ const vpInjected = vpExisting.replace(/\s*$/, '\n') + vpSection;
999
+ writeFileSync(vpReportPath, vpInjected);
1000
+ await mirrorInitArtifact(vpReportPath, 'verify-result.md', vpInjected);
1001
+ console.log(`\n📄 存量旧格式 verify-result.md 已补注入探针预填段: ${vpReportPath}(正文其余未动;补注后按新预填段如实核对结论)${vpPlatformNote}`);
938
1002
  } else {
939
- console.log(`\nℹ️ verify-result.md 已存在,不覆盖: ${vpReportPath}`);
1003
+ console.log(`\nℹ️ verify-result.md 已存在,不覆盖: ${vpReportPath}${vpPlatformNote}`);
940
1004
  }
941
1005
  } else {
942
- writeFileSync(vpReportPath, generateVerifyResultSkeleton(vpResult));
943
- console.log(`\n📄 已生成 verify-result.md 骨架: ${vpReportPath}(探针已预填;结论必须写明 PASS/FAIL,留待填会被 gate 判不过)`);
1006
+ const vpSkeleton = generateVerifyResultSkeleton(vpResult);
1007
+ writeFileSync(vpReportPath, vpSkeleton);
1008
+ await mirrorInitArtifact(vpReportPath, 'verify-result.md', vpSkeleton);
1009
+ console.log(`\n📄 已生成 verify-result.md 骨架: ${vpReportPath}(探针已预填;结论必须写明 PASS/FAIL,留待填会被 gate 判不过)${vpPlatformNote}`);
1010
+ }
1011
+ const vpFacts = writeVerifyFacts(join(vpSpecBase, 'changes', vpChange), vpResult, vpChange, {
1012
+ platformNote: formatPlatformPathNote(vpPlatformBase, vpChange, 'verify-facts.json'),
1013
+ }); // P3b:facts 底稿随 --init 刷新(骨架已存在也刷新;v2 起分段合并保留固化段)
1014
+ if (vpFacts && vpFacts.path) {
1015
+ try { await mirrorInitArtifact(vpFacts.path, 'verify-facts.json', readFileSync(vpFacts.path, 'utf8')) } catch { /* 读回失败跳过镜像 */ }
1016
+ }
1017
+ // 2026-09-08-ir-verify-facts R-02:已存在 md 缺证据账/回执槽段时仅追加骨架(幂等二跑
1018
+ // 零改动);task 行按 verify-required-evidence.json 预填(存在时)。
1019
+ if (existsSync(vpReportPath)) {
1020
+ let vpReqItems = []
1021
+ try {
1022
+ const vpReqPath = join(vpSpecBase, 'changes', vpChange, 'verify-required-evidence.json')
1023
+ if (existsSync(vpReqPath)) {
1024
+ const vpReq = JSON.parse(readFileSync(vpReqPath, 'utf8'))
1025
+ vpReqItems = Array.isArray(vpReq.items) ? vpReq.items.map(it => ({ task: it.task, evidence: it.evidence || [] })) : []
1026
+ }
1027
+ } catch { /* 读取失败 → 空预填,不阻断 */ }
1028
+ const { backfillMissingEvidenceSlots } = await import('./verify-probes.js')
1029
+ const vpAdded = backfillMissingEvidenceSlots(vpReportPath, vpReqItems)
1030
+ if (vpAdded.added.length > 0) {
1031
+ console.log(`📄 已补齐缺失槽段(不触碰既有正文): ${vpAdded.added.join('、')}`)
1032
+ }
944
1033
  }
945
- writeVerifyFacts(join(vpSpecBase, 'changes', vpChange), vpResult, vpChange); // P3b:facts 底稿随 --init 刷新(骨架已存在也刷新,最近快照语义)
1034
+ // P0-1(noai-ir-roadmap §3)两件机械预填,均幂等 fail-soft:
1035
+ // ①决策追踪矩阵机械半边——D→FR→task 链自 decisions.md × tasks/*.md frontmatter
1036
+ // 结构化字段构建,Evidence/状态两列留 agent 逐格复核;
1037
+ // ②结论草稿——机械事实全绿(noAI 实测 + 探针 1/3/5/6 + 风险门)才预填 PASS,
1038
+ // 「草稿待确认」形态,gate 判定链不动(前置三)。
1039
+ try {
1040
+ const { injectDecisionChainDraft } = await import('./verify-probes.js')
1041
+ const vpChain = injectDecisionChainDraft(vpReportPath, join(vpSpecBase, 'changes', vpChange))
1042
+ if (vpChain) {
1043
+ try { await mirrorInitArtifact(vpReportPath, 'verify-result.md', readFileSync(vpReportPath, 'utf8')) } catch { /* 镜像失败不阻断 */ }
1044
+ console.log(`🔗 决策追踪矩阵机械半边已预填:${vpChain.decisions} 条决策 × ${vpChain.tasks} 张 task 卡(Evidence/状态两列待逐格复核;未闭环行须在报告标注风险)`)
1045
+ }
1046
+ } catch { /* 链路预填失败不阻断 --init */ }
1047
+ try {
1048
+ const { extractVerifyConclusionSlot } = await import('./stage-contract.js')
1049
+ const { evaluateConclusionDraft } = await import('./run/verify-quality-scan.js')
1050
+ const { applyConclusionDraftToText } = await import('./verify-probes.js')
1051
+ const vpMd = readFileSync(vpReportPath, 'utf8')
1052
+ if (extractVerifyConclusionSlot(vpMd) === '') {
1053
+ const vpDraftEval = evaluateConclusionDraft({ cwd: dir, specBase: vpSpecBase, changeName: vpChange, changeDir: join(vpSpecBase, 'changes', vpChange), probesResult: vpResult })
1054
+ if (vpDraftEval.draft) {
1055
+ const vpNext = applyConclusionDraftToText(vpMd, vpDraftEval.draft)
1056
+ writeFileSync(vpReportPath, vpNext)
1057
+ try { await mirrorInitArtifact(vpReportPath, 'verify-result.md', vpNext) } catch { /* 镜像失败不阻断 */ }
1058
+ console.log(`🤖 结论草稿已预填 ${vpDraftEval.draft}(机械事实全绿:noAI 实测通过 + 探针 1/3/5/6 干净 + 风险门非 integration/deployment)——草稿待确认,复核后不同意即改写槽行;gate 独立复核不受预填影响。`)
1059
+ } else {
1060
+ console.log(`ℹ️ 结论草稿未预填(${vpDraftEval.reasons.join(';')})——槽行由你填写。`)
1061
+ }
1062
+ }
1063
+ } catch { /* 草稿预填失败不阻断 --init */ }
1064
+ }
1065
+ break;
1066
+ }
1067
+ case 'review-dispatch': {
1068
+ // tier=independent 独立审查的平台派发命令(2026-09-10-review-dispatch FR-01~04 / D-001~003@1):
1069
+ // 三形态——缺省=create(probe 三层前置 + createMission(external) + dispatchWorker(read_only),
1070
+ // 异步立即返回)、--status=轮询+停滞检测+终态回收(artifacts→schema/docHash 校验→落盘既有
1071
+ // stage-reviews 路径)、--kill=清在途记录+平台处置指引(处置权在人)。薄壳:全部逻辑在
1072
+ // src/review-dispatch.js(可测核心),此处只做 flag 解析与退出码。
1073
+ const rdChangeIdx = args.indexOf('--change');
1074
+ const rdChange = rdChangeIdx >= 0 && args[rdChangeIdx + 1] ? args[rdChangeIdx + 1] : null;
1075
+ const rdStageIdx = args.indexOf('--stage');
1076
+ const rdStage = rdStageIdx >= 0 && args[rdStageIdx + 1] ? args[rdStageIdx + 1] : null;
1077
+ const rdStatus = args.includes('--status');
1078
+ const rdKill = args.includes('--kill');
1079
+ const rdMode = rdKill ? 'kill' : rdStatus ? 'status' : 'create';
1080
+ const rdUsage = '用法: sillyspec review-dispatch --change <名> --stage <brainstorm|plan|execute> [--status] [--kill]\n' +
1081
+ ' 缺省=派发(probe 前置→create_mission(external)→dispatch_worker(read_only),异步返回 missionId)\n' +
1082
+ ' --status=轮询状态+停滞提示;worker completed 自动回收落盘 review.json(reviewer.channel=platform)\n' +
1083
+ ' --kill=清本地在途记录+平台处置指引(不自动 kill,处置权在人)';
1084
+ if (!rdChange) { console.error(rdUsage); process.exit(2); }
1085
+ assertSafeChangeName(rdChange, '--change 变更名');
1086
+ if (rdMode === 'create' && !['brainstorm', 'plan', 'execute'].includes(rdStage)) {
1087
+ console.error(`❌ create 形态必须 --stage ∈ brainstorm|plan|execute(实际:${rdStage || '缺'})\n${rdUsage}`);
1088
+ process.exit(2);
1089
+ }
1090
+ const { runReviewDispatch, readReviewDispatchConfig } = await import('./review-dispatch.js');
1091
+ const rdCfg = readReviewDispatchConfig(dir);
1092
+ // MCP client:kill 不需要网络;create/status 需要(未配置时核心层 probe no-config 退出并给降级指引)
1093
+ let rdClient = null;
1094
+ if (rdMode !== 'kill') {
1095
+ const { SillyHubMcpClient } = await import('./sillyhub-mcp/client.js');
1096
+ rdClient = new SillyHubMcpClient({ cwd: dir });
1097
+ }
1098
+ // specBase:与其他顶级命令同口径(平台接管优先;无 pointer 回退本地 .sillyspec)
1099
+ const rdSpecBase = resolvePlatformSpecDir(dir, specDir);
1100
+ const r = await runReviewDispatch({
1101
+ mode: rdMode, cwd: dir, specBase: rdSpecBase, changeName: rdChange, stage: rdStage,
1102
+ client: rdClient, budgetUsd: rdCfg.budget_usd, stallMs: rdCfg.stall_ms,
1103
+ });
1104
+ if (r.ok) {
1105
+ if (r.statusLine) console.log(`🛰️ ${r.statusLine}`);
1106
+ if (r.state === 'completed') {
1107
+ console.log(`✅ 平台审查回收完成:review.json 已落 ${r.reviewPath}(verdict ${r.verdict})——重跑阶段 --done 走 Stage Review Gate`);
1108
+ } else if (rdMode === 'create') {
1109
+ console.log(`✅ 已派发:mission ${r.missionId} / worker ${r.workerId}(reviewRunId ${r.reviewRunId})`);
1110
+ }
1111
+ if (r.stalled) console.warn(`⚠️ ${r.stallHint}`);
1112
+ if (r.nextStep && rdMode !== 'kill') console.log(`👉 ${r.nextStep}`);
1113
+ if (rdMode === 'kill') console.log(`👉 ${r.platformHint}`);
1114
+ } else {
1115
+ console.error(`❌ review-dispatch(${rdMode}): ${r.reason}`);
1116
+ if (r.statusLine) console.error(` ${r.statusLine}`);
1117
+ if (r.errors) for (const e of r.errors) console.error(` - ${e}`);
1118
+ if (r.guidance) console.error(` 下一步:\n${r.guidance}`);
1119
+ process.exitCode = 1;
1120
+ }
1121
+ break;
1122
+ }
1123
+ case 'module-docs-sync': {
1124
+ // 模块文档 sidecar 同步命令化(2026-09-09 刀批):diff 归属模块 → sidecar 追加变更索引行 +
1125
+ // 卡 updated_at 戳——机械部分 CLI 代算,卡片正文语义更新仍归 agent/归档流程。幂等。
1126
+ const mdsChangeIdx = args.indexOf('--change');
1127
+ const mdsChange = mdsChangeIdx >= 0 && args[mdsChangeIdx + 1] ? args[mdsChangeIdx + 1] : null;
1128
+ const mdsNoteIdx = args.indexOf('--note');
1129
+ const mdsNote = mdsNoteIdx >= 0 && args[mdsNoteIdx + 1] ? args[mdsNoteIdx + 1] : null;
1130
+ if (!mdsChange) {
1131
+ console.error('用法: sillyspec module-docs-sync --change <name> [--note "一句话"] [--json]\n diff 归属模块 → sidecar 追加变更索引行 + 卡 updated_at 戳(幂等;卡片正文语义更新仍归 agent)');
1132
+ process.exit(2);
946
1133
  }
1134
+ const { syncModuleDocSidecars } = await import('./module-impact.js');
1135
+ const mdsResult = syncModuleDocSidecars({ cwd: dir, changeName: mdsChange, note: mdsNote, specDir });
1136
+ if (json) {
1137
+ console.log(JSON.stringify({ command: 'module-docs-sync', change: mdsChange, ...mdsResult }, null, 2));
1138
+ break;
1139
+ }
1140
+ if (mdsResult.reason) console.log(mdsResult.reason);
1141
+ if (mdsResult.synced.length > 0) {
1142
+ console.log(String.fromCharCode(9989) + ' sidecar 已同步 ' + mdsResult.synced.length + ' 个模块: ' + mdsResult.synced.join(', '));
1143
+ console.log(' 卡 updated_at 已戳;卡片正文语义更新(如需要)仍归归档流程/agent。');
1144
+ }
1145
+ if (mdsResult.skipped.length > 0) console.log(String.fromCharCode(8505) + ' 已含本变更行跳过: ' + mdsResult.skipped.join(', '));
1146
+ if (mdsResult.unmatched.length > 0) console.log(String.fromCharCode(9888) + ' 未匹配模块的文件 ' + mdsResult.unmatched.length + ' 个(不入 sidecar): ' + mdsResult.unmatched.slice(0, 5).join(', '));
947
1147
  break;
948
1148
  }
949
1149
  case 'plan-adopt-waves': {
@@ -1023,6 +1223,96 @@ async function main() {
1023
1223
  console.log(` 归类 ${miResult.matchedCount} 个文件,未匹配 ${miResult.unmatchedCount} 个;影响类型列逐行替换 <!--TODO-->。`);
1024
1224
  break;
1025
1225
  }
1226
+ case 'scope-audit': {
1227
+ // 变更范围对账命令化(2026-09-10-change-scope-audit task-03,FR-02):Wave 1 纯函数
1228
+ // computeChangeScopeAudit 的 CLI 形态——用户与 agent 不依赖阶段 --done 时点,随时查
1229
+ // 「计划改动 × 实际改动」三态全表(✓ 计划内 / ⚠️ 计划外 / ⚠️ 计划未动;quick 会话出
1230
+ // 归属表)。advisory only(D-006):只读不落盘、不写门禁状态、既有命令行为零变化;
1231
+ // 路由形态对齐 verify-probes(:897)/ module-impact(:1041)先例——--change 必填缺参
1232
+ // 用法错 exit 2、assertSafeChangeName 消毒、动态 import、--json 结构化输出、运行错
1233
+ // fail-soft 出错误摘要 exit 1 不抛栈。
1234
+ const saChangeIdx = args.indexOf('--change');
1235
+ const saChange = saChangeIdx >= 0 && args[saChangeIdx + 1] ? args[saChangeIdx + 1] : null;
1236
+ if (!saChange) {
1237
+ console.error('用法: sillyspec scope-audit --change <name> [--file <path>] [--json] [--spec-dir <path>]\n 变更范围对账:计划×实际三态全表 + 行数(quick 会话传 quick-<8hex> 出归属表;已归档变更可查——快照冻结记录态);--file <path> 看单文件变化内容(对账同源锚点的 git diff);advisory 只读展示,不构成门禁');
1238
+ process.exit(2);
1239
+ }
1240
+ assertSafeChangeName(saChange, '--change 变更名');
1241
+ // --file(quick-63776328):单文件 diff 内容查看——锚点与表格行数同源(getFileDiff 内部
1242
+ // 复用 computeChangeScopeAudit 锚 + resolveDiffRoot),用户无需手记基点/worktree 路径。
1243
+ const saFileIdx = args.indexOf('--file');
1244
+ const saFile = saFileIdx >= 0 && args[saFileIdx + 1] && !String(args[saFileIdx + 1]).startsWith('--') ? args[saFileIdx + 1] : null;
1245
+ const { computeChangeScopeAudit, renderScopeAuditTable, getFileDiff } = await import('./scope-audit.js');
1246
+ // specBase 解析对齐 verify-probes 的 worktree 锚定主仓口径(坑 worktree-spec-artifact-misplace):
1247
+ // resolvePlatformSpecDir 仍先调(保留平台接管 fail-closed 检查副作用),但仅 pointer 存在时才把
1248
+ // 返回值当平台根——无 pointer 时它回退 resolveSpecDir(cwd) 会返回 worktree 副本根,误当
1249
+ // platformBase 会让锚定被跳过。无显式 --spec-dir 时 detectWorktreeSpecDrift 锚回主仓
1250
+ // (worktree 副本里查不到主仓 change 目录——dogfood 自测即此形态)。
1251
+ const saResolved = resolvePlatformSpecDir(dir, specDir);
1252
+ const saPlatformBase = existsSync(join(dir, '.sillyspec-platform.json')) ? saResolved : null;
1253
+ let saSpecBase = saPlatformBase || resolveSpecDir(dir, { specDir: specDir || undefined });
1254
+ let saDriftAnchor = null;
1255
+ if (!saPlatformBase && !specDir) {
1256
+ const saWt = detectWorktreeSpecDrift(saSpecBase);
1257
+ if (saWt) {
1258
+ console.warn(`⚠️ 已自动锚定主仓 spec:${saWt.mainSpecBase}(原 cwd 命中 worktree 副本 ${saWt.changeName},scope-audit 对账读主仓,流程继续)`);
1259
+ saSpecBase = saWt.mainSpecBase;
1260
+ saDriftAnchor = saWt.mainSpecBase;
1261
+ }
1262
+ }
1263
+ // full-flow 变更目录存在性预检(endpoints baseline 先例同款):post-apply 形态下实际侧三源
1264
+ // 对「变更不存在」不敏感(status 成功即 ok=true,会把主仓脏区误当实际侧),靠预检给明确
1265
+ // 错误摘要 exit 1;quick 会话 id 无 changes/ 目录(guard 祖先链定位),不预检。
1266
+ // 归档形态兼容(quick-8aa52289):changes/<名> 不在 → 接受 changes/archive/<名>(scope-audit
1267
+ // 内部走快照记录态/HEAD 未提交窗口兜底)。
1268
+ if (!/^quick-[0-9a-f]{8}$/.test(saChange)
1269
+ && !existsSync(join(saSpecBase, 'changes', saChange))
1270
+ && !existsSync(join(saSpecBase, 'changes', 'archive', saChange))) {
1271
+ console.error(`❌ 变更目录不存在(活跃与归档区均未找到): ${join(saSpecBase, 'changes', saChange)}(确认 --change 名,或 --spec-dir 指向主仓 .sillyspec)`);
1272
+ process.exit(1);
1273
+ }
1274
+ // --file 分支:单文件 diff(预检同表模式——锚点依赖变更上下文;json 面出结构化)
1275
+ if (saFile) {
1276
+ let fd;
1277
+ try {
1278
+ fd = await getFileDiff({ cwd: dir, specBase: saSpecBase, changeName: saChange, platformOpts: saDriftAnchor ? { specDriftAnchor: saDriftAnchor } : null, filePath: saFile });
1279
+ } catch (e) {
1280
+ console.error(`❌ scope-audit --file 执行失败: ${e && e.message ? String(e.message).split('\n')[0] : e}`);
1281
+ process.exit(1);
1282
+ }
1283
+ if (json) {
1284
+ console.log(JSON.stringify({ command: 'scope-audit', change: saChange, file: saFile, ...fd }, null, 2));
1285
+ } else if (fd.ok && fd.diff) {
1286
+ console.log(`📄 ${saFile}(${fd.mode}|锚点 ${fd.anchorLabel}|git diff 原生格式)`);
1287
+ console.log(fd.diff);
1288
+ } else {
1289
+ console.log(`ℹ️ ${saFile}(${fd.mode}|锚点 ${fd.anchorLabel || '—'}):${fd.note || '无 diff'}`);
1290
+ }
1291
+ if (!fd.ok) process.exitCode = 1;
1292
+ break;
1293
+ }
1294
+ // 计算包在 withJsonOutput 内(gate/derive 先例同款):quick 模式重跑 auditQuickCompletion
1295
+ // 的既有 console 输出(并发 warn 等,R-07)不污染 --json 的 stdout。
1296
+ let saResult;
1297
+ try {
1298
+ saResult = await withJsonOutput(json, () => computeChangeScopeAudit({ cwd: dir, specBase: saSpecBase, changeName: saChange, platformOpts: saDriftAnchor ? { specDriftAnchor: saDriftAnchor } : null }));
1299
+ } catch (e) {
1300
+ console.error(`❌ scope-audit 计算失败: ${e && e.message ? String(e.message).split('\n')[0] : e}`);
1301
+ process.exit(1);
1302
+ }
1303
+ if (json) {
1304
+ console.log(JSON.stringify({ command: 'scope-audit', change: saChange, ...saResult }, null, 2));
1305
+ } else {
1306
+ console.log(renderScopeAuditTable(saResult));
1307
+ }
1308
+ // ok=false(quick 会话不存在 / 实际侧整体失败)→ 明确错误摘要 + exit 1(json 面摘要走
1309
+ // stderr,stdout 保持合法 JSON)
1310
+ if (saResult && saResult.ok === false) {
1311
+ console.error(`❌ scope-audit 对账不可用: ${saResult.degradedReason || '未知原因'}`);
1312
+ process.exitCode = 1;
1313
+ }
1314
+ break;
1315
+ }
1026
1316
  case 'endpoints': {
1027
1317
  // endpoints artifact 生成(2026-08-21 审计第三批 H6):后端 router task 的 endpoints.json
1028
1318
  // 此前靠 agent 手扫装饰器手写(易漏 endpoint)。CLI 复用 endpoint-extractor 全套提取器
@@ -1313,6 +1603,97 @@ async function main() {
1313
1603
  console.log(' 逐行替换 <!--TODO--> 为结论(无签名级变更也显式写「无」);gate 拒绝未替换的占位行。');
1314
1604
  break;
1315
1605
  }
1606
+ case 'fourpiece-init': {
1607
+ // 四件套骨架预生成(2026-09-09 ql-20260909-003,轮次经济学 §3.2):brainstorm 期 agent
1608
+ // 手写 proposal/requirements/decisions 的 frontmatter/章节骨架是格式返工根源——本命令
1609
+ // 逐文件生成骨架(已存在不覆盖),agent 只填语义。与 design-init 同族(幂等)。
1610
+ const fpChangeIdx = args.indexOf('--change');
1611
+ const fpChange = fpChangeIdx >= 0 && args[fpChangeIdx + 1] ? args[fpChangeIdx + 1] : null;
1612
+ if (!fpChange) {
1613
+ console.error('用法: sillyspec fourpiece-init --change <name> [--json] [--spec-dir <path>]\n 生成 proposal.md/requirements.md/decisions.md 骨架(frontmatter+章节标题+模板占位;已存在不覆盖;design.md 归 design-init)');
1614
+ process.exit(2);
1615
+ }
1616
+ const fpSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
1617
+ const fpDir = join(fpSpecBase, 'changes', fpChange);
1618
+ mkdirSync(fpDir, { recursive: true });
1619
+ const fpNow = new Date();
1620
+ const fpDate = fpNow.toISOString().slice(0, 10);
1621
+ const fpStamp = fpNow.toISOString().slice(0, 19).replace('T', ' ');
1622
+ const generated = [];
1623
+ const mk = (fname, body) => {
1624
+ const p = join(fpDir, fname);
1625
+ if (existsSync(p)) { console.log(`ℹ️ ${fname} 已存在,不覆盖`); return; }
1626
+ writeFileSync(p, body);
1627
+ generated.push(fname);
1628
+ console.log(`✅ 已生成 ${fname} 骨架: ${p}`);
1629
+ };
1630
+ mk('proposal.md', `---
1631
+ author: qinyi
1632
+ created_at: ${fpStamp}
1633
+ generated_by: sillyspec-fourpiece-init
1634
+ ---
1635
+ # 提案书(Proposal)
1636
+
1637
+ ## 动机
1638
+ <!--TODO: 为什么做、解决什么核心问题-->
1639
+
1640
+ ## 关键问题
1641
+ <!--TODO: 为什么现有方案不够(2-3 个痛点)-->
1642
+
1643
+ ## 变更范围
1644
+ <!--TODO: 本次做什么-->
1645
+
1646
+ ## 不在范围内(显式清单)
1647
+ - <!--TODO: 不做 X-->
1648
+
1649
+ ## 成功标准(可验证)
1650
+ - <!--TODO: 可验证条目-->
1651
+ `);
1652
+ mk('requirements.md', `---
1653
+ author: qinyi
1654
+ created_at: ${fpStamp}
1655
+ generated_by: sillyspec-fourpiece-init
1656
+ ---
1657
+ # 需求规格(Requirements)
1658
+
1659
+ ## 角色
1660
+ | 角色 | 说明 |
1661
+ |---|---|
1662
+ | <!--TODO--> | <!--TODO--> |
1663
+
1664
+ ## 功能需求
1665
+
1666
+ ### FR-01: <!--TODO-->
1667
+ Given <!--TODO-->
1668
+ When <!--TODO-->
1669
+ Then <!--TODO-->
1670
+
1671
+ ## 非功能需求
1672
+ - 兼容性:<!--TODO-->
1673
+
1674
+ ## 决策覆盖矩阵(如存在 decisions.md)
1675
+ | 决策 ID | 覆盖的 FR | 说明 |
1676
+ |---|---|---|
1677
+ `);
1678
+ mk('decisions.md', `---
1679
+ author: qinyi
1680
+ created_at: ${fpStamp}
1681
+ generated_by: sillyspec-fourpiece-init
1682
+ change: ${fpChange}
1683
+ ---
1684
+
1685
+ # 决策记录(Decisions)
1686
+
1687
+ <!-- 增量落盘:每解决一个有实现影响的问题当场追加一条(格式见 brainstorm Step 3 模板);幂等按 D-xxx@vN 判重 -->
1688
+ `);
1689
+ if (json) {
1690
+ console.log(JSON.stringify({ command: 'fourpiece-init', change: fpChange, generated }, null, 2));
1691
+ } else if (generated.length > 0) {
1692
+ console.log(`
1693
+ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语义内容(骨架勿手删章节);design.md 用 sillyspec design-init。`);
1694
+ }
1695
+ break;
1696
+ }
1316
1697
  case 'design-init': {
1317
1698
  // design.md 十三章节骨架生成(change: 2026-09-07-ir-stage-p3c,task-02,FR-03):agent 此前
1318
1699
  // 从零手写整份设计文档,章节漏项到 stage-contract/plan 门禁才暴露。本命令从 decisions.md
@@ -1358,6 +1739,17 @@ async function main() {
1358
1739
  break;
1359
1740
  }
1360
1741
  writeFileSync(diDesignPath, diSkeleton);
1742
+ // 平台模式双写镜像(坑 platform-init-artifact-daemon-dir-only,2026-09-10 驾驭小结第三批②):
1743
+ // agent 本地(pointer 态、非显式 --spec-dir)跑 design-init 时 design.md 同时镜像主仓
1744
+ // changeDir——不再单点依赖 spec-sync 回程(两次实锤产物落 daemon specs 同步目录)。
1745
+ try {
1746
+ const { mirrorPlatformArtifactToMainRepo } = await import('./run/shared.js');
1747
+ mirrorPlatformArtifactToMainRepo({
1748
+ cwd: dir, changeName: diChange, file: 'design.md', content: diSkeleton,
1749
+ platformBase: existsSync(join(dir, '.sillyspec-platform.json')) ? diSpecBase : null,
1750
+ specDirExplicit: Boolean(specDir),
1751
+ });
1752
+ } catch { /* 镜像失败不阻断主写入 */ }
1361
1753
  if (json) {
1362
1754
  console.log(JSON.stringify({ command: 'design-init', change: diChange, ok: true, path: diDesignPath, written: true, overwritten: diExisted }, null, 2));
1363
1755
  break;
@@ -1658,23 +2050,36 @@ async function main() {
1658
2050
  // 只读 stdout 可捕获);stderr 仅留 ⚠️ 诊断(console.warn)与配置错误(exit 2 路径)。
1659
2051
  // 文本逐字不变,只改通道。
1660
2052
  console.log(`\n❌ docs check: ${result.invalid.length}/${result.total} 处引用失效:`);
2053
+ // 显示去重(2026-09-10 驾驭小结第二批③):同 doc+docLine+ref 的重复 invalid(同一
2054
+ // 引用在行内多次出现时逐条收集)只显示一次、带 ×N 次数——invalid 集与 exit code
2055
+ // 不动,纯渲染降噪。
2056
+ const shownInvalid = [];
2057
+ const dupCountByIdentity = new Map();
2058
+ for (const inv of result.invalid) {
2059
+ const key = `${inv.doc}#${inv.docLine}#${inv.ref}`;
2060
+ dupCountByIdentity.set(key, (dupCountByIdentity.get(key) || 0) + 1);
2061
+ if (dupCountByIdentity.get(key) === 1) shownInvalid.push(inv);
2062
+ }
2063
+ const dupTotal = result.invalid.length - shownInvalid.length;
1661
2064
  let appliedLeft = fixResult ? fixResult.applied : 0;
1662
2065
  const skippedQueue = fixResult ? [...fixResult.skipped] : [];
1663
- for (const inv of result.invalid) {
2066
+ for (const inv of shownInvalid) {
2067
+ const dupN = dupCountByIdentity.get(`${inv.doc}#${inv.docLine}#${inv.ref}`);
2068
+ const dupTag = dupN > 1 ? `(同引用重复 ×${dupN})` : '';
1664
2069
  const newRef = newRefByInv.get(inv);
1665
2070
  if (newRef !== undefined && appliedLeft > 0) {
1666
2071
  // 已应用(dry-run 为将应用):归因按构造顺序消耗 applied 计数——写回 skipped 仅
1667
2072
  // 防御路径(文档消失/行内失配)触发,CLI 正常流 fixable 必然落位
1668
- console.log(` ✅ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${newRef}${dryRun ? '(dry-run 未写盘)' : ''}`);
2073
+ console.log(` ✅ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${newRef}${dryRun ? '(dry-run 未写盘)' : ''}${dupTag}`);
1669
2074
  appliedLeft--;
1670
2075
  } else if (newRef !== undefined) {
1671
2076
  const s = skippedQueue.shift();
1672
- console.log(` ⚠️ [${inv.doc}:L${inv.docLine}] ${inv.ref} → 写回跳过:${s ? s.reason : '未知原因'}`);
2077
+ console.log(` ⚠️ [${inv.doc}:L${inv.docLine}] ${inv.ref} → 写回跳过:${s ? s.reason : '未知原因'}${dupTag}`);
1673
2078
  } else if (fixResult) {
1674
2079
  // needs-manual:多命中歧义/零命中/无 token——fix.reason 含候选行号,交人工
1675
- console.log(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${(inv.fix && inv.fix.reason) || inv.reason}(待人工)`);
2080
+ console.log(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${(inv.fix && inv.fix.reason) || inv.reason}(待人工)${dupTag}`);
1676
2081
  } else {
1677
- console.log(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${inv.reason}`);
2082
+ console.log(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${inv.reason}${dupTag}`);
1678
2083
  }
1679
2084
  if (inv.suggest && inv.suggest.length > 0) {
1680
2085
  // 2026-09-07-ir-hardening task-08:原 --suggest 旗标删除(no-op 首选路径退役),
@@ -1682,6 +2087,9 @@ async function main() {
1682
2087
  console.log(` 💡 候选行号: ${inv.suggest.join(', ')}(token 命中行,人工确认后更新文档锚)`);
1683
2088
  }
1684
2089
  }
2090
+ if (dupTotal > 0) {
2091
+ console.log(` ℹ️ 已折叠 ${dupTotal} 条同文档行同引用的重复条目(计数 ×N 已并入上文)。`);
2092
+ }
1685
2093
  if (fixResult) {
1686
2094
  console.log(`\n🔧 重锚报告:${fixResult.applied} 处已${dryRun ? '预览' : '改写'}${dryRun ? '(dry-run 未写盘)' : ''}、${result.invalid.length - fixes.length} 处待人工${fixResult.skipped.length > 0 ? `、${fixResult.skipped.length} 处写回跳过` : ''}。`);
1687
2095
  if (fixReceipt) {
@@ -1694,9 +2102,21 @@ async function main() {
1694
2102
  // 变更名提名 advisory 段(主结果之后、exit 之前;不参与 ok/invalid 与 exit code)。
1695
2103
  // 零输出原则(docs-check-fix 契约:无新 flag 时 stdout 逐字节一致;决策规则族同款
1696
2104
  // 「无信号零输出」):仅 findings 非空才输出——悬空即信号,无悬空不打扰。
2105
+ // 输出降噪(2026-09-10 驾驭小结第二批③):①advisory 固定尾部分区(硬失效区块与
2106
+ // 修复指引之后),标题明示「不阻断、不进 exit code」,与硬失效 ❌ 视觉分离;②同一
2107
+ // 悬空名多处提名按名聚合一行(名 + 提及处数 + 首处定位),不再逐处刷屏。
1697
2108
  if (changeNameReport && changeNameReport.findings.length > 0) {
1698
- console.warn(`⚠️ 变更名提名悬空 ${changeNameReport.findings.length}/${changeNameReport.mentions} 处(advisory 不阻断、不进 docs gate):`);
1699
- for (const f of changeNameReport.findings) console.warn(` - ${f.message}`);
2109
+ console.log(`\n⚠️ ── 变更名提名悬空(advisory:不阻断、不进 exit code / docs gate)──`);
2110
+ const byName = new Map(); // name -> { count, first }
2111
+ for (const f of changeNameReport.findings) {
2112
+ const e = byName.get(f.name) || { count: 0, first: f };
2113
+ e.count++;
2114
+ byName.set(f.name, e);
2115
+ }
2116
+ for (const [name, e] of byName) {
2117
+ console.log(` - 「${name}」${e.count} 处提及悬空(活跃∪归档变更名单中不存在;首处 ${e.first.doc}:L${e.first.docLine})——拼错 / 改名未同步 / 他仓变更?修正后复跑,或 local.yaml known_failures 加 "change-name.${name}" 豁免`);
2118
+ }
2119
+ console.log(` (共 ${changeNameReport.findings.length} 处 / ${changeNameReport.uniqueNames} 个名字,全文提及 ${changeNameReport.mentions} 处)`);
1700
2120
  if (changeNameReport.exempted.length > 0) {
1701
2121
  console.log(`ℹ️ 另有 ${changeNameReport.exempted.length} 处经 known_failures change-name.* 键豁免`);
1702
2122
  }
@@ -1810,9 +2230,10 @@ async function main() {
1810
2230
  // slice(1) 去掉的是 'run' 字面量,这里 command 本身就是 stage 名不能丢。
1811
2231
  case 'doctor': {
1812
2232
  const doctorEffectiveDir = specDir ? dir : resolveEffectiveDir(dir);
1813
- // 执行流:--cleanup-remnant / --cleanup-ghosts / --dump-db(结构化诊断之外的修复/取证动作)
2233
+ // 执行流:--cleanup-remnant / --cleanup-ghosts / --gc-unstamped-runs / --dump-db(结构化诊断之外的修复/取证动作)
1814
2234
  const cleanupRemnant = filteredArgs.includes('--cleanup-remnant');
1815
2235
  const cleanupGhosts = filteredArgs.includes('--cleanup-ghosts');
2236
+ const gcUnstampedRuns = filteredArgs.includes('--gc-unstamped-runs');
1816
2237
  const dumpDbFlag = filteredArgs.includes('--dump-db');
1817
2238
  const doctorConfirm = filteredArgs.includes('--confirm');
1818
2239
  const pathIdx = filteredArgs.indexOf('--path');
@@ -1918,6 +2339,29 @@ async function main() {
1918
2339
  process.exitCode = r.errors.length > 0 ? 1 : 0;
1919
2340
  break;
1920
2341
  }
2342
+ if (gcUnstampedRuns) {
2343
+ // ql-20260908-006-5f04:无戳 execute-runs 一次性清扫。默认 dry-run,--confirm 才删。
2344
+ // 不进 archive 热路径(pruneArchivedChangeRuntime 故意不猜无戳 run)。
2345
+ const { gcUnstampedExecuteRuns } = await import('./doctor-diagnostics.js');
2346
+ const r = await gcUnstampedExecuteRuns({ cwd: doctorEffectiveDir, specDir, confirm: doctorConfirm });
2347
+ if (json) {
2348
+ console.log(JSON.stringify(r, null, 2));
2349
+ } else {
2350
+ const verb = doctorConfirm ? '已删除' : '待删除(dry-run)';
2351
+ console.log(`🧹 无戳 execute-runs(仅已归档变更、交叉核验通过)${verb}:${r.count} 个 [runtime: ${r.runtime_root || '(无)'}]`);
2352
+ for (const c of r.candidates || []) {
2353
+ console.log(` ${doctorConfirm ? '✅' : '-'} ${c.runId} ← ${c.owner}(via ${c.via})`);
2354
+ }
2355
+ for (const s of r.skipped || []) console.log(` ⏭️ ${s.runId}(${s.reason})`);
2356
+ for (const e of r.errors || []) console.log(` ❌ ${e.runId || ''}: ${e.error}`);
2357
+ if (!doctorConfirm && r.count > 0) {
2358
+ console.log(`\n加 --confirm 执行删除(只删上面列出的无戳 run,有 change 戳 / 命中活跃变更 / 归属不明一律不动)。`);
2359
+ }
2360
+ if (r.reason) console.log(`ℹ️ ${r.reason}`);
2361
+ }
2362
+ process.exitCode = (r.errors && r.errors.length > 0) ? 1 : 0;
2363
+ break;
2364
+ }
1921
2365
  if (dumpDbFlag) {
1922
2366
  if (!dbPath) { console.error('❌ --dump-db 需要 --path <db 路径>'); process.exitCode = 2; break; }
1923
2367
  const { dumpDb } = await import('./doctor-diagnostics.js');
@@ -1944,9 +2388,21 @@ async function main() {
1944
2388
  process.exitCode = output.overall_status === 'pass' ? 0 : 1;
1945
2389
  break;
1946
2390
  }
1947
- // 否则:保持原有 prompt 驱动的 bash 自检流程
1948
- const { runCommand } = await import('./run.js');
1949
- await runCommand([command, ...filteredArgs.slice(1)], doctorEffectiveDir, specDir, { json });
2391
+ // 2026-09-09-doctor-noai:--status 拦截(审查 B-03:等价只读渲染——阶段进度视图,
2392
+ // 零副作用);非 --json 其余形态改道直跑诊断 + renderDoctorSummary(不走
2393
+ // runCommand/initChange/不刷 lastActive——只读零副作用承诺保真)。
2394
+ if (filteredArgs.includes('--status')) {
2395
+ // 等价只读渲染:复用 progress show(cwd 无活跃变更时提示无——与原只读短路视图同语义级)
2396
+ const pm = new ProgressManager({ specDir: resolvePlatformSpecDir(dir, specDir) });
2397
+ pm.show(doctorEffectiveDir);
2398
+ break;
2399
+ }
2400
+ {
2401
+ const { runDoctorDiagnostics, renderDoctorSummary, writeDoctorDiagnosis } = await import('./doctor-diagnostics.js')
2402
+ const diag = await runDoctorDiagnostics({ cwd: doctorEffectiveDir })
2403
+ console.log(renderDoctorSummary(diag))
2404
+ try { writeDoctorDiagnosis(diag, resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec')) } catch { /* fail-soft */ }
2405
+ }
1950
2406
  break;
1951
2407
  }
1952
2408
  case 'scan': {
@@ -2852,6 +3308,108 @@ SillySpec platform — SillyHub 平台同步
2852
3308
  }
2853
3309
  break;
2854
3310
  }
3311
+ case 'mcp': {
3312
+ // MCP Phase 2(P2-g,noai-ir-roadmap §5):最小 stdio MCP server——只读 tools
3313
+ //(next/gate/derive/progress)经子进程 --json 隔离执行,schema 化参数锁死 flag 幻觉面。
3314
+ // hosts 配 command=sillyspec args=[mcp]。Phase 3(--done 状态推进走 MCP)另计。
3315
+ const { startMcpServer } = await import('./mcp-server.js');
3316
+ await startMcpServer();
3317
+ break;
3318
+ }
3319
+ case 'validate': {
3320
+ // 产物 schema 校验总命令(P2-d,noai-ir-roadmap §5):聚合既有校验器(verify-facts /
3321
+ // task review / stage review / module-map)+ 轻形状检查(required-evidence / endpoints)
3322
+ // 为一把伞——「新解析器写之前先过 schema」的统一落点。skip = 未生成不算失败。
3323
+ const vaChangeIdx = args.indexOf('--change');
3324
+ const vaChange = vaChangeIdx >= 0 && args[vaChangeIdx + 1] ? args[vaChangeIdx + 1] : null;
3325
+ if (!vaChange) {
3326
+ console.error('用法: sillyspec validate --change <名> [--json]');
3327
+ console.error(' 校验该变更的全部结构化产物(verify-facts/required-evidence/task reviews/stage reviews/endpoints + module-map)');
3328
+ process.exit(2);
3329
+ }
3330
+ assertSafeChangeName(vaChange, '--change 变更名');
3331
+ const { validateChangeArtifacts, renderValidateReport } = await import('./validate-artifacts.js');
3332
+ const vaSpecBase = resolveSpecDir(dir, specDir && typeof specDir === 'string' ? { specDir } : undefined);
3333
+ const vaResult = await validateChangeArtifacts({ cwd: dir, specBase: vaSpecBase, changeName: vaChange });
3334
+ if (json) {
3335
+ console.log(JSON.stringify({ command: 'validate', change: vaChange, ok: vaResult.ok, checks: vaResult.checks }, null, 2));
3336
+ } else {
3337
+ console.log(renderValidateReport(vaResult));
3338
+ }
3339
+ if (!vaResult.ok) process.exit(1);
3340
+ break;
3341
+ }
3342
+ case 'decisions': {
3343
+ // P1-5(noai-ir-roadmap §4):decisions.md 单一写入者——canonical 格式只此一家(标题式
3344
+ // 条目 + applyField 白名单中文标签),agent 不再手拼自由 markdown(扁平列表静默 0 条 /
3345
+ // 字段拼写漂移事故族退役;读取侧 parseDecisions 零改动双向兼容)。
3346
+ // decisions add --change <c> --id D-001 [--version N] --title "…" [--status confirmed]
3347
+ // [--type architecture] [--domain a(可重复)] [--question/--answer/--anchor]
3348
+ // [--reject-reason/--revisit-when/--supersedes/--impacts] [--note "散文行(可重复)"]
3349
+ // 同 id@v 整块替换(幂等);新 id/version 追加。
3350
+ // decisions list --change <c>——parseDecisions 只读视图(id/状态/类型/模块域/标题)。
3351
+ const decSub = filteredArgs[1];
3352
+ const decChangeIdx = args.indexOf('--change');
3353
+ const decChange = decChangeIdx >= 0 && args[decChangeIdx + 1] ? args[decChangeIdx + 1] : null;
3354
+ if (!decChange || !['add', 'list'].includes(decSub)) {
3355
+ console.error('用法: sillyspec decisions add --change <名> --id D-001 --title "…" [--version N] [--status confirmed] [--type architecture] [--domain x(可重复)] [--question/--answer/--anchor/--reject-reason/--revisit-when/--supersedes/--impacts/--note]\n sillyspec decisions list --change <名>');
3356
+ process.exit(2);
3357
+ }
3358
+ assertSafeChangeName(decChange, '--change 变更名');
3359
+ const decChangeDir = join(resolveSpecDir(dir), 'changes', decChange);
3360
+ if (decSub === 'list') {
3361
+ const { parseDecisions } = await import('./decision-distill.js');
3362
+ const parsed = parseDecisions(decChangeDir);
3363
+ if (parsed.missing) { console.error(`❌ decisions.md 不存在: ${join(decChangeDir, 'decisions.md')}`); process.exit(1); }
3364
+ for (const e of parsed.entries) {
3365
+ console.log(`${e.id} [${e.status || '?'}${e.type ? '/' + e.type : ''}]${(e.domains || []).length ? `[${e.domains.join(',')}]` : ''} ${e.title || '(无标题)'}`);
3366
+ }
3367
+ console.log(`共 ${parsed.entries.length} 条`);
3368
+ break;
3369
+ }
3370
+ // add:flag 收集(多值 --domain/--note 重复收集)
3371
+ const decFlag = (name) => { const i = args.indexOf(name); return i >= 0 && args[i + 1] && !String(args[i + 1]).startsWith('--') ? args[i + 1] : null; };
3372
+ const decMulti = (name) => { const out = []; for (let i = 0; i < args.length; i++) if (args[i] === name && args[i + 1] && !String(args[i + 1]).startsWith('--')) out.push(args[i + 1]); return out; };
3373
+ const decId = decFlag('--id');
3374
+ const decTitle = decFlag('--title');
3375
+ if (!decId || !decTitle) { console.error('❌ decisions add 需要 --id D-xxx 与 --title'); process.exit(2); }
3376
+ if (!/^D-\d+$/.test(decId)) { console.error(`❌ --id 形态非法(期望 D-xxx,实得 ${decId})`); process.exit(2); }
3377
+ const decVersionRaw = decFlag('--version');
3378
+ const decVersion = decVersionRaw === null ? null : parseInt(decVersionRaw, 10);
3379
+ const { parseDecisions } = await import('./decision-distill.js');
3380
+ const decParsed = parseDecisions(decChangeDir);
3381
+ let version = decVersion;
3382
+ if (!Number.isInteger(version) || version < 1) {
3383
+ const maxV = decParsed.entries.filter(e => e.number === decId).reduce((m, e) => Math.max(m, e.version), 0);
3384
+ version = maxV + 1;
3385
+ }
3386
+ const decision = {
3387
+ number: decId,
3388
+ version,
3389
+ title: decTitle,
3390
+ status: decFlag('--status') || undefined,
3391
+ type: decFlag('--type') || undefined,
3392
+ domains: decMulti('--domain'),
3393
+ question: decFlag('--question') || undefined,
3394
+ answer: decFlag('--answer') || undefined,
3395
+ anchor: decFlag('--anchor') || undefined,
3396
+ rejectReason: decFlag('--reject-reason') || undefined,
3397
+ revisitWhen: decFlag('--revisit-when') || undefined,
3398
+ supersedes: decFlag('--supersedes') || undefined,
3399
+ impacts: decFlag('--impacts') || undefined,
3400
+ body: decMulti('--note'),
3401
+ };
3402
+ const { upsertDecision } = await import('./decisions-io.js');
3403
+ try {
3404
+ const r = upsertDecision(decChangeDir, decision);
3405
+ console.log(`📝 决策 ${decId}@v${version} 已${r.action === 'replaced' ? '整块替换(幂等)' : '追加'}: ${r.path}`);
3406
+ if (!decVersionRaw) console.log(` 版本缺省 → 既有最高 v${version - 1} + 1(显式版本用 --version)`);
3407
+ } catch (e) {
3408
+ console.error(`❌ 决策写入被拒: ${e.message}`);
3409
+ process.exit(1);
3410
+ }
3411
+ break;
3412
+ }
2855
3413
  case 'taskcard': {
2856
3414
  const tcName = filteredArgs.slice(1).find(a => !a.startsWith('-'));
2857
3415
  if (!tcName || tcName === 'help' || tcName === '--help' || tcName === '-h') {
@@ -2920,7 +3478,33 @@ checkbox 行;depends_on 自动反填行内注解 "(depends_on: task-01,02)";
2920
3478
 
2921
3479
  const { cmdTaskcard } = await import('./taskcard.js');
2922
3480
  try {
2923
- const result = cmdTaskcard(tcName, { cwd: dir, specDir, taskIds, title: titleVal, titleZh: titleZhVal, force, sets });
3481
+ // 坑 platform-sync-progress-rollback-and-db-corruption 坑4:taskcard 此前只走
3482
+ // resolveSpecDir(祖先遍历本地 .sillyspec),不感知平台接管指针——平台模式下
3483
+ // 仓库根裸跑报「变更目录不存在」,需显式 --spec-dir(与 run/plan 系列不一致)。
3484
+ // 对齐 endpoints 等命令:先 resolvePlatformSpecDir(指针 fail-closed 语义同源)。
3485
+ const tcPlatformSpecDir = resolvePlatformSpecDir(dir, specDir) || specDir;
3486
+ const result = cmdTaskcard(tcName, { cwd: dir, specDir: tcPlatformSpecDir, taskIds, title: titleVal, titleZh: titleZhVal, force, sets });
3487
+ // 平台模式产物双写镜像(坑 platform-init-artifact-daemon-dir-only 同族,2026-09-10 驾驭
3488
+ // 小结第六批①,用户实证 taskcard --all 落 daemon 镜像「中间一度两份副本」):agent 本地
3489
+ // (pointer 态、非显式 --spec-dir)生成的卡片即时镜像主仓 changeDir——不再等 spec-sync
3490
+ // 回程。镜像失败不阻断(镜像根照写,spec-sync 仍是兜底)。
3491
+ if (result.created.length > 0 && existsSync(join(dir, '.sillyspec-platform.json')) && !specDir) {
3492
+ try {
3493
+ const { mirrorPlatformArtifactToMainRepo } = await import('./run/shared.js');
3494
+ const changesAbs = join(tcPlatformSpecDir, 'changes', tcName);
3495
+ for (const created of result.created) {
3496
+ const rel = relative(changesAbs, created).split('\\').join('/');
3497
+ if (!rel || rel.startsWith('..')) continue;
3498
+ try {
3499
+ mirrorPlatformArtifactToMainRepo({
3500
+ cwd: dir, changeName: tcName, file: rel,
3501
+ content: readFileSync(created, 'utf8'),
3502
+ platformBase: tcPlatformSpecDir, ensureParentDir: true,
3503
+ });
3504
+ } catch { /* 单卡镜像失败不连坐 */ }
3505
+ }
3506
+ } catch { /* 镜像链路异常不阻断主命令 */ }
3507
+ }
2924
3508
  for (const f of result.created) console.log(`✅ 已生成: ${f}`);
2925
3509
  for (const f of result.skipped) console.log(`⏭️ 已存在,跳过(--force 覆盖): ${f}`);
2926
3510
  if (result.created.length > 0) {