sillyspec 3.28.2 → 3.28.4

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 (67) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +16 -55
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -1
  3. package/.claude/skills/sillyspec-continue/SKILL.md +8 -38
  4. package/.claude/skills/sillyspec-execute/SKILL.md +10 -2
  5. package/.claude/skills/sillyspec-knowledge/SKILL.md +19 -249
  6. package/.claude/skills/sillyspec-plan/SKILL.md +1 -1
  7. package/.claude/skills/sillyspec-propose/SKILL.md +6 -77
  8. package/.claude/skills/sillyspec-resume/SKILL.md +8 -57
  9. package/.claude/skills/sillyspec-state/SKILL.md +7 -46
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +14 -156
  11. package/AGENTS.md +24 -0
  12. package/INSTRUCTIONS.md +12 -0
  13. package/package.json +1 -1
  14. package/src/archive-delta.js +127 -2
  15. package/src/change-delete.js +2 -1
  16. package/src/change-list.js +7 -2
  17. package/src/change-risk-profile.js +77 -6
  18. package/src/config-cat.js +12 -1
  19. package/src/constants.js +9 -1
  20. package/src/design-facts.js +64 -0
  21. package/src/dispatch/backends/sillyhub-mcp.js +17 -5
  22. package/src/docs-check.js +153 -27
  23. package/src/docs-migrate.js +124 -0
  24. package/src/doctor-diagnostics.js +441 -5
  25. package/src/foreign-declared.js +53 -2
  26. package/src/index.js +353 -28
  27. package/src/module-impact.js +77 -12
  28. package/src/plan-adopt-waves.js +13 -5
  29. package/src/progress/change-registry.js +35 -0
  30. package/src/progress.js +64 -1
  31. package/src/quick-recommend.js +6 -1
  32. package/src/quicklog.js +10 -2
  33. package/src/run/command.js +159 -13
  34. package/src/run/complete-handlers.js +162 -9
  35. package/src/run/complete.js +104 -5
  36. package/src/run/gates.js +116 -8
  37. package/src/run/progress-confirm.js +66 -0
  38. package/src/run/prompt.js +235 -36
  39. package/src/run/quick-audit.js +12 -5
  40. package/src/run/scan-profile.js +20 -2
  41. package/src/run/shared.js +84 -11
  42. package/src/run/stage.js +42 -0
  43. package/src/runtime-hygiene.js +84 -0
  44. package/src/scan-postcheck.js +5 -1
  45. package/src/spec-sync.js +63 -8
  46. package/src/stage-contract.js +83 -3
  47. package/src/stage-review.js +3 -0
  48. package/src/stages/archive.js +4 -4
  49. package/src/stages/brainstorm-auto.js +2 -1
  50. package/src/stages/brainstorm.js +11 -25
  51. package/src/stages/doctor.js +41 -515
  52. package/src/stages/execute.js +10 -14
  53. package/src/stages/plan-postcheck.js +191 -7
  54. package/src/stages/plan.js +7 -27
  55. package/src/stages/quick.js +15 -13
  56. package/src/stages/verify.js +23 -21
  57. package/src/sync-noise.js +158 -0
  58. package/src/sync.js +42 -17
  59. package/src/task-review.js +225 -1
  60. package/src/verify-facts-schema.js +141 -0
  61. package/src/verify-postcheck.js +421 -49
  62. package/src/verify-probes.js +142 -13
  63. package/src/workflow.js +6 -0
  64. package/src/worktree-apply.js +51 -13
  65. package/src/worktree.js +5 -1
  66. package/templates/agents-instruction.md +1 -1
  67. package/.claude/skills/verify-per-user/SKILL.md +0 -6
package/src/index.js CHANGED
@@ -93,13 +93,18 @@ SillySpec CLI — 规范驱动开发工具包
93
93
  sillyspec gate <stage> --change <name> [--json] 机器门控:阶段能否标记完成(只读)
94
94
  sillyspec derive <facet> --change <name> [--json] 单项事实核验(facet: execute-evidence|verify-test|task-reviews|artifacts)
95
95
  sillyspec backfill-reviews --change <name> [--adopt] [--json] 缺 review.json 的 task 生成草稿;--adopt 重算代填已有 review 的 base/head 等机械字段(verdict 保留)
96
+ sillyspec review write --change <name> --task task-NN --spec <verdict> --quality <verdict>
97
+ [--notes ...] [--evidence ...] [--changed-files a,b] [--force]
98
+ 单 task review.json 命令式写入:verdict/notes 你给,base/head/changedFiles/diffPaths CLI 代算
96
99
  sillyspec symbol-impact --change <name> 生成 symbol-impact.md 逐 task <!--TODO--> 骨架(gate 拒绝未替换占位,防骨架直接过门)
97
100
  sillyspec design-init --change <name> [--force] 从 decisions.md 生成 design.md 十三章节骨架(决策追踪表预填;已存在不覆盖)
101
+ sillyspec fourpiece-init --change <name> 生成 proposal/requirements/decisions 骨架(frontmatter+章节+模板占位;已存在不覆盖)
98
102
  sillyspec delta --change <name> [--json] 生成变更 delta.md(Before/Delta/After 三段式聚合;幂等覆盖重跑即刷新)
99
103
  sillyspec next 项目状态探测:输出当前状态 + 下一步命令 + 依据(吸收 continue/resume 手工探测表)
100
104
  sillyspec commit [--json] 智能提交建议:收集 QUICKLOG/已勾 task/阶段产出语义,生成建议 message(只建议不执行)
101
105
  sillyspec verify-probes --change <name> [--init] verify 机械探针(TODO 标记/测试覆盖/API 对账/删除对账);--init 生成 verify-result.md 骨架
102
106
  sillyspec module-impact --change <name> 生成 module-impact.md 骨架(文件×模块归属按 module-map 预填 + 未匹配清单)
