sillyspec 3.28.2 → 3.28.3

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 (41) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +54 -93
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  3. package/.claude/skills/sillyspec-continue/SKILL.md +17 -47
  4. package/.claude/skills/sillyspec-execute/SKILL.md +222 -214
  5. package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -270
  6. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  7. package/.claude/skills/sillyspec-propose/SKILL.md +16 -87
  8. package/.claude/skills/sillyspec-resume/SKILL.md +21 -70
  9. package/.claude/skills/sillyspec-state/SKILL.md +25 -64
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +33 -175
  11. package/package.json +1 -1
  12. package/src/archive-delta.js +45 -2
  13. package/src/constants.js +6 -0
  14. package/src/design-facts.js +64 -0
  15. package/src/dispatch/backends/sillyhub-mcp.js +17 -5
  16. package/src/docs-check.js +119 -27
  17. package/src/docs-migrate.js +124 -0
  18. package/src/index.js +172 -22
  19. package/src/progress/change-registry.js +19 -0
  20. package/src/progress.js +2 -0
  21. package/src/run/command.js +129 -4
  22. package/src/run/complete-handlers.js +10 -3
  23. package/src/run/complete.js +59 -5
  24. package/src/run/gates.js +22 -2
  25. package/src/run/progress-confirm.js +66 -0
  26. package/src/run/prompt.js +139 -31
  27. package/src/run/quick-audit.js +12 -5
  28. package/src/run/scan-profile.js +20 -2
  29. package/src/run/shared.js +10 -5
  30. package/src/run/stage.js +7 -0
  31. package/src/scan-postcheck.js +5 -1
  32. package/src/stages/brainstorm-auto.js +2 -1
  33. package/src/stages/brainstorm.js +9 -17
  34. package/src/stages/execute.js +10 -14
  35. package/src/stages/plan.js +2 -1
  36. package/src/stages/quick.js +4 -2
  37. package/src/stages/verify.js +13 -15
  38. package/src/task-review.js +200 -0
  39. package/src/verify-postcheck.js +164 -30
  40. package/templates/agents-instruction.md +1 -1
  41. package/.claude/skills/verify-per-user/SKILL.md +0 -6
package/src/index.js CHANGED
@@ -93,6 +93,9 @@ 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 十三章节骨架(决策追踪表预填;已存在不覆盖)
98
101
  sillyspec delta --change <name> [--json] 生成变更 delta.md(Before/Delta/After 三段式聚合;幂等覆盖重跑即刷新)
@@ -565,6 +568,72 @@ async function main() {
565
568
  process.exitCode = exitCode;
566
569
  break;
567
570
  }