107
+ sillyspec module-docs-sync --change <name> [--note ...] diff 归属模块 → sidecar 追加变更索引行 + 卡 updated_at 戳(幂等)
103
108
  sillyspec endpoints extract --change <name> [--task task-NN | --all-tasks] [--dir <dir>|--files <a.py,b.js>] 静态扫描路由装饰器生成 endpoints.json
104
109
  sillyspec endpoints baseline --change <name> [--spec-dir <path>] [--json] 拍变更前端点基线(幂等不覆盖;worktree 内跑自动锚主仓;归档 delta 端点增删 before 侧)
105
110
  sillyspec scan-fix-headers [--project <名>] scan 文档补 author/created_at header(幂等)
@@ -114,6 +119,7 @@ SillySpec CLI — 规范驱动开发工具包
114
119
  (无子命令) 跑诊断,列出问题
115
120
  --align-execute-progress [--change <名>] [--confirm] 基于 plan.md 对齐 execute 派生戳(默认 dry-run)
116
121
  --cleanup-remnant [--confirm] 删除 0 字节空占位 db(默认 dry-run,仅 --confirm 落盘)
122
+ --gc-unstamped-runs [--confirm] 回收已归档变更留下的无 change 戳 execute-runs(默认 dry-run)
117
123
  --dump-db --path <db 路径> dump 指定 db 内容到文件(取证用)
118
124
  --json 结构化诊断 + 落盘 .sillyspec/.runtime/doctor-diagnosis.json
119
125
  sillyspec modules <rebuild | status | migrate>
@@ -565,6 +571,72 @@ async function main() {
565
571
  process.exitCode = exitCode;
566
572
  break;
567
573
  }
574
+ case 'review': {
575
+ // 单 task review.json 命令式写入(2026-09-07 noAI 主流程批次 T2-4):agent 只给判断字段
576
+ // (verdict/notes/evidence),mechanics(executeRunId/base/head/changedFiles/diffPaths)全由
577
+ // CLI 代算——消灭「手拼 JSON→gate 拒→重写」循环与手算 hash 错误面。
578
+ // sillyspec review write --change <名> --task task-NN --spec pass --quality pass
579
+ // [--notes "..."] [--evidence "..."](cannot_verify 必填)
580
+ // [--changed-files a,b](归属切片为空或纯验证任务时显式给出;""=空覆盖)
581
+ // [--base <rev> --head <rev>](无 worktree meta 时显式锡点)[--force]
582
+ const rwSub = filteredArgs[1];
583
+ if (rwSub !== 'write') {
584
+ console.error('用法: sillyspec review write --change <名> --task task-NN --spec <pass|fail|cannot_verify> --quality <verdict>\n [--notes "评审备注"] [--evidence "cannot_verify 证据说明"] [--changed-files a,b] [--base <rev> --head <rev>] [--force] [--json]\n 单 task review.json 命令式写入:verdict/notes 由你给,executeRunId/base/head/changedFiles/diffPaths 由 CLI 从 git + task 卡代算;已存在默认拒覆盖(--force 越过)');
585
+ process.exit(2);
586
+ }
587
+ const rwVal = (flag) => {
588
+ const i = filteredArgs.indexOf(flag);
589
+ return i >= 0 && filteredArgs[i + 1] ? filteredArgs[i + 1] : null;
590
+ };
591
+ const rwChange = rwVal('--change');
592
+ const rwTask = rwVal('--task');
593
+ const rwSpec = rwVal('--spec');
594
+ const rwQuality = rwVal('--quality');
595
+ const rwNotes = rwVal('--notes');
596
+ const rwEvidence = rwVal('--evidence');
597
+ const rwBase = rwVal('--base');
598
+ const rwHead = rwVal('--head');
599
+ const rwFilesIdx = filteredArgs.indexOf('--changed-files');
600
+ // --changed-files 存在即视为显式覆盖(含空串=纯验证任务零 diff 声明);不存在传 null 走归属切片
601
+ const rwFilesOverride = rwFilesIdx >= 0
602
+ ? (filteredArgs[rwFilesIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean)
603
+ : null;
604
+ if (!rwChange || !rwTask || !rwSpec || !rwQuality) {
605
+ console.error('用法: sillyspec review write --change <名> --task task-NN --spec <verdict> --quality <verdict> [--notes ...] [--evidence ...] [--changed-files a,b] [--base <rev> --head <rev>] [--force]');
606
+ process.exit(2);
607
+ }
608
+ assertSafeChangeName(rwChange, '--change 变更名');
609
+ const { writeTaskReview } = await import('./task-review.js');
610
+ const rwPlatformOpts = {};
611
+ const rwResolved = resolvePlatformOpts(dir, specDir);
612
+ if (rwResolved) {
613
+ rwPlatformOpts.specRoot = rwResolved.specRoot;
614
+ if (rwResolved.runtimeRoot) rwPlatformOpts.runtimeRoot = rwResolved.runtimeRoot;
615
+ }
616
+ const rwResult = await writeTaskReview({
617
+ changeName: rwChange, cwd: dir, taskId: rwTask,
618
+ specVerdict: rwSpec, qualityVerdict: rwQuality,
619
+ reviewerNotes: rwNotes || '',
620
+ requiredEvidence: rwEvidence ? [rwEvidence] : [],
621
+ baseOverride: rwBase, headOverride: rwHead,
622
+ changedFilesOverride: rwFilesOverride,
623
+ force: filteredArgs.includes('--force'),
624
+ platformOpts: rwPlatformOpts,
625
+ });
626
+ if (json) {
627
+ console.log(JSON.stringify({ ok: rwResult.ok, ...rwResult }, null, 2));
628
+ } else if (rwResult.ok) {
629
+ console.log(`✅ review.json 已写入:${rwResult.reviewPath}`);
630
+ console.log(` executeRunId: ${rwResult.executeRunId}`);
631
+ for (const w of rwResult.warnings) console.log(`⚠️ ${w}`);
632
+ } else {
633
+ console.error('❌ review write 失败:');
634
+ for (const e of rwResult.errors) console.error(` - ${e}`);
635
+ for (const w of rwResult.warnings) console.error(`⚠️ ${w}`);
636
+ process.exit(1);
637
+ }
638
+ break;
639
+ }
568
640
  case 'backfill-reviews': {
569
641
  // 手动补 task(reopen execute / 直接实现)缺 review.json 的官方入口(坑 verify-archive-flow-pitfalls 坑2)。
570
642
  // 复用 generateTaskReviewDrafts(execute --done 同源兜底,幂等、fail-open):据 git diff base..head + working-tree
@@ -873,10 +945,53 @@ async function main() {
873
945
  writeFileSync(vpReportPath, generateVerifyResultSkeleton(vpResult));
874
946
  console.log(`\n📄 已生成 verify-result.md 骨架: ${vpReportPath}(探针已预填;结论必须写明 PASS/FAIL,留待填会被 gate 判不过)`);
875
947
  }
876
- writeVerifyFacts(join(vpSpecBase, 'changes', vpChange), vpResult, vpChange); // P3b:facts 底稿随 --init 刷新(骨架已存在也刷新,最近快照语义)
948
+ writeVerifyFacts(join(vpSpecBase, 'changes', vpChange), vpResult, vpChange); // P3b:facts 底稿随 --init 刷新(骨架已存在也刷新;v2 起分段合并保留固化段)
949
+ // 2026-09-08-ir-verify-facts R-02:已存在 md 缺证据账/回执槽段时仅追加骨架(幂等二跑
950
+ // 零改动);task 行按 verify-required-evidence.json 预填(存在时)。
951
+ if (existsSync(vpReportPath)) {
952
+ let vpReqItems = []
953
+ try {
954
+ const vpReqPath = join(vpSpecBase, 'changes', vpChange, 'verify-required-evidence.json')
955
+ if (existsSync(vpReqPath)) {
956
+ const vpReq = JSON.parse(readFileSync(vpReqPath, 'utf8'))
957
+ vpReqItems = Array.isArray(vpReq.items) ? vpReq.items.map(it => ({ task: it.task, evidence: it.evidence || [] })) : []
958
+ }
959
+ } catch { /* 读取失败 → 空预填,不阻断 */ }
960
+ const { backfillMissingEvidenceSlots } = await import('./verify-probes.js')
961
+ const vpAdded = backfillMissingEvidenceSlots(vpReportPath, vpReqItems)
962
+ if (vpAdded.added.length > 0) {
963
+ console.log(`📄 已补齐缺失槽段(不触碰既有正文): ${vpAdded.added.join('、')}`)
964
+ }
965
+ }
877
966
  }
878
967
  break;
879
968
  }