571
+ case 'review': {
572
+ // 单 task review.json 命令式写入(2026-09-07 noAI 主流程批次 T2-4):agent 只给判断字段
573
+ // (verdict/notes/evidence),mechanics(executeRunId/base/head/changedFiles/diffPaths)全由
574
+ // CLI 代算——消灭「手拼 JSON→gate 拒→重写」循环与手算 hash 错误面。
575
+ // sillyspec review write --change <名> --task task-NN --spec pass --quality pass
576
+ // [--notes "..."] [--evidence "..."](cannot_verify 必填)
577
+ // [--changed-files a,b](归属切片为空或纯验证任务时显式给出;""=空覆盖)
578
+ // [--base <rev> --head <rev>](无 worktree meta 时显式锡点)[--force]
579
+ const rwSub = filteredArgs[1];
580
+ if (rwSub !== 'write') {
581
+ 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 越过)');
582
+ process.exit(2);
583
+ }
584
+ const rwVal = (flag) => {
585
+ const i = filteredArgs.indexOf(flag);
586
+ return i >= 0 && filteredArgs[i + 1] ? filteredArgs[i + 1] : null;
587
+ };
588
+ const rwChange = rwVal('--change');
589
+ const rwTask = rwVal('--task');
590
+ const rwSpec = rwVal('--spec');
591
+ const rwQuality = rwVal('--quality');
592
+ const rwNotes = rwVal('--notes');
593
+ const rwEvidence = rwVal('--evidence');
594
+ const rwBase = rwVal('--base');
595
+ const rwHead = rwVal('--head');
596
+ const rwFilesIdx = filteredArgs.indexOf('--changed-files');
597
+ // --changed-files 存在即视为显式覆盖(含空串=纯验证任务零 diff 声明);不存在传 null 走归属切片
598
+ const rwFilesOverride = rwFilesIdx >= 0
599
+ ? (filteredArgs[rwFilesIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean)
600
+ : null;
601
+ if (!rwChange || !rwTask || !rwSpec || !rwQuality) {
602
+ console.error('用法: sillyspec review write --change <名> --task task-NN --spec <verdict> --quality <verdict> [--notes ...] [--evidence ...] [--changed-files a,b] [--base <rev> --head <rev>] [--force]');
603
+ process.exit(2);
604
+ }
605
+ assertSafeChangeName(rwChange, '--change 变更名');
606
+ const { writeTaskReview } = await import('./task-review.js');
607
+ const rwPlatformOpts = {};
608
+ const rwResolved = resolvePlatformOpts(dir, specDir);
609
+ if (rwResolved) {
610
+ rwPlatformOpts.specRoot = rwResolved.specRoot;
611
+ if (rwResolved.runtimeRoot) rwPlatformOpts.runtimeRoot = rwResolved.runtimeRoot;
612
+ }
613
+ const rwResult = await writeTaskReview({
614
+ changeName: rwChange, cwd: dir, taskId: rwTask,
615
+ specVerdict: rwSpec, qualityVerdict: rwQuality,
616
+ reviewerNotes: rwNotes || '',
617
+ requiredEvidence: rwEvidence ? [rwEvidence] : [],
618
+ baseOverride: rwBase, headOverride: rwHead,
619
+ changedFilesOverride: rwFilesOverride,
620
+ force: filteredArgs.includes('--force'),
621
+ platformOpts: rwPlatformOpts,
622
+ });
623
+ if (json) {
624
+ console.log(JSON.stringify({ ok: rwResult.ok, ...rwResult }, null, 2));
625
+ } else if (rwResult.ok) {
626
+ console.log(`✅ review.json 已写入:${rwResult.reviewPath}`);
627
+ console.log(` executeRunId: ${rwResult.executeRunId}`);
628
+ for (const w of rwResult.warnings) console.log(`⚠️ ${w}`);
629
+ } else {
630
+ console.error('❌ review write 失败:');
631
+ for (const e of rwResult.errors) console.error(` - ${e}`);
632
+ for (const w of rwResult.warnings) console.error(`⚠️ ${w}`);
633
+ process.exit(1);
634
+ }
635
+ break;
636
+ }
568
637
  case 'backfill-reviews': {
569
638
  // 手动补 task(reopen execute / 直接实现)缺 review.json 的官方入口(坑 verify-archive-flow-pitfalls 坑2)。
570
639
  // 复用 generateTaskReviewDrafts(execute --done 同源兜底,幂等、fail-open):据 git diff base..head + working-tree
@@ -1328,16 +1397,29 @@ async function main() {
1328
1397
  const { resolveRuntimeRoot } = await import('./run/shared.js');
1329
1398
  const dlRuntimeRoot = resolveRuntimeRoot(dlPlatformOpts, dlSpecBase);
1330
1399
  const { buildDeltaReport } = await import('./archive-delta.js');
1331
- // CLI 不收 --project:project=null 走 loadModuleMap 降级路径(报告内「无 module-map」注记),
1332
- // 与归档确认步自动生成(progress.project 有值时命中 module-map)形成口径互补。
1333
- const dlMarkdown = buildDeltaReport({
1400
+ // project 同口径修复(change: 2026-09-07-ir-hardening D-005@v1):此前硬编码 null →
1401
+ // 手动补跑恒走 loadModuleMap 降级(报告恒「无 module-map」注记)。改从 progress.project
1402
+ // 取值(与归档确认步自动生成同源),null 兜底降级不变。
1403
+ let dlProject = null;
1404
+ try {
1405
+ const { ProgressManager } = await import('./progress.js');
1406
+ const dlPm = new ProgressManager({ specDir: dlSpecBase });
1407
+ const dlProgress = dlPm.read(cwd, dlChange);
1408
+ dlProject = dlProgress?.project || null;
1409
+ } catch { /* 读 progress 失败 → null 兜底降级(fail-soft) */ }
1410
+ const dlResult = buildDeltaReport({
1334
1411
  changeDir: dlChangeDir,
1335
1412
  specRoot: dlSpecBase,
1336
- project: null,
1413
+ project: dlProject,
1337
1414
  runtimeRoot: dlRuntimeRoot,
1415
+ withSummary: true,
1338
1416
  });
1417
+ const dlMarkdown = dlResult.markdown;
1339
1418
  const dlPath = join(dlChangeDir, 'delta.md');
1340
1419
  writeFileSync(dlPath, dlMarkdown);
1420
+ // 回灌 sidecar(2026-09-07-ir-hardening D-006):fail-soft,写失败不阻断 delta 输出
1421
+ const { writeLastDeltaSidecar } = await import('./archive-delta.js');
1422
+ writeLastDeltaSidecar(dlRuntimeRoot, dlResult);
1341
1423
  if (json) {
1342
1424
  console.log(JSON.stringify({ command: 'delta', change: dlChange, ok: true, path: dlPath, written: true }, null, 2));
1343
1425
  break;
@@ -1419,8 +1501,40 @@ async function main() {
1419
1501
  case 'docs': {
1420
1502
  const docsSubCmd = filteredArgs[1];
1421
1503
  if (docsSubCmd === 'migrate') {
1422
- const { migrateDocs } = await import('./migrate.js');
1423
- migrateDocs(dir);
1504
+ // docs migrate 双形态(2026-09-08-docs-fix-capability / D-002+D-004):
1505
+ // 有 --from/--to → 新语义(路径引用前缀迁移,薄模块 docs-migrate.js)
1506
+ // 无 flag → 旧语义(.sillyspec 结构一次性迁移 migrateDocs,兼容保留)
1507
+ const rawMigrateArgs = filteredArgs.slice(2);
1508
+ let mFrom = null, mTo = null, mApply = false;
1509
+ for (let i = 0; i < rawMigrateArgs.length; i++) {
1510
+ const a = rawMigrateArgs[i];
1511
+ if (a === '--from' && rawMigrateArgs[i + 1] !== undefined) { mFrom = rawMigrateArgs[i + 1]; i++; }
1512
+ else if (a === '--to' && rawMigrateArgs[i + 1] !== undefined) { mTo = rawMigrateArgs[i + 1]; i++; }
1513
+ else if (a === '--apply') { mApply = true; }
1514
+ else if (a === '--from' || a === '--to') {
1515
+ console.error(`❌ docs migrate: ${a} 缺值(如 --from "modules/" --to "backend/app/modules/")`);
1516
+ process.exit(2);
1517
+ } else if (a.startsWith('--')) {
1518
+ console.error(`❌ docs migrate: 未知 flag「${a}」。已知 flag:--from --to --apply`);
1519
+ process.exit(2);
1520
+ }
1521
+ }
1522
+ if (mFrom !== null || mTo !== null) {
1523
+ if (mFrom === null || mTo === null) {
1524
+ console.error('❌ docs migrate: --from 与 --to 必须成对提供');
1525
+ process.exit(2);
1526
+ }
1527
+ const { runDocsMigrate } = await import('./docs-migrate.js');
1528
+ const result = runDocsMigrate({ projectRoot: dir, from: mFrom, to: mTo, apply: mApply });
1529
+ // exit code 契约:0=dry-run 零计划或 --apply 后 postCheck 全绿;1=--apply 后仍失效或
1530
+ // unverified>0;dry-run 有计划恒 0(预览不判成败)
1531
+ if (!mApply) { process.exit(0); }
1532
+ const fail = result.unverified > 0 || (result.postCheck && result.postCheck.invalid > 0);
1533
+ process.exit(fail ? 1 : 0);
1534
+ } else {
1535
+ const { migrateDocs } = await import('./migrate.js');
1536
+ migrateDocs(dir);
1537
+ }
1424
1538
  } else if (docsSubCmd === 'check') {
1425
1539
  // docs check(2026-08-15 docs-check-productize D-6):文档 file:line 引用校验。
1426
1540
  // exit code 三档(D-003):0 全绿 / 1 存在无效引用 / 2 配置/IO 错误(DocsCheckConfigError)。
@@ -1433,14 +1547,14 @@ async function main() {
1433
1547
  // 写回);--dry-run 预览不写盘,单独传(无 --fix)也走预览路径——design §5.2 行为矩阵
1434
1548
  // --dry-run 列独立存在(「报告修复预览 + exit 1」),即 dryRun 置位 = fix 语义自动生效
1435
1549
  // 但零写盘(本口径记录于此,防歧义)。
1436
- const BARE_FLAGS = ['--suggest', '--fix', '--dry-run'];
1550
+ const BARE_FLAGS = ['--fix', '--dry-run', '--no-exempt'];
1437
1551
  const PAIRED_FLAGS = ['--paths'];
1438
1552
  const rawDocsArgs = filteredArgs.slice(2);
1439
1553
  const docsCheckFlags = [];
1440
1554
  let cliPaths = null;
1441
- let suggest = false;
1442
1555
  let fix = false;
1443
1556
  let dryRun = false;
1557
+ let noExempt = false;
1444
1558
  for (let i = 0; i < rawDocsArgs.length; i++) {
1445
1559
  const a = rawDocsArgs[i];
1446
1560
  if (a === '--paths' && rawDocsArgs[i + 1] !== undefined) {
@@ -1450,9 +1564,9 @@ async function main() {
1450
1564
  console.error('❌ docs check: --paths 缺值(逗号分隔 glob,如 --paths "docs/**/*.md")');
1451
1565
  process.exit(2);
1452
1566
  } else if (BARE_FLAGS.includes(a)) {
1453
- if (a === '--suggest') suggest = true;
1454
- else if (a === '--fix') fix = true;
1567
+ if (a === '--fix') fix = true;
1455
1568
  else if (a === '--dry-run') dryRun = true;
1569
+ else if (a === '--no-exempt') noExempt = true;
1456
1570
  } else if (a.startsWith('--')) {
1457
1571
  console.error(`❌ docs check: 未知 flag「${a}」。已知 flag:${[...BARE_FLAGS, ...PAIRED_FLAGS].join(' ')}(--json 为全局 flag)`);
1458
1572
  process.exit(2);
@@ -1471,6 +1585,7 @@ async function main() {
1471
1585
  skip: cfg.skip,
1472
1586
  keywordAssert: cfg.keywordAssert,
1473
1587
  crossRepoRoots: cfg.crossRepoRoots,
1588
+ exempt: !noExempt,
1474
1589
  });
1475
1590
  // task-03 修复链路(FR-04,platform-map-auto-anchors):fixActive 才构造 fixes——
1476
1591
  // 仅 fix.fixable===true 且 newLine 为整数的条目可重锚;newRef = ref 的行号部分替换为
@@ -1491,6 +1606,31 @@ async function main() {
1491
1606
  }
1492
1607
  }
1493
1608
  const fixResult = fixActive ? applyFixes(dir, fixes, dryRun ? { dryRun: true } : {}) : null;
1609
+ // 修复回执(change: 2026-09-07-ir-hardening D-007@v1,FR-04):--fix 落盘后用与首查
1610
+ // 完全相同的口径重跑一次失效收集,得 after 计数——「前 N → 重锚 M → 后 K(消除 N-K)」
1611
+ // 即修复建议被机器证明消除了多少诊断(acceptsFix 最小件)。仅 --fix(写盘)路径;
1612
+ // --dry-run 不写盘重跑无意义,receipt 为 null;非 fix 路径零变化。
1613
+ let fixReceipt = null;
1614
+ if (fix && !dryRun && fixResult && fixResult.applied > 0) {
1615
+ try {
1616
+ const after = runDocsCheck({
1617
+ projectRoot: dir,
1618
+ docs: docsCheckFlags.length > 0 ? docsCheckFlags : null,
1619
+ paths: cliPaths || cfg.paths,
1620
+ skip: cfg.skip,
1621
+ keywordAssert: cfg.keywordAssert,
1622
+ crossRepoRoots: cfg.crossRepoRoots,
1623
+ });
1624
+ fixReceipt = {
1625
+ invalidBefore: result.invalid.length,
1626
+ reAnchored: fixResult.applied,
1627
+ invalidAfter: after.invalid.length,
1628
+ eliminated: result.invalid.length - after.invalid.length,
1629
+ };
1630
+ } catch (e) {
1631
+ console.warn(`⚠️ 修复回执重算失败(不影响修复本身):${e && e.message ? e.message : e}`);
1632
+ }
1633
+ }
1494
1634
  // 变更名提名 advisory(2026-08-31 变更关联审计 P2):铁律同决策规则族——只 warn,
1495
1635
  // 不进 invalid、不影响下方 exit code 与 docs gate。降级容错:advisory 自身异常
1496
1636
  // 不拖垮主检查(try-catch 吞并提示)。
@@ -1503,7 +1643,7 @@ async function main() {
1503
1643
  if (json) {
1504
1644
  // --fix 与 --json 组合保持 stdout 纯 JSON 可解析(constraints):修复统计作 result.fixReport
1505
1645
  // 附加后整体 stringify,主体仍是 runDocsCheck 返回(invalid[].fix 是 task-01 设计内增量)
1506
- if (fixResult) result.fixReport = { applied: fixResult.applied, skipped: fixResult.skipped.length, dryRun };
1646
+ if (fixResult) result.fixReport = { applied: fixResult.applied, skipped: fixResult.skipped.length, dryRun, ...(fixReceipt ? { receipt: fixReceipt } : {}) };
1507
1647
  if (changeNameReport) result.changeNameReport = changeNameReport;
1508
1648
  console.log(JSON.stringify(result, null, 2));
1509
1649
  } else {
@@ -1514,7 +1654,10 @@ async function main() {
1514
1654
  if (result.ok) {
1515
1655
  console.log(`✅ docs check: ${result.total} 处引用全通过(其中 ${result.kwChecked} 处带关键词断言)`);
1516
1656
  } else {
1517
- console.error(`\n❌ docs check: ${result.invalid.length}/${result.total} 处引用失效:`);
1657
+ // FR-5(D-005,2026-09-08-docs-fix-capability):报告内容统一 stdout(脚本 capture_output
1658
+ // 只读 stdout 可捕获);stderr 仅留 ⚠️ 诊断(console.warn)与配置错误(exit 2 路径)。
1659
+ // 文本逐字不变,只改通道。
1660
+ console.log(`\n❌ docs check: ${result.invalid.length}/${result.total} 处引用失效:`);
1518
1661
  let appliedLeft = fixResult ? fixResult.applied : 0;
1519
1662
  const skippedQueue = fixResult ? [...fixResult.skipped] : [];
1520
1663
  for (const inv of result.invalid) {
@@ -1522,26 +1665,31 @@ async function main() {
1522
1665
  if (newRef !== undefined && appliedLeft > 0) {
1523
1666
  // 已应用(dry-run 为将应用):归因按构造顺序消耗 applied 计数——写回 skipped 仅
1524
1667
  // 防御路径(文档消失/行内失配)触发,CLI 正常流 fixable 必然落位
1525
- console.error(` ✅ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${newRef}${dryRun ? '(dry-run 未写盘)' : ''}`);
1668
+ console.log(` ✅ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${newRef}${dryRun ? '(dry-run 未写盘)' : ''}`);
1526
1669
  appliedLeft--;
1527
1670
  } else if (newRef !== undefined) {
1528
1671
  const s = skippedQueue.shift();
1529
- console.error(` ⚠️ [${inv.doc}:L${inv.docLine}] ${inv.ref} → 写回跳过:${s ? s.reason : '未知原因'}`);
1672
+ console.log(` ⚠️ [${inv.doc}:L${inv.docLine}] ${inv.ref} → 写回跳过:${s ? s.reason : '未知原因'}`);
1530
1673
  } else if (fixResult) {
1531
1674
  // needs-manual:多命中歧义/零命中/无 token——fix.reason 含候选行号,交人工
1532
- console.error(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${(inv.fix && inv.fix.reason) || inv.reason}(待人工)`);
1675
+ console.log(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${(inv.fix && inv.fix.reason) || inv.reason}(待人工)`);
1533
1676
  } else {
1534
- console.error(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${inv.reason}`);
1677
+ console.log(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${inv.reason}`);
1535
1678
  }
1536
- if (suggest && inv.suggest && inv.suggest.length > 0) {
1537
- console.error(` 💡 候选行号: ${inv.suggest.join(', ')}(token 命中行,人工确认后更新文档锚)`);
1679
+ if (inv.suggest && inv.suggest.length > 0) {
1680
+ // 2026-09-07-ir-hardening task-08:原 --suggest 旗标删除(no-op 首选路径退役),
1681
+ // 候选行号提示改为 needs-manual 默认输出——旗标唯一用途,默认开更有信息量
1682
+ console.log(` 💡 候选行号: ${inv.suggest.join(', ')}(token 命中行,人工确认后更新文档锚)`);
1538
1683
  }
1539
1684
  }
1540
1685
  if (fixResult) {
1541
- console.error(`\n🔧 重锚报告:${fixResult.applied} 处已${dryRun ? '预览' : '改写'}${dryRun ? '(dry-run 未写盘)' : ''}、${result.invalid.length - fixes.length} 处待人工${fixResult.skipped.length > 0 ? `、${fixResult.skipped.length} 处写回跳过` : ''}。`);
1686
+ console.log(`\n🔧 重锚报告:${fixResult.applied} 处已${dryRun ? '预览' : '改写'}${dryRun ? '(dry-run 未写盘)' : ''}、${result.invalid.length - fixes.length} 处待人工${fixResult.skipped.length > 0 ? `、${fixResult.skipped.length} 处写回跳过` : ''}。`);
1687
+ if (fixReceipt) {
1688
+ console.log(`🔁 修复回执(机器复跑证明):修复前 ${fixReceipt.invalidBefore} 处失效 → 重锚 ${fixReceipt.reAnchored} 处 → 修复后 ${fixReceipt.invalidAfter} 处(消除 ${fixReceipt.eliminated} 处)。`);
1689
+ }
1542
1690
  }
1543
- console.error(`\n修复指引:行号漂移 → 更新文档行号到当前源码;文件删改名 → 更新引用路径;`);
1544
- console.error(`关键词缺失但行号正确 → 确认符号是否改名,改文档 token 或行号。`);
1691
+ console.log(`\n修复指引:行号漂移 → 更新文档行号到当前源码;文件删改名 → 更新引用路径;`);
1692
+ console.log(`关键词缺失但行号正确 → 确认符号是否改名,改文档 token 或行号。`);
1545
1693
  }
1546
1694
  // 变更名提名 advisory 段(主结果之后、exit 之前;不参与 ok/invalid 与 exit code)。
1547
1695
  // 零输出原则(docs-check-fix 契约:无新 flag 时 stdout 逐字节一致;决策规则族同款
@@ -1622,7 +1770,9 @@ async function main() {
1622
1770
  }
1623
1771
  process.exit(g.exitCode);
1624
1772
  } else {
1625
- console.log('用法: sillyspec docs migrate | sillyspec docs check [--paths <glob,...>] [--json] [--suggest] [--fix] [--dry-run] | sillyspec docs gate [--init-baseline] [--json]');
1773
+ 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]');
1774
+ console.log(' docs migrate:--from/--to 路径前缀迁移(默认 dry-run,--apply 写盘+自动复核);无 flag = 旧 .sillyspec 结构迁移');
1775
+ console.log(' docs check --no-exempt:archive/finished 与 doc_type: snapshot 文档照常校验(缺省豁免)');
1626
1776
  process.exit(2);
1627
1777
  }
1628
1778
  break;
@@ -19,6 +19,25 @@ export class ChangeRegistry {
19
19
  return rows.map(r => r.name);
20
20
  }
21
21
 
22
+ /**
23
+ * 读变更创建时间(IR 严格模式闸门判别源,change: 2026-09-07-ir-hardening D-001@v1)。
24
+ * 只读不抛(与 listChanges 同款 DB 访问):变更不存在 / db 异常 → null——调用方
25
+ * (isStrictChange)按 fail-open 落存量豁免,不误伤。
26
+ * @param {string} cwd
27
+ * @param {string} changeName
28
+ * @returns {string|null} ISO 时间字符串或 null
29
+ */
30
+ getChangeCreatedAt(cwd, changeName) {
31
+ if (!changeName || typeof changeName !== 'string') return null;
32
+ try {
33
+ const db = this.pm._ensureDB(cwd);
34
+ const row = db.getDb().prepare('SELECT created_at FROM changes WHERE name = ?').get(changeName);
35
+ return row && typeof row.created_at === 'string' ? row.created_at : null;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
22
41
  /**
23
42
  * 查询单个变更的流程阶段与状态(quick 轻量归档阶段闸用)。
24
43
  * 与 readChangeIsolation 不同(展示读,catch → null 宽容):本方法服务权限判定,读失败
package/src/progress.js CHANGED
@@ -833,6 +833,8 @@ export class ProgressManager {
833
833
  // ── 变更管理 ──
834
834
 
835
835
  listChanges(cwd) { return this._changeRegistry.listChanges(cwd); }
836
+ /** 读变更创建时间(IR 严格模式闸门判别源,2026-09-07-ir-hardening D-001)——只读不抛,缺行/异常 → null */
837
+ getChangeCreatedAt(cwd, changeName) { return this._changeRegistry.getChangeCreatedAt(cwd, changeName); }
836
838
 
837
839
  // 单变更阶段查询(quick 轻量归档阶段闸):无行 null,读失败抛(调用方 fail-closed)
838
840
  getChangeStage(cwd, changeName) { return this._changeRegistry.getChangeStage(cwd, changeName); }
@@ -1087,6 +1087,22 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
1087
1087
  // brainstorm 自动创建变更也写 title(--input 需求描述优先 / 自动名兜底),proposal 落盘后刷新。
1088
1088
  progress = pm.initChange(cwd, autoName, { title: inputText ? sanitizeDesc(inputText) : autoName })
1089
1089
  changeName = autoName
1090
+ } else if (stageName === 'auto') {
1091
+ // auto 零活跃建变更(2026-09-08-auto-driver D-003@v1,Grill P1-②):SKILL 启动命令即
1092
+ // run auto --input——此前此处 exit 2 是断头路。复用 brainstorm 命名(date-new-change-hex)。
1093
+ // pm.read 对零/多活跃都返 null——先分流:多活跃报错列候选(不隐式选择),零活跃才建。
1094
+ const activeCount = pm.listChanges(cwd)
1095
+ if (activeCount.length > 1) {
1096
+ console.error(`❌ 已存在 ${activeCount.length} 个活跃变更,run auto 无法自动定位目标。`)
1097
+ console.error(' 活跃变更:' + activeCount.join('、'))
1098
+ console.error(' 请用 --change <变更名> 显式指定,或先归档/收尾现有变更。')
1099
+ process.exit(2)
1100
+ }
1101
+ const date = new Date().toISOString().slice(0, 10)
1102
+ const autoName = `${date}-new-change-${randomBytes(4).toString('hex')}`
1103
+ console.log(`🔄 auto 模式自动创建变更:${autoName}(可用 --change 指定名称,或事后 change-rename)`)
1104
+ progress = pm.initChange(cwd, autoName, { title: inputText ? sanitizeDesc(inputText) : autoName })
1105
+ changeName = autoName
1090
1106
  } else {
1091
1107
  console.error('❌ 未找到进度数据,请先运行 sillyspec init 或指定 --change <变更名>')
1092
1108
  // 自愈引导(坑 suggestion-command-missing-change 同族):多活跃变更仓不带 --change 时
@@ -1546,8 +1562,15 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1546
1562
  const inputText = autoFlagValue('--input')
1547
1563
  const skipApproval = flags.includes('--skip-approval')
1548
1564
  const explicitMode = autoFlagValue('--mode')
1565
+ // --wait-interactive(2026-09-08-auto-driver D-004@v1,FR-03):TTY 下 requiresUser 步 readline 直收
1566
+ const waitInteractive = autoFlagValue('--wait-interactive') === 'true'
1567
+ globalThis.__ssWaitInteractive = waitInteractive // maybeWaitInteractive 消费(同进程直通)
1549
1568
  const specBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
1550
1569
 
1570
+ // 单活跃自动选中为既有行为(入口 pm.read(cwd,null))——此处仅回显(D-003@v1;
1571
+ // 零活跃建变更已上移入口守卫层,多活跃 exit 2 由入口守卫处理)。
1572
+ if (changeName) console.log(`🎯 auto 目标变更(单活跃自动选中或显式指定):${changeName}`)
1573
+
1551
1574
  // Helper: 在 auto 模式下获取步骤定义
1552
1575
  const getAutoSteps = async (stage) => {
1553
1576
  if (stage === 'brainstorm') {
@@ -1568,6 +1591,10 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1568
1591
  if (stage === 'brainstorm') {
1569
1592
  const existingSteps = progress.stages?.brainstorm?.steps
1570
1593
  const isAutoModeSteps = existingSteps?.length === 4 && existingSteps?.[0]?.name === '进度确认与上下文加载'
1594
+ // auto 表在位即早退(2026-09-08 E2E 实证 bug:此前仅 !isAutoModeSteps 重种后 return,
1595
+ // auto 表存在时掉进下方通用 ensureStageSteps——主模式 8 步定义判 4 步 auto 表「漂移」
1596
+ // 重种主模式表,与 getAutoSteps 渲染双轨互踩,进度永远回 step1)
1597
+ if (isAutoModeSteps) return progress
1571
1598
  if (!isAutoModeSteps) {
1572
1599
  if (!progress.stages) progress.stages = {}
1573
1600
  progress.stages.brainstorm = {
@@ -1613,6 +1640,8 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1613
1640
  currentStage = firstOpenStage()
1614
1641
  }
1615
1642
  if (!currentStage) {
1643
+ // 重进入且已全完成:复用收尾总结 helper(FR-04)
1644
+ if (changeName) printAutoCompletionSummary(pm, cwd, changeName, flowStages, specBase)
1616
1645
  console.log('All auto flow stages are complete.')
1617
1646
  return
1618
1647
  }
@@ -1656,7 +1685,11 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1656
1685
  }
1657
1686
  const next = nextInFlow(currentStage)
1658
1687
  if (next) console.log(`${currentStage} is complete. Run: sillyspec run auto --done --output "${currentStage} complete"`)
1659
- else console.log('All auto flow stages are complete.')
1688
+ else {
1689
+ console.log('All auto flow stages are complete.')
1690
+ // 展示路径全完成(阶段已关但用户重进)——复用收尾总结(FR-04)
1691
+ printAutoCompletionSummary(pm, cwd, changeName, flowStages, specBase)
1692
+ }
1660
1693
  return
1661
1694
  }
1662
1695
  // execute 阶段启动前检查审批
@@ -1677,7 +1710,9 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1677
1710
  }
1678
1711
  }
1679
1712
  }
1680
- await outputStep(currentStage, pendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts, null, collectStageWaitHistory(progress, currentStage))
1713
+ await outputStep(currentStage, pendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts, null, collectStageWaitHistory(progress, currentStage), { changeName })
1714
+ // wait 直通(FR-03):requiresUser 且 TTY 且旗标开 → readline 直收,等价 --continue --answer
1715
+ await maybeWaitInteractive(defSteps[pendingIdx], changeName, currentStage)
1681
1716
  return
1682
1717
  }
1683
1718
 
@@ -1717,13 +1752,15 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1717
1752
  }
1718
1753
  }
1719
1754
  }
1720
- await outputStep(currentStage, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts, null, collectStageWaitHistory(progress, currentStage))
1755
+ await outputStep(currentStage, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts, null, collectStageWaitHistory(progress, currentStage), { changeName })
1721
1756
  return
1722
1757
  }
1723
1758
 
1724
1759
  const next = nextInFlow(currentStage)
1725
1760
  if (!next) {
1726
1761
  console.log('\nAll auto flow stages are complete.')
1762
+ // 自然收尾点(--done 后 next==null,FR-04):打印收尾总结
1763
+ printAutoCompletionSummary(pm, cwd, changeName, flowStages, specBase)
1727
1764
  return
1728
1765
  }
1729
1766
 
@@ -1795,7 +1832,95 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1795
1832
  }
1796
1833
  }
1797
1834
  }
1798
- await outputStep(next, firstPending, nextSteps, cwd, changeName, progress.project || null, platformOpts, null, collectStageWaitHistory(progress, next))
1835
+ await outputStep(next, firstPending, nextSteps, cwd, changeName, progress.project || null, platformOpts, null, collectStageWaitHistory(progress, next), { changeName })
1836
+ }
1837
+ }
1838
+
1839
+
1840
+ /**
1841
+ * maybeWaitInteractive —— auto 模式 wait 用户输入直通(change: 2026-09-08-auto-driver,D-004@v1,FR-03)。
1842
+ *
1843
+ * 时序:outputStep 渲染返回后调用。条件全满足(旗标开 + requiresUser 四源 + stdin TTY)→
1844
+ * readline 直收一行,等价自动 `--continue --answer <输入>`(复用 completeStep wait→continue 状态机,
1845
+ * 无新状态);任一不满足/异常 → 静默或提示回三段式(fail-open,Grill P2-①时序)。
1846
+ * 仅 runAutoMode 的首渲染点消费(--done 推进路径不重收)。
1847
+ */
1848
+ export async function maybeWaitInteractive(stepDef, changeName, stageName, { rlFactory = null } = {}) {
1849
+ try {
1850
+ if (!globalThis.__ssWaitInteractive) return
1851
+ if (!stepDef) return
1852
+ const { requiresUser } = await import('./prompt.js')
1853
+ // prompt 正文在此不可得——四源三键优先(WAIT_MARKER_RE 源由 SS-META 渲染时已并入,直通用三键近似:
1854
+ // 真需 wait 的步必有标记,纯正文标记步三段式照旧)
1855
+ const needsUser = stepDef.requiresWait === true || stepDef.conditionalWait === true || stepDef.requiresConfirm === true
1856
+ if (!needsUser) return
1857
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1858
+ console.log('ℹ️ --wait-interactive 需要 TTY(当前非交互环境)——按 prompt 的 wait 指令走三段式')
1859
+ return
1860
+ }
1861
+ // rlFactory 为测试缝(注入伪 readline);生产路径用 node:readline/promises 真实 TTY
1862
+ const rl = rlFactory
1863
+ ? rlFactory()
1864
+ : (await import('node:readline/promises')).createInterface({ input: process.stdin, output: process.stdout })
1865
+ console.log('')
1866
+ const answer = await rl.question('⌨️ 本步需要你的输入(直通模式,回答后将自动 --continue --answer):')
1867
+ rl.close()
1868
+ if (answer && answer.trim()) {
1869
+ console.log(`📩 已接收,自动续行:sillyspec run auto --continue --answer "${answer.trim().slice(0, 60)}${answer.trim().length > 60 ? '…' : ''}" ...`)
1870
+ // 复用既有 continue 路径:与 runCommand 的 --continue 同源(子进程自调避免递归锁)
1871
+ const { execFileSync } = await import('node:child_process')
1872
+ const binSelf = new URL('../bin/sillyspec.js', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')
1873
+ try {
1874
+ execFileSync(process.execPath, [binSelf, 'run', 'auto', '--continue', '--answer', answer.trim(), '--change', changeName], { stdio: 'inherit', cwd: process.cwd() })
1875
+ } catch { /* continue 失败(非 waiting 态等)静默——回三段式由 agent 走 */ }
1876
+ }
1877
+ } catch (e) {
1878
+ console.log(`ℹ️ wait 直通异常回退三段式:${e && e.message ? e.message : e}`)
1799
1879
  }
1800
1880
  }
1801
1881
 
1882
+
1883
+ /**
1884
+ * printAutoCompletionSummary —— auto 流程收尾总结(change: 2026-09-08-auto-driver,D-005@v1,FR-04)。
1885
+ * 挂点:--done 后 next==null 的自然收尾(:1726 区)+ :1616 重进入分支复用。纯 console 无新文件:
1886
+ * 变更名/各阶段状态/tasks 勾选(readPlanCheckboxStatus)/产物存在清单/last-delta 模块清单(缺失跳过)。
1887
+ */
1888
+ function printAutoCompletionSummary(pm, cwd, changeName, flowStages, specBase) {
1889
+ try {
1890
+ if (!changeName) return
1891
+ const progress = pm.read(cwd, changeName)
1892
+ console.log('')
1893
+ console.log('═'.repeat(56))
1894
+ console.log(`🏁 auto 流程收尾总结 — ${changeName}`)
1895
+ for (const s of flowStages) {
1896
+ const st = progress.stages?.[s]?.status || '未开始'
1897
+ const icon = st === 'completed' ? '✅' : st === 'in-progress' ? '🔄' : '⬜'
1898
+ console.log(` ${icon} ${s}: ${st}`)
1899
+ }
1900
+ // tasks 勾选
1901
+ try {
1902
+ const changeDirCk = join((specBase || join(cwd, '.sillyspec')), 'changes', changeName)
1903
+ const plan = pm.readPlanCheckboxStatus(changeDirCk)
1904
+ if (plan && plan.total > 0) console.log(` 📋 任务勾选:${plan.checked}/${plan.total}`)
1905
+ } catch { /* 勾选读取 fail-soft */ }
1906
+ // 产物存在清单
1907
+ const changeDirArt = join((specBase || join(cwd, '.sillyspec')), 'changes', changeName)
1908
+ const artifacts = ['proposal.md', 'design.md', 'tasks.md', 'plan.md', 'verify-result.md', 'delta.md']
1909
+ const found = artifacts.filter(a => existsSync(join(changeDirArt, a)))
1910
+ if (found.length > 0) console.log(` 📁 产物:${found.join('、')}`)
1911
+ // last-delta 模块清单(sidecar 缺失跳过)
1912
+ try {
1913
+ const runtimeRoot = join((specBase || join(cwd, '.sillyspec')), '.runtime')
1914
+ const sidecarPath = join(runtimeRoot, 'last-delta.json')
1915
+ if (existsSync(sidecarPath)) {
1916
+ const sidecar = JSON.parse(readFileSync(sidecarPath, 'utf8'))
1917
+ if (Array.isArray(sidecar.affectedModules) && sidecar.affectedModules.length > 0) {
1918
+ console.log(` 🔁 涉及模块(最近 delta):${sidecar.affectedModules.join('、')}`)
1919
+ }
1920
+ }
1921
+ } catch { /* sidecar 读取 fail-soft */ }
1922
+ console.log('═'.repeat(56))
1923
+ } catch (e) {
1924
+ console.log(`ℹ️ 收尾总结渲染降级:${e && e.message ? e.message : e}`)
1925
+ }
1926
+ }
@@ -441,16 +441,19 @@ export async function handleArchiveConfirmStep({ stageName, steps, currentIdx, c
441
441
  const deltaChangeName = progress.currentChange || changeName
442
442
  if (deltaChangeName) {
443
443
  try {
444
- const { buildDeltaReport } = await import('../archive-delta.js')
444
+ const { buildDeltaReport, writeLastDeltaSidecar } = await import('../archive-delta.js')
445
445
  const deltaChangeDir = join(specBase, 'changes', deltaChangeName)
446
- const deltaMd = buildDeltaReport({
446
+ const deltaResult = buildDeltaReport({
447
447
  changeDir: deltaChangeDir,
448
448
  specRoot: specBase,
449
449
  // project 缺参可 null(loadModuleMap null 降级路径已支持,报告内逐段降级注记)
450
450
  project: progress?.project || null,
451
451
  runtimeRoot: resolveRuntimeRoot(platformOpts, specBase),
452
+ // IR 回灌 sidecar(2026-09-07-ir-hardening D-006):与手动补跑同源数据
453
+ withSummary: true,
452
454
  })
453
- writeFileSync(join(deltaChangeDir, 'delta.md'), deltaMd)
455
+ writeFileSync(join(deltaChangeDir, 'delta.md'), deltaResult.markdown)
456
+ writeLastDeltaSidecar(resolveRuntimeRoot(platformOpts, specBase), deltaResult)
454
457
  console.log(`📊 归档前已生成 delta.md: ${join(deltaChangeDir, 'delta.md')}`)
455
458
  } catch (e) {
456
459
  console.error(`⚠️ 归档前 delta.md 自动生成失败(不阻断归档): ${e.message}`)
@@ -932,10 +935,14 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
932
935
  // P0-2(2026-09-02 跨 agent 工单):触及 src/test 的 quick --done 内置 test+lint
933
936
  // 实测门禁——把 CLAUDE.md 规则 8 从 agent 自律下沉为 CLI 卡点(fail → step 回
934
937
  // pending + exit 1,重跑不丢进度;纯 doc/配置与未配置命令自动跳过不阻断)。
938
+ // declaredFiles 兜底(2026-09-07):倒推 B 模式(代码先行 --done 收尾)的文件全部
939
+ // 早于会话启动被基线吸收 → 审计 changedFiles 空 → 此前静默 skip;声明边界触及
940
+ // src/test 时实测仍应跑(quick-f9138c2f 实证)。
935
941
  const gate = await runQuickTestLintGate({
936
942
  cwd,
937
943
  specBase,
938
944
  changedFiles: review.changedFiles,
945
+ declaredFiles: Array.isArray(mergedGuard?.allowedFiles) ? mergedGuard.allowedFiles : [],
939
946
  changeName,
940
947
  })
941
948
  printQuickTestLintGate(gate)