969
+ case 'module-docs-sync': {
970
+ // 模块文档 sidecar 同步命令化(2026-09-09 刀批):diff 归属模块 → sidecar 追加变更索引行 +
971
+ // 卡 updated_at 戳——机械部分 CLI 代算,卡片正文语义更新仍归 agent/归档流程。幂等。
972
+ const mdsChangeIdx = args.indexOf('--change');
973
+ const mdsChange = mdsChangeIdx >= 0 && args[mdsChangeIdx + 1] ? args[mdsChangeIdx + 1] : null;
974
+ const mdsNoteIdx = args.indexOf('--note');
975
+ const mdsNote = mdsNoteIdx >= 0 && args[mdsNoteIdx + 1] ? args[mdsNoteIdx + 1] : null;
976
+ if (!mdsChange) {
977
+ console.error('用法: sillyspec module-docs-sync --change <name> [--note "一句话"] [--json]\n diff 归属模块 → sidecar 追加变更索引行 + 卡 updated_at 戳(幂等;卡片正文语义更新仍归 agent)');
978
+ process.exit(2);
979
+ }
980
+ const { syncModuleDocSidecars } = await import('./module-impact.js');
981
+ const mdsResult = syncModuleDocSidecars({ cwd: dir, changeName: mdsChange, note: mdsNote, specDir });
982
+ if (json) {
983
+ console.log(JSON.stringify({ command: 'module-docs-sync', change: mdsChange, ...mdsResult }, null, 2));
984
+ break;
985
+ }
986
+ if (mdsResult.reason) console.log(mdsResult.reason);
987
+ if (mdsResult.synced.length > 0) {
988
+ console.log(String.fromCharCode(9989) + ' sidecar 已同步 ' + mdsResult.synced.length + ' 个模块: ' + mdsResult.synced.join(', '));
989
+ console.log(' 卡 updated_at 已戳;卡片正文语义更新(如需要)仍归归档流程/agent。');
990
+ }
991
+ if (mdsResult.skipped.length > 0) console.log(String.fromCharCode(8505) + ' 已含本变更行跳过: ' + mdsResult.skipped.join(', '));
992
+ if (mdsResult.unmatched.length > 0) console.log(String.fromCharCode(9888) + ' 未匹配模块的文件 ' + mdsResult.unmatched.length + ' 个(不入 sidecar): ' + mdsResult.unmatched.slice(0, 5).join(', '));
993
+ break;
994
+ }
880
995
  case 'plan-adopt-waves': {
881
996
  // plan.md Wave 段一键重排(坑 wave-manual-mismatch-noise,2026-08-24 用户反馈二期):
882
997
  // 手排 Wave 与拓扑比对不一致需手改;依赖方向硬拦(plan-postcheck)落地后给一条零手写出路。
@@ -1244,6 +1359,94 @@ async function main() {
1244
1359
  console.log(' 逐行替换 <!--TODO--> 为结论(无签名级变更也显式写「无」);gate 拒绝未替换的占位行。');
1245
1360
  break;
1246
1361
  }
1362
+ case 'fourpiece-init': {
1363
+ // 四件套骨架预生成(2026-09-09 ql-20260909-003,轮次经济学 §3.2):brainstorm 期 agent
1364
+ // 手写 proposal/requirements/decisions 的 frontmatter/章节骨架是格式返工根源——本命令
1365
+ // 逐文件生成骨架(已存在不覆盖),agent 只填语义。与 design-init 同族(幂等)。
1366
+ const fpChangeIdx = args.indexOf('--change');
1367
+ const fpChange = fpChangeIdx >= 0 && args[fpChangeIdx + 1] ? args[fpChangeIdx + 1] : null;
1368
+ if (!fpChange) {
1369
+ console.error('用法: sillyspec fourpiece-init --change <name> [--json] [--spec-dir <path>]\n 生成 proposal.md/requirements.md/decisions.md 骨架(frontmatter+章节标题+模板占位;已存在不覆盖;design.md 归 design-init)');
1370
+ process.exit(2);
1371
+ }
1372
+ const fpSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
1373
+ const fpDir = join(fpSpecBase, 'changes', fpChange);
1374
+ mkdirSync(fpDir, { recursive: true });
1375
+ const fpNow = new Date();
1376
+ const fpDate = fpNow.toISOString().slice(0, 10);
1377
+ const fpStamp = fpNow.toISOString().slice(0, 19).replace('T', ' ');
1378
+ const generated = [];
1379
+ const mk = (fname, body) => {
1380
+ const p = join(fpDir, fname);
1381
+ if (existsSync(p)) { console.log(`ℹ️ ${fname} 已存在,不覆盖`); return; }
1382
+ writeFileSync(p, body);
1383
+ generated.push(fname);
1384
+ console.log(`✅ 已生成 ${fname} 骨架: ${p}`);
1385
+ };
1386
+ mk('proposal.md', `---
1387
+ author: qinyi
1388
+ created_at: ${fpStamp}
1389
+ ---
1390
+ # 提案书(Proposal)
1391
+
1392
+ ## 动机
1393
+ <!--TODO: 为什么做、解决什么核心问题-->
1394
+
1395
+ ## 关键问题
1396
+ <!--TODO: 为什么现有方案不够(2-3 个痛点)-->
1397
+
1398
+ ## 变更范围
1399
+ <!--TODO: 本次做什么-->
1400
+
1401
+ ## 不在范围内(显式清单)
1402
+ - <!--TODO: 不做 X-->
1403
+
1404
+ ## 成功标准(可验证)
1405
+ - <!--TODO: 可验证条目-->
1406
+ `);
1407
+ mk('requirements.md', `---
1408
+ author: qinyi
1409
+ created_at: ${fpStamp}
1410
+ ---
1411
+ # 需求规格(Requirements)
1412
+
1413
+ ## 角色
1414
+ | 角色 | 说明 |
1415
+ |---|---|
1416
+ | <!--TODO--> | <!--TODO--> |
1417
+
1418
+ ## 功能需求
1419
+
1420
+ ### FR-01: <!--TODO-->
1421
+ Given <!--TODO-->
1422
+ When <!--TODO-->
1423
+ Then <!--TODO-->
1424
+
1425
+ ## 非功能需求
1426
+ - 兼容性:<!--TODO-->
1427
+
1428
+ ## 决策覆盖矩阵(如存在 decisions.md)
1429
+ | 决策 ID | 覆盖的 FR | 说明 |
1430
+ |---|---|---|
1431
+ `);
1432
+ mk('decisions.md', `---
1433
+ author: qinyi
1434
+ created_at: ${fpStamp}
1435
+ change: ${fpChange}
1436
+ ---
1437
+
1438
+ # 决策记录(Decisions)
1439
+
1440
+ <!-- 增量落盘:每解决一个有实现影响的问题当场追加一条(格式见 brainstorm Step 3 模板);幂等按 D-xxx@vN 判重 -->
1441
+ `);
1442
+ if (json) {
1443
+ console.log(JSON.stringify({ command: 'fourpiece-init', change: fpChange, generated }, null, 2));
1444
+ } else if (generated.length > 0) {
1445
+ console.log(`
1446
+ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语义内容(骨架勿手删章节);design.md 用 sillyspec design-init。`);
1447
+ }
1448
+ break;
1449
+ }
1247
1450
  case 'design-init': {
1248
1451
  // design.md 十三章节骨架生成(change: 2026-09-07-ir-stage-p3c,task-02,FR-03):agent 此前
1249
1452
  // 从零手写整份设计文档,章节漏项到 stage-contract/plan 门禁才暴露。本命令从 decisions.md
@@ -1328,16 +1531,29 @@ async function main() {
1328
1531
  const { resolveRuntimeRoot } = await import('./run/shared.js');
1329
1532
  const dlRuntimeRoot = resolveRuntimeRoot(dlPlatformOpts, dlSpecBase);
1330
1533
  const { buildDeltaReport } = await import('./archive-delta.js');
1331
- // CLI 不收 --project:project=null 走 loadModuleMap 降级路径(报告内「无 module-map」注记),
1332
- // 与归档确认步自动生成(progress.project 有值时命中 module-map)形成口径互补。
1333
- const dlMarkdown = buildDeltaReport({
1534
+ // project 同口径修复(change: 2026-09-07-ir-hardening D-005@v1):此前硬编码 null →
1535
+ // 手动补跑恒走 loadModuleMap 降级(报告恒「无 module-map」注记)。改从 progress.project
1536
+ // 取值(与归档确认步自动生成同源),null 兜底降级不变。
1537
+ let dlProject = null;
1538
+ try {
1539
+ const { ProgressManager } = await import('./progress.js');
1540
+ const dlPm = new ProgressManager({ specDir: dlSpecBase });
1541
+ const dlProgress = dlPm.read(cwd, dlChange);
1542
+ dlProject = dlProgress?.project || null;
1543
+ } catch { /* 读 progress 失败 → null 兜底降级(fail-soft) */ }
1544
+ const dlResult = buildDeltaReport({
1334
1545
  changeDir: dlChangeDir,
1335
1546
  specRoot: dlSpecBase,
1336
- project: null,
1547
+ project: dlProject,
1337
1548
  runtimeRoot: dlRuntimeRoot,
1549
+ withSummary: true,
1338
1550
  });
1551
+ const dlMarkdown = dlResult.markdown;
1339
1552
  const dlPath = join(dlChangeDir, 'delta.md');
1340
1553
  writeFileSync(dlPath, dlMarkdown);
1554
+ // 回灌 sidecar(2026-09-07-ir-hardening D-006):fail-soft,写失败不阻断 delta 输出
1555
+ const { writeLastDeltaSidecar } = await import('./archive-delta.js');
1556
+ writeLastDeltaSidecar(dlRuntimeRoot, dlResult);
1341
1557
  if (json) {
1342
1558
  console.log(JSON.stringify({ command: 'delta', change: dlChange, ok: true, path: dlPath, written: true }, null, 2));
1343
1559
  break;
@@ -1419,8 +1635,40 @@ async function main() {
1419
1635
  case 'docs': {
1420
1636
  const docsSubCmd = filteredArgs[1];
1421
1637
  if (docsSubCmd === 'migrate') {
1422
- const { migrateDocs } = await import('./migrate.js');
1423
- migrateDocs(dir);
1638
+ // docs migrate 双形态(2026-09-08-docs-fix-capability / D-002+D-004):
1639
+ // 有 --from/--to → 新语义(路径引用前缀迁移,薄模块 docs-migrate.js)
1640
+ // 无 flag → 旧语义(.sillyspec 结构一次性迁移 migrateDocs,兼容保留)
1641
+ const rawMigrateArgs = filteredArgs.slice(2);
1642
+ let mFrom = null, mTo = null, mApply = false;
1643
+ for (let i = 0; i < rawMigrateArgs.length; i++) {
1644
+ const a = rawMigrateArgs[i];
1645
+ if (a === '--from' && rawMigrateArgs[i + 1] !== undefined) { mFrom = rawMigrateArgs[i + 1]; i++; }
1646
+ else if (a === '--to' && rawMigrateArgs[i + 1] !== undefined) { mTo = rawMigrateArgs[i + 1]; i++; }
1647
+ else if (a === '--apply') { mApply = true; }
1648
+ else if (a === '--from' || a === '--to') {
1649
+ console.error(`❌ docs migrate: ${a} 缺值(如 --from "modules/" --to "backend/app/modules/")`);
1650
+ process.exit(2);
1651
+ } else if (a.startsWith('--')) {
1652
+ console.error(`❌ docs migrate: 未知 flag「${a}」。已知 flag:--from --to --apply`);
1653
+ process.exit(2);
1654
+ }
1655
+ }
1656
+ if (mFrom !== null || mTo !== null) {
1657
+ if (mFrom === null || mTo === null) {
1658
+ console.error('❌ docs migrate: --from 与 --to 必须成对提供');
1659
+ process.exit(2);
1660
+ }
1661
+ const { runDocsMigrate } = await import('./docs-migrate.js');
1662
+ const result = runDocsMigrate({ projectRoot: dir, from: mFrom, to: mTo, apply: mApply });
1663
+ // exit code 契约:0=dry-run 零计划或 --apply 后 postCheck 全绿;1=--apply 后仍失效或
1664
+ // unverified>0;dry-run 有计划恒 0(预览不判成败)
1665
+ if (!mApply) { process.exit(0); }
1666
+ const fail = result.unverified > 0 || (result.postCheck && result.postCheck.invalid > 0);
1667
+ process.exit(fail ? 1 : 0);
1668
+ } else {
1669
+ const { migrateDocs } = await import('./migrate.js');
1670
+ migrateDocs(dir);
1671
+ }
1424
1672
  } else if (docsSubCmd === 'check') {
1425
1673
  // docs check(2026-08-15 docs-check-productize D-6):文档 file:line 引用校验。
1426
1674
  // exit code 三档(D-003):0 全绿 / 1 存在无效引用 / 2 配置/IO 错误(DocsCheckConfigError)。
@@ -1433,14 +1681,14 @@ async function main() {
1433
1681
  // 写回);--dry-run 预览不写盘,单独传(无 --fix)也走预览路径——design §5.2 行为矩阵
1434
1682
  // --dry-run 列独立存在(「报告修复预览 + exit 1」),即 dryRun 置位 = fix 语义自动生效
1435
1683
  // 但零写盘(本口径记录于此,防歧义)。
1436
- const BARE_FLAGS = ['--suggest', '--fix', '--dry-run'];
1684
+ const BARE_FLAGS = ['--fix', '--dry-run', '--no-exempt'];
1437
1685
  const PAIRED_FLAGS = ['--paths'];
1438
1686
  const rawDocsArgs = filteredArgs.slice(2);
1439
1687
  const docsCheckFlags = [];
1440
1688
  let cliPaths = null;
1441
- let suggest = false;
1442
1689
  let fix = false;
1443
1690
  let dryRun = false;
1691
+ let noExempt = false;
1444
1692
  for (let i = 0; i < rawDocsArgs.length; i++) {
1445
1693
  const a = rawDocsArgs[i];
1446
1694
  if (a === '--paths' && rawDocsArgs[i + 1] !== undefined) {
@@ -1450,9 +1698,9 @@ async function main() {
1450
1698
  console.error('❌ docs check: --paths 缺值(逗号分隔 glob,如 --paths "docs/**/*.md")');
1451
1699
  process.exit(2);
1452
1700
  } else if (BARE_FLAGS.includes(a)) {
1453
- if (a === '--suggest') suggest = true;
1454
- else if (a === '--fix') fix = true;
1701
+ if (a === '--fix') fix = true;
1455
1702
  else if (a === '--dry-run') dryRun = true;
1703
+ else if (a === '--no-exempt') noExempt = true;
1456
1704
  } else if (a.startsWith('--')) {
1457
1705
  console.error(`❌ docs check: 未知 flag「${a}」。已知 flag:${[...BARE_FLAGS, ...PAIRED_FLAGS].join(' ')}(--json 为全局 flag)`);
1458
1706
  process.exit(2);
@@ -1471,6 +1719,7 @@ async function main() {
1471
1719
  skip: cfg.skip,
1472
1720
  keywordAssert: cfg.keywordAssert,
1473
1721
  crossRepoRoots: cfg.crossRepoRoots,
1722
+ exempt: !noExempt,
1474
1723
  });
1475
1724
  // task-03 修复链路(FR-04,platform-map-auto-anchors):fixActive 才构造 fixes——
1476
1725
  // 仅 fix.fixable===true 且 newLine 为整数的条目可重锚;newRef = ref 的行号部分替换为
@@ -1491,6 +1740,31 @@ async function main() {
1491
1740
  }
1492
1741
  }
1493
1742
  const fixResult = fixActive ? applyFixes(dir, fixes, dryRun ? { dryRun: true } : {}) : null;
1743
+ // 修复回执(change: 2026-09-07-ir-hardening D-007@v1,FR-04):--fix 落盘后用与首查
1744
+ // 完全相同的口径重跑一次失效收集,得 after 计数——「前 N → 重锚 M → 后 K(消除 N-K)」
1745
+ // 即修复建议被机器证明消除了多少诊断(acceptsFix 最小件)。仅 --fix(写盘)路径;
1746
+ // --dry-run 不写盘重跑无意义,receipt 为 null;非 fix 路径零变化。
1747
+ let fixReceipt = null;
1748
+ if (fix && !dryRun && fixResult && fixResult.applied > 0) {
1749
+ try {
1750
+ const after = runDocsCheck({
1751
+ projectRoot: dir,
1752
+ docs: docsCheckFlags.length > 0 ? docsCheckFlags : null,
1753
+ paths: cliPaths || cfg.paths,
1754
+ skip: cfg.skip,
1755
+ keywordAssert: cfg.keywordAssert,
1756
+ crossRepoRoots: cfg.crossRepoRoots,
1757
+ });
1758
+ fixReceipt = {
1759
+ invalidBefore: result.invalid.length,
1760
+ reAnchored: fixResult.applied,
1761
+ invalidAfter: after.invalid.length,
1762
+ eliminated: result.invalid.length - after.invalid.length,
1763
+ };
1764
+ } catch (e) {
1765
+ console.warn(`⚠️ 修复回执重算失败(不影响修复本身):${e && e.message ? e.message : e}`);
1766
+ }
1767
+ }
1494
1768
  // 变更名提名 advisory(2026-08-31 变更关联审计 P2):铁律同决策规则族——只 warn,
1495
1769
  // 不进 invalid、不影响下方 exit code 与 docs gate。降级容错:advisory 自身异常
1496
1770
  // 不拖垮主检查(try-catch 吞并提示)。
@@ -1503,7 +1777,7 @@ async function main() {
1503
1777
  if (json) {
1504
1778
  // --fix 与 --json 组合保持 stdout 纯 JSON 可解析(constraints):修复统计作 result.fixReport
1505
1779
  // 附加后整体 stringify,主体仍是 runDocsCheck 返回(invalid[].fix 是 task-01 设计内增量)
1506
- if (fixResult) result.fixReport = { applied: fixResult.applied, skipped: fixResult.skipped.length, dryRun };
1780
+ if (fixResult) result.fixReport = { applied: fixResult.applied, skipped: fixResult.skipped.length, dryRun, ...(fixReceipt ? { receipt: fixReceipt } : {}) };
1507
1781
  if (changeNameReport) result.changeNameReport = changeNameReport;
1508
1782
  console.log(JSON.stringify(result, null, 2));
1509
1783
  } else {
@@ -1514,7 +1788,10 @@ async function main() {
1514
1788
  if (result.ok) {
1515
1789
  console.log(`✅ docs check: ${result.total} 处引用全通过(其中 ${result.kwChecked} 处带关键词断言)`);
1516
1790
  } else {
1517
- console.error(`\n❌ docs check: ${result.invalid.length}/${result.total} 处引用失效:`);
1791
+ // FR-5(D-005,2026-09-08-docs-fix-capability):报告内容统一 stdout(脚本 capture_output
1792
+ // 只读 stdout 可捕获);stderr 仅留 ⚠️ 诊断(console.warn)与配置错误(exit 2 路径)。
1793
+ // 文本逐字不变,只改通道。
1794
+ console.log(`\n❌ docs check: ${result.invalid.length}/${result.total} 处引用失效:`);
1518
1795
  let appliedLeft = fixResult ? fixResult.applied : 0;
1519
1796
  const skippedQueue = fixResult ? [...fixResult.skipped] : [];
1520
1797
  for (const inv of result.invalid) {
@@ -1522,26 +1799,31 @@ async function main() {
1522
1799
  if (newRef !== undefined && appliedLeft > 0) {
1523
1800
  // 已应用(dry-run 为将应用):归因按构造顺序消耗 applied 计数——写回 skipped 仅
1524
1801
  // 防御路径(文档消失/行内失配)触发,CLI 正常流 fixable 必然落位
1525
- console.error(` ✅ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${newRef}${dryRun ? '(dry-run 未写盘)' : ''}`);
1802
+ console.log(` ✅ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${newRef}${dryRun ? '(dry-run 未写盘)' : ''}`);
1526
1803
  appliedLeft--;
1527
1804
  } else if (newRef !== undefined) {
1528
1805
  const s = skippedQueue.shift();
1529
- console.error(` ⚠️ [${inv.doc}:L${inv.docLine}] ${inv.ref} → 写回跳过:${s ? s.reason : '未知原因'}`);
1806
+ console.log(` ⚠️ [${inv.doc}:L${inv.docLine}] ${inv.ref} → 写回跳过:${s ? s.reason : '未知原因'}`);
1530
1807
  } else if (fixResult) {
1531
1808
  // needs-manual:多命中歧义/零命中/无 token——fix.reason 含候选行号,交人工
1532
- console.error(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${(inv.fix && inv.fix.reason) || inv.reason}(待人工)`);
1809
+ console.log(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${(inv.fix && inv.fix.reason) || inv.reason}(待人工)`);
1533
1810
  } else {
1534
- console.error(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${inv.reason}`);
1811
+ console.log(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${inv.reason}`);
1535
1812
  }
1536
- if (suggest && inv.suggest && inv.suggest.length > 0) {
1537
- console.error(` 💡 候选行号: ${inv.suggest.join(', ')}(token 命中行,人工确认后更新文档锚)`);
1813
+ if (inv.suggest && inv.suggest.length > 0) {
1814
+ // 2026-09-07-ir-hardening task-08:原 --suggest 旗标删除(no-op 首选路径退役),
1815
+ // 候选行号提示改为 needs-manual 默认输出——旗标唯一用途,默认开更有信息量
1816
+ console.log(` 💡 候选行号: ${inv.suggest.join(', ')}(token 命中行,人工确认后更新文档锚)`);
1538
1817
  }
1539
1818
  }
1540
1819
  if (fixResult) {
1541
- console.error(`\n🔧 重锚报告:${fixResult.applied} 处已${dryRun ? '预览' : '改写'}${dryRun ? '(dry-run 未写盘)' : ''}、${result.invalid.length - fixes.length} 处待人工${fixResult.skipped.length > 0 ? `、${fixResult.skipped.length} 处写回跳过` : ''}。`);
1820
+ console.log(`\n🔧 重锚报告:${fixResult.applied} 处已${dryRun ? '预览' : '改写'}${dryRun ? '(dry-run 未写盘)' : ''}、${result.invalid.length - fixes.length} 处待人工${fixResult.skipped.length > 0 ? `、${fixResult.skipped.length} 处写回跳过` : ''}。`);
1821
+ if (fixReceipt) {
1822
+ console.log(`🔁 修复回执(机器复跑证明):修复前 ${fixReceipt.invalidBefore} 处失效 → 重锚 ${fixReceipt.reAnchored} 处 → 修复后 ${fixReceipt.invalidAfter} 处(消除 ${fixReceipt.eliminated} 处)。`);
1542
1823
  }
1543
- console.error(`\n修复指引:行号漂移 → 更新文档行号到当前源码;文件删改名 → 更新引用路径;`);
1544
- console.error(`关键词缺失但行号正确 → 确认符号是否改名,改文档 token 或行号。`);
1824
+ }
1825
+ console.log(`\n修复指引:行号漂移 → 更新文档行号到当前源码;文件删改名 → 更新引用路径;`);
1826
+ console.log(`关键词缺失但行号正确 → 确认符号是否改名,改文档 token 或行号。`);
1545
1827
  }
1546
1828
  // 变更名提名 advisory 段(主结果之后、exit 之前;不参与 ok/invalid 与 exit code)。
1547
1829
  // 零输出原则(docs-check-fix 契约:无新 flag 时 stdout 逐字节一致;决策规则族同款
@@ -1622,7 +1904,9 @@ async function main() {
1622
1904
  }
1623
1905
  process.exit(g.exitCode);
1624
1906
  } else {
1625
- console.log('用法: sillyspec docs migrate | sillyspec docs check [--paths <glob,...>] [--json] [--suggest] [--fix] [--dry-run] | sillyspec docs gate [--init-baseline] [--json]');
1907
+ console.log('用法: sillyspec docs migrate [--from <旧前缀> --to <新前缀> [--apply]] | sillyspec docs check [--paths <glob,...>] [--json] [--fix] [--dry-run] [--no-exempt] | sillyspec docs gate [--init-baseline] [--json]');
1908
+ console.log(' docs migrate:--from/--to 路径前缀迁移(默认 dry-run,--apply 写盘+自动复核);无 flag = 旧 .sillyspec 结构迁移');
1909
+ console.log(' docs check --no-exempt:archive/finished 与 doc_type: snapshot 文档照常校验(缺省豁免)');
1626
1910
  process.exit(2);
1627
1911
  }
1628
1912
  break;
@@ -1660,9 +1944,10 @@ async function main() {
1660
1944
  // slice(1) 去掉的是 'run' 字面量,这里 command 本身就是 stage 名不能丢。
1661
1945
  case 'doctor': {
1662
1946
  const doctorEffectiveDir = specDir ? dir : resolveEffectiveDir(dir);
1663
- // 执行流:--cleanup-remnant / --cleanup-ghosts / --dump-db(结构化诊断之外的修复/取证动作)
1947
+ // 执行流:--cleanup-remnant / --cleanup-ghosts / --gc-unstamped-runs / --dump-db(结构化诊断之外的修复/取证动作)
1664
1948
  const cleanupRemnant = filteredArgs.includes('--cleanup-remnant');
1665
1949
  const cleanupGhosts = filteredArgs.includes('--cleanup-ghosts');
1950
+ const gcUnstampedRuns = filteredArgs.includes('--gc-unstamped-runs');
1666
1951
  const dumpDbFlag = filteredArgs.includes('--dump-db');
1667
1952
  const doctorConfirm = filteredArgs.includes('--confirm');
1668
1953
  const pathIdx = filteredArgs.indexOf('--path');
@@ -1768,6 +2053,29 @@ async function main() {
1768
2053
  process.exitCode = r.errors.length > 0 ? 1 : 0;
1769
2054
  break;
1770
2055
  }
2056
+ if (gcUnstampedRuns) {
2057
+ // ql-20260908-006-5f04:无戳 execute-runs 一次性清扫。默认 dry-run,--confirm 才删。
2058
+ // 不进 archive 热路径(pruneArchivedChangeRuntime 故意不猜无戳 run)。
2059
+ const { gcUnstampedExecuteRuns } = await import('./doctor-diagnostics.js');
2060
+ const r = await gcUnstampedExecuteRuns({ cwd: doctorEffectiveDir, specDir, confirm: doctorConfirm });
2061
+ if (json) {
2062
+ console.log(JSON.stringify(r, null, 2));
2063
+ } else {
2064
+ const verb = doctorConfirm ? '已删除' : '待删除(dry-run)';
2065
+ console.log(`🧹 无戳 execute-runs(仅已归档变更、交叉核验通过)${verb}:${r.count} 个 [runtime: ${r.runtime_root || '(无)'}]`);
2066
+ for (const c of r.candidates || []) {
2067
+ console.log(` ${doctorConfirm ? '✅' : '-'} ${c.runId} ← ${c.owner}(via ${c.via})`);
2068
+ }
2069
+ for (const s of r.skipped || []) console.log(` ⏭️ ${s.runId}(${s.reason})`);
2070
+ for (const e of r.errors || []) console.log(` ❌ ${e.runId || ''}: ${e.error}`);
2071
+ if (!doctorConfirm && r.count > 0) {
2072
+ console.log(`\n加 --confirm 执行删除(只删上面列出的无戳 run,有 change 戳 / 命中活跃变更 / 归属不明一律不动)。`);
2073
+ }
2074
+ if (r.reason) console.log(`ℹ️ ${r.reason}`);
2075
+ }
2076
+ process.exitCode = (r.errors && r.errors.length > 0) ? 1 : 0;
2077
+ break;
2078
+ }
1771
2079
  if (dumpDbFlag) {
1772
2080
  if (!dbPath) { console.error('❌ --dump-db 需要 --path <db 路径>'); process.exitCode = 2; break; }
1773
2081
  const { dumpDb } = await import('./doctor-diagnostics.js');
@@ -1794,9 +2102,21 @@ async function main() {
1794
2102
  process.exitCode = output.overall_status === 'pass' ? 0 : 1;
1795
2103
  break;
1796
2104
  }
1797
- // 否则:保持原有 prompt 驱动的 bash 自检流程
1798
- const { runCommand } = await import('./run.js');
1799
- await runCommand([command, ...filteredArgs.slice(1)], doctorEffectiveDir, specDir, { json });
2105
+ // 2026-09-09-doctor-noai:--status 拦截(审查 B-03:等价只读渲染——阶段进度视图,
2106
+ // 零副作用);非 --json 其余形态改道直跑诊断 + renderDoctorSummary(不走
2107
+ // runCommand/initChange/不刷 lastActive——只读零副作用承诺保真)。
2108
+ if (filteredArgs.includes('--status')) {
2109
+ // 等价只读渲染:复用 progress show(cwd 无活跃变更时提示无——与原只读短路视图同语义级)
2110
+ const pm = new ProgressManager({ specDir: resolvePlatformSpecDir(dir, specDir) });
2111
+ pm.show(doctorEffectiveDir);
2112
+ break;
2113
+ }
2114
+ {
2115
+ const { runDoctorDiagnostics, renderDoctorSummary, writeDoctorDiagnosis } = await import('./doctor-diagnostics.js')
2116
+ const diag = await runDoctorDiagnostics({ cwd: doctorEffectiveDir })
2117
+ console.log(renderDoctorSummary(diag))
2118
+ try { writeDoctorDiagnosis(diag, resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec')) } catch { /* fail-soft */ }
2119
+ }
1800
2120
  break;
1801
2121
  }
1802
2122
  case 'scan': {
@@ -2770,7 +3090,12 @@ checkbox 行;depends_on 自动反填行内注解 "(depends_on: task-01,02)";
2770
3090
 
2771
3091
  const { cmdTaskcard } = await import('./taskcard.js');
2772
3092
  try {
2773
- const result = cmdTaskcard(tcName, { cwd: dir, specDir, taskIds, title: titleVal, titleZh: titleZhVal, force, sets });
3093
+ // 坑 platform-sync-progress-rollback-and-db-corruption 坑4:taskcard 此前只走
3094
+ // resolveSpecDir(祖先遍历本地 .sillyspec),不感知平台接管指针——平台模式下
3095
+ // 仓库根裸跑报「变更目录不存在」,需显式 --spec-dir(与 run/plan 系列不一致)。
3096
+ // 对齐 endpoints 等命令:先 resolvePlatformSpecDir(指针 fail-closed 语义同源)。
3097
+ const tcPlatformSpecDir = resolvePlatformSpecDir(dir, specDir) || specDir;
3098
+ const result = cmdTaskcard(tcName, { cwd: dir, specDir: tcPlatformSpecDir, taskIds, title: titleVal, titleZh: titleZhVal, force, sets });
2774
3099
  for (const f of result.created) console.log(`✅ 已生成: ${f}`);
2775
3100
  for (const f of result.skipped) console.log(`⏭️ 已存在,跳过(--force 覆盖): ${f}`);
2776
3101
  if (result.created.length > 0) {