sillyspec 3.26.13 → 3.26.15

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 (56) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +2 -0
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -1
  3. package/.claude/skills/sillyspec-commit/SKILL.md +18 -60
  4. package/.claude/skills/sillyspec-continue/SKILL.md +2 -0
  5. package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
  6. package/.claude/skills/sillyspec-plan/SKILL.md +10 -2
  7. package/.claude/skills/sillyspec-quick/SKILL.md +2 -2
  8. package/.claude/skills/sillyspec-resume/SKILL.md +2 -0
  9. package/.claude/skills/sillyspec-verify/SKILL.md +2 -0
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +7 -1
  11. package/package.json +1 -1
  12. package/src/commit-suggest.js +202 -0
  13. package/src/config-cat.js +88 -0
  14. package/src/docs-check.js +48 -12
  15. package/src/doctor-diagnostics.js +49 -0
  16. package/src/hooks/worktree-guard.js +20 -1
  17. package/src/index.js +547 -21
  18. package/src/init.js +1 -1
  19. package/src/local-register.js +98 -0
  20. package/src/module-impact.js +146 -0
  21. package/src/progress/change-registry.js +43 -4
  22. package/src/progress/stage-machine.js +10 -5
  23. package/src/progress.js +18 -3
  24. package/src/quicklog.js +106 -1
  25. package/src/run/command.js +151 -38
  26. package/src/run/complete-handlers.js +62 -4
  27. package/src/run/complete.js +10 -8
  28. package/src/run/concurrent-detect.js +3 -1
  29. package/src/run/gates.js +86 -4
  30. package/src/run/multi-repo-context.js +3 -1
  31. package/src/run/next.js +128 -0
  32. package/src/run/prompt.js +28 -6
  33. package/src/run/scan-profile.js +35 -23
  34. package/src/run/shared.js +28 -7
  35. package/src/scan-postcheck.js +142 -2
  36. package/src/stage-contract-spec.js +1 -1
  37. package/src/stage-review.js +55 -7
  38. package/src/stages/archive.js +1 -1
  39. package/src/stages/brainstorm.js +26 -12
  40. package/src/stages/doctor.js +2 -4
  41. package/src/stages/execute.js +110 -26
  42. package/src/stages/knowledge.js +21 -5
  43. package/src/stages/plan-postcheck.js +24 -10
  44. package/src/stages/plan.js +14 -9
  45. package/src/stages/quick.js +14 -6
  46. package/src/stages/scan.js +1 -0
  47. package/src/stages/verify.js +7 -49
  48. package/src/sync.js +19 -7
  49. package/src/task-review.js +256 -10
  50. package/src/verify-probes.js +264 -0
  51. package/src/workspace.js +125 -0
  52. package/src/worktree-apply.js +15 -1
  53. package/src/worktree.js +58 -8
  54. package/templates/claude-instruction.md +1 -1
  55. package/templates/prompts/taskcard-rules.md +1 -1
  56. package/templates/prompts/verify-probes.md +13 -47
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, join, resolve } from 'path';
11
+ import { basename, extname, join, resolve } from 'path';
12
12
  import { safeGit, git } from './git-helper.js';
13
13
  import { getVersion } from './version.js';
14
14
 
@@ -80,7 +80,9 @@ SillySpec CLI — 规范驱动开发工具包
80
80
  --cleanup-ghosts [--confirm] 归档幽灵 active 记录(db 有行无目录 / 空壳目录 0 文件超 7 天,SS-2/2b)
81
81
 
82
82
  sillyspec local detect [--dir <path>] 生成本地配置 local.yaml(纯 fs 嗅探,零 token、不跑 scan)
83
+ sillyspec local register-repo <key> <path> 注册跨仓仓到 local.yaml repos: 段(外科手术式写入,其余内容保留)
83
84
  sillyspec config [schema] [--json] 打印 local.yaml 全部已知键 + 生效状态 + 读取点(堵外部 agent 配置发现缺口)
85
+ sillyspec config cat [--json] 读取 local.yaml 实际内容(自动定位真实路径,worktree 内解析到主仓)
84
86
  sillyspec runtime list [--json] 枚举 .sillyspec/.runtime/ 运行时产物(只读,看手上有哪些证据/状态文件)
85
87
  sillyspec dispatch <probe | hint> SillyHub 派发能力探测 + 策略生成(agent 调用桥,仅渲染不执行 tool)
86
88
 
@@ -89,8 +91,18 @@ SillySpec CLI — 规范驱动开发工具包
89
91
  sillyspec workflow list
90
92
  sillyspec gate <stage> --change <name> [--json] 机器门控:阶段能否标记完成(只读)
91
93
  sillyspec derive <facet> --change <name> [--json] 单项事实核验(facet: execute-evidence|verify-test|task-reviews|artifacts)
92
- sillyspec backfill-reviews --change <name> [--json] 为手动补的 task 生成 review.json 草稿(cannot_verify,解 archive 客观完成度阻断)
93
- sillyspec register-stage-review --change <name> --stage <brainstorm|plan|execute> [--from <review.json>] [--json]
94
+ sillyspec backfill-reviews --change <name> [--adopt] [--json] 缺 review.json 的 task 生成草稿;--adopt 重算代填已有 review 的 base/head 等机械字段(verdict 保留)
95
+ sillyspec symbol-impact --change <name> 生成 symbol-impact.md 逐 task <!--TODO--> 骨架(gate 拒绝未替换占位,防骨架直接过门)
96
+ sillyspec next 项目状态探测:输出当前状态 + 下一步命令 + 依据(吸收 continue/resume 手工探测表)
97
+ sillyspec commit [--json] 智能提交建议:收集 QUICKLOG/已勾 task/阶段产出语义,生成建议 message(只建议不执行)
98
+ sillyspec verify-probes --change <name> [--init] verify 机械探针(TODO 标记/测试覆盖/API 对账/删除对账);--init 生成 verify-result.md 骨架
99
+ sillyspec module-impact --change <name> 生成 module-impact.md 骨架(文件×模块归属按 module-map 预填 + 未匹配清单)
100
+ sillyspec endpoints extract --change <name> [--task task-NN] [--dir <dir>|--files <a.py,b.js>] 静态扫描路由装饰器生成 endpoints.json
101
+ sillyspec scan-fix-headers [--project <名>] scan 文档补 author/created_at header(幂等)
102
+ sillyspec workspace add <名> <路径> [--role] [--repo] | remove <名> | status 多项目工作区登记与状态探测
103
+ sillyspec scan-fix-leak --spec-dir <specRoot> source_root 误写产物搬到平台 specRoot(污染一键修复)
104
+ sillyspec run quick --cancel --change <会话ID> [--ql <ql-id>] 取消误启动的 quick 会话(QUICKLOG 翻已取消 + 挂载行移除)
105
+ sillyspec register-stage-review --change <name> (--stage <brainstorm|plan|execute> | --all) [--from <review.json>] [--refresh-hash] [--json]
94
106
  生成/adopt stage 级 review.json(docHash 自动算 + 写 marker,治 tier=independent marker 死锁)
95
107
 
96
108
  sillyspec doctor [子命令] 进度库健康检查 + 修复(顶层命令,非 worktree doctor)
@@ -520,10 +532,15 @@ async function main() {
520
532
  // 无子代理 review 落盘 → archive step1 客观完成度(真相源=review.json verdict)判缺 → 阻断归档;本命令补齐
521
533
  // 缺失草稿,独立可随时跑(不必再 execute --done)。缺数据(无 tasks/ 目录 / 改动未 commit / 无 worktree meta)
522
534
  // 时 generateTaskReviewDrafts 提前返回 reason,如实打印不报错。
535
+ // --adopt(2026-08-21 agent-手工产出审计项①):已存在 review.json 的 mechanics 字段
536
+ // (schemaVersion/task/base/head/changedFiles/repo)一键重算代填,agent/子代理只写
537
+ // verdict——手算 hash、手抄 diff 清单是纯错误面,gate 校验的恰是这些字段。与草稿互补:
538
+ // 草稿补「缺失」,adopt 修「已存在但 mechanics 错/缺」,verdict 原样保留。
523
539
  const brChangeIdx = args.indexOf('--change');
524
540
  const brChange = brChangeIdx >= 0 && args[brChangeIdx + 1] ? args[brChangeIdx + 1] : null;
541
+ const brAdopt = args.includes('--adopt');
525
542
  if (!brChange) {
526
- console.error('用法: sillyspec backfill-reviews --change <name> [--json] [--spec-dir <path>]\n 为手动补的 task 生成 review.json 草稿(cannot_verify),解 archive 客观完成度阻断');
543
+ console.error('用法: sillyspec backfill-reviews --change <name> [--adopt] [--json] [--spec-dir <path>]\n 缺失 review.json → 生成 cannot_verify 草稿(解 archive 客观完成度阻断)\n --adopt → 已存在 review.json 的 base/head/changedFiles 等 mechanics 字段一键重算代填(verdict 保留)');
527
544
  process.exit(2);
528
545
  }
529
546
  // 与 run 入口同源消毒(防路径穿越;backfill 下游拼 marker/changes/review 路径)
@@ -560,6 +577,399 @@ async function main() {
560
577
  console.log(' execute run id: ' + result.executeRunId);
561
578
  }
562
579
  }
580
+ if (brAdopt) {
581
+ const { adoptTaskReviewMechanics } = await import('./task-review.js');
582
+ const adoptResult = await adoptTaskReviewMechanics({ changeName: brChange, cwd: dir, platformOpts: brPlatformOpts, ctx: _brCtx });
583
+ if (json) {
584
+ process.stdout.write(JSON.stringify({ ok: true, command: 'backfill-reviews --adopt', change: brChange, ...adoptResult }));
585
+ } else {
586
+ if (adoptResult.adopted > 0) {
587
+ console.log('🔁 adopt: 重算代填 ' + adoptResult.adopted + ' 个已存在 review.json 的 mechanics 字段(verdict 原样保留) [' + brChange + ']');
588
+ }
589
+ if (adoptResult.unchanged > 0) {
590
+ console.log(' ' + adoptResult.unchanged + ' 个 mechanics 已一致,无需改写');
591
+ }
592
+ if (adoptResult.missing > 0) {
593
+ console.log(' ' + adoptResult.missing + ' 个 task 无 review.json(由上方草稿生成负责,adopt 不覆盖)');
594
+ }
595
+ if (adoptResult.downgraded.length > 0) {
596
+ console.warn(' ⚠️ ' + adoptResult.downgraded.join(', ') + ' 原 verdict 非法/缺失,已降级 cannot_verify(不猜本意,待人工复核升级)');
597
+ }
598
+ if (adoptResult.skipped > 0 || adoptResult.reasons.length > 0) {
599
+ console.log(' 跳过 ' + adoptResult.skipped + ' 个:' + (adoptResult.reasons.join(';') || '原因见上'));
600
+ }
601
+ if (adoptResult.adopted === 0 && adoptResult.unchanged === 0 && adoptResult.skipped === 0 && adoptResult.missing === 0) {
602
+ console.log('ℹ️ adopt 无对象 [' + brChange + ']' + (adoptResult.reasons[0] ? ':' + adoptResult.reasons[0] : ''));
603
+ }
604
+ }
605
+ }
606
+ break;
607
+ }
608
+ case 'workspace': {
609
+ // 多项目工作区管理命令化(2026-08-21 审计第四批 C-1):workspace skill 此前纯手写
610
+ // projects/*.yaml + bash for-loop 探测(skill 明言「CLI 无 workspace 命令」)。
611
+ // 机械面全命令化:add 外科写入(init 三字段 + role/repo 追加,已有值保留)、
612
+ // remove 删登记、status 三态探测表。
613
+ const wsSub = filteredArgs[1];
614
+ const wsArgs = filteredArgs.slice(2);
615
+ const wsFlag = (name) => {
616
+ const i = wsArgs.indexOf(name);
617
+ return i >= 0 && wsArgs[i + 1] ? wsArgs[i + 1] : null;
618
+ };
619
+ const { workspaceAdd, workspaceRemove, workspaceStatus } = await import('./workspace.js');
620
+ if (wsSub === 'add') {
621
+ const name = wsFlag('--name') || wsArgs[0];
622
+ const projPath = wsFlag('--path') || wsArgs[1];
623
+ if (!name || !projPath) {
624
+ console.error('用法: sillyspec workspace add <name> <相对路径> [--role <角色>] [--repo <仓库地址>]\n 登记子项目到 .sillyspec/projects/<name>.yaml(外科写入,已有字段保留;路径须存在)');
625
+ process.exit(2);
626
+ }
627
+ try {
628
+ const r = workspaceAdd({ cwd: dir, name, path: projPath, role: wsFlag('--role'), repo: wsFlag('--repo'), specDir });
629
+ console.log(`✅ ${r.created ? '已登记' : '已更新'}子项目 ${name}: ${r.file}`);
630
+ console.log(` 下一步:cd ${projPath} && sillyspec init(或 scan)`);
631
+ } catch (e) {
632
+ console.error(`❌ ${e.message}`);
633
+ process.exit(1);
634
+ }
635
+ } else if (wsSub === 'remove') {
636
+ const name = wsFlag('--name') || wsArgs[0];
637
+ if (!name) {
638
+ console.error('用法: sillyspec workspace remove <name>\n 删除子项目登记(.sillyspec/projects/<name>.yaml;不删项目本体)');
639
+ process.exit(2);
640
+ }
641
+ try {
642
+ const r = workspaceRemove({ cwd: dir, name, specDir });
643
+ console.log(`🗑️ 已移除子项目登记 ${name}: ${r.file}(项目本体未动)`);
644
+ console.log(' 记得 git add 该删除');
645
+ } catch (e) {
646
+ console.error(`❌ ${e.message}`);
647
+ process.exit(1);
648
+ }
649
+ } else if (!wsSub || wsSub === 'status') {
650
+ const r = workspaceStatus({ cwd: dir, specDir });
651
+ if (json) {
652
+ console.log(JSON.stringify({ command: 'workspace status', ...r }, null, 2));
653
+ break;
654
+ }
655
+ if (r.projects.length === 0) {
656
+ console.log('📭 无子项目登记(.sillyspec/projects/ 为空)——sillyspec workspace add <name> <路径> 登记后管理');
657
+ break;
658
+ }
659
+ console.log(`🏢 工作区(${r.projects.length} 个子项目):`);
660
+ const icon = { scanned: '✅', initialized: '⚠️', unregistered: '❌', missing: '❌' };
661
+ for (const p of r.projects) {
662
+ console.log(` ${icon[p.state] || '•'} ${p.name.padEnd(16)} ${(p.path || '').padEnd(18)} ${(p.role || '').padEnd(14)} ${p.detail}`);
663
+ }
664
+ if (r.sharedDocs.length > 0) {
665
+ console.log(`📄 共享规范:${r.sharedDocs.length} 份(${r.sharedDocs.join('、')})`);
666
+ }
667
+ console.log('💡 操作:workspace add/remove <name> <路径>');
668
+ } else {
669
+ console.error('用法: sillyspec workspace add <name> <路径> [--role] [--repo] | remove <name> | status');
670
+ process.exit(2);
671
+ }
672
+ break;
673
+ }
674
+ case 'scan-fix-leak': {
675
+ // source_root 污染一键修复(2026-08-21 审计第四批 C-3):平台模式 scan-postcheck 对
676
+ // 「agent 把产物写到 <source_root>/.sillyspec/ 而非 specRoot」只报 FAILED 不修,agent 手工
677
+ // 搬常搬错。本命令按检查同款清单搬到 specRoot(已存在不覆盖,报告 skipped)。
678
+ const sflSpecDir = specDir || (await import('./progress.js')).resolvePlatformSpecDir(dir, null);
679
+ if (!sflSpecDir || !existsSync(sflSpecDir)) {
680
+ console.error('❌ 缺 --spec-dir <specRoot>(平台模式 spec 根)——scan-fix-leak 把 <cwd>/.sillyspec/ 误写产物搬到 specRoot');
681
+ process.exit(2);
682
+ }
683
+ const { fixSourceRootLeak } = await import('./scan-postcheck.js');
684
+ const r = fixSourceRootLeak({ cwd: dir, specDir: sflSpecDir });
685
+ if (json) {
686
+ console.log(JSON.stringify({ command: 'scan-fix-leak', ...r }, null, 2));
687
+ break;
688
+ }
689
+ if (r.moved.length === 0 && r.skipped.length === 0) {
690
+ console.log('📭 无污染(<cwd>/.sillyspec/ 下无 docs/projects/workflows/knowledge/manifest/local 产物)');
691
+ break;
692
+ }
693
+ for (const m of r.moved) console.log(`📦 已搬移: ${m}`);
694
+ for (const d of r.removedDirs) console.log(`🧹 已清空目录: ${d}`);
695
+ for (const s of r.skipped) console.log(`⏭️ ${s}`);
696
+ if (r.moved.length > 0) console.log(' 搬完重跑 sillyspec scan 的 postcheck 应转绿');
697
+ break;
698
+ }
699
+ case 'next': {
700
+ // 项目状态探测(2026-08-21 审计第二批 G7/G8):吸收 continue/resume 两个 skill 的
701
+ // 手工探测表(文件存在性状态机),一条命令输出「状态 + 下一步命令 + 依据」。
702
+ // 纯 fs 零 token;活跃进度的权威状态仍归 progress show,本命令管无活跃进度时反推。
703
+ const { detectNextStep } = await import('./run/next.js');
704
+ const nxResult = detectNextStep({ cwd: dir, specDir });
705
+ if (json) {
706
+ console.log(JSON.stringify({ command: 'next', ...nxResult }, null, 2));
707
+ break;
708
+ }
709
+ console.log('🤖 SillySpec 状态探测');
710
+ console.log(` 状态:${nxResult.state}`);
711
+ if (nxResult.activeChanges.length > 0) {
712
+ for (const c of nxResult.activeChanges) {
713
+ console.log(` - ${c.name}(task ${c.checked}/${c.total})`);
714
+ }
715
+ }
716
+ console.log(` 👉 下一步:${nxResult.next}`);
717
+ if (nxResult.evidence.length > 0) {
718
+ console.log(` 依据:${nxResult.evidence.join('、')}`);
719
+ }
720
+ break;
721
+ }
722
+ case 'commit': {
723
+ // 智能提交建议(2026-08-21 审计第二批 G1-G3):commit skill 此前让 agent 手工收集
724
+ // 语义(LAST_COMMIT_TIME / cat QUICKLOG / 扫变更目录 / 按路径模式分类)——进度库、
725
+ // quicklog、git 全在 CLI 手里。一条命令产出统计 + 语义来源 + conventional 建议 +
726
+ // 可照抄的 git 命令。**只建议不执行**(确认权在人,对齐 skill 绝对规则「不要自动提交」)。
727
+ const { collectCommitContext } = await import('./commit-suggest.js');
728
+ const cc = collectCommitContext({ cwd: dir, specDir });
729
+ if (json) {
730
+ console.log(JSON.stringify({ command: 'commit', ...cc }, null, 2));
731
+ break;
732
+ }
733
+ if (!cc.hasChanges) {
734
+ console.log('📭 没有需要提交的修改(工作区干净)');
735
+ break;
736
+ }
737
+ console.log(`📦 待提交变更(${cc.changedCount} 个文件${cc.untrackedCount > 0 ? `,其中未跟踪 ${cc.untrackedCount} 个` : ''};上方 stat 仅列已跟踪文件的改动)`);
738
+ if (cc.stat) console.log(cc.stat.split('\n').map(l => ' ' + l).join('\n'));
739
+ if (cc.quickEntries.length > 0) {
740
+ console.log(`\n📝 QUICKLOG(上次提交 ${cc.lastCommitTime || '(无)'} 之后 ${cc.quickEntries.length} 条):`);
741
+ for (const q of cc.quickEntries) {
742
+ console.log(` ${q.status === '已完成' ? '✅' : '⏳'} ${q.qlId} | ${q.title}${q.status === '已完成' ? '' : '(进行中,不进 commit 语义)'}`);
743
+ }
744
+ }
745
+ if (cc.checkedTasks.length > 0) {
746
+ console.log(`\n✅ execute 已勾 task:`);
747
+ for (const t of cc.checkedTasks) console.log(` - [${t.change}] ${t.item}`);
748
+ }
749
+ if (cc.stageArtifacts.length > 0) {
750
+ console.log(`\n📁 阶段产出:${cc.stageArtifacts.join(' / ')}`);
751
+ }
752
+ if (cc.suggestion) {
753
+ console.log(`\n💡 建议 commit message:`);
754
+ console.log(` ${cc.suggestion.subject}`);
755
+ if (cc.suggestion.body) {
756
+ console.log(cc.suggestion.body.split('\n').map(l => ' ' + l).join('\n'));
757
+ }
758
+ const bodyArgs = cc.suggestion.body ? ` -m "${cc.suggestion.body.replace(/"/g, '\\"').replace(/\n/g, ' ')}"` : '';
759
+ console.log(`\n 执行:git add -A && git commit -m "${cc.suggestion.subject.replace(/"/g, '\\"')}"${bodyArgs}`);
760
+ } else {
761
+ console.log('\n💡 无语义来源匹配——请结合上方 diff stat 手写 message');
762
+ }
763
+ console.log('\n(本命令只建议不提交;确认后执行上面的命令)');
764
+ break;
765
+ }
766
+ case 'verify-probes': {
767
+ // verify 机械探针命令化(2026-08-21 审计第三批 H1/H3/H5/H7/F9):verify-probes.md 模板
768
+ // 六探针中的纯机械四个(未实现标记 grep / 测试文件递归查找 / API 契约对账 / 删除对账)
769
+ // 一条命令跑完并渲染成可直接粘贴的 markdown;半语义探针(2/4 + 3.4/3.5)显式留 TODO。
770
+ // --init 顺带生成 verify-result.md 七章节骨架(探针结果预填;结论留「待填」——gate 找
771
+ // 不到 PASS/FAIL 即判不过,骨架不能直接过门)。已存在不覆盖。
772
+ const vpChangeIdx = args.indexOf('--change');
773
+ const vpChange = vpChangeIdx >= 0 && args[vpChangeIdx + 1] ? args[vpChangeIdx + 1] : null;
774
+ const vpInit = args.includes('--init');
775
+ if (!vpChange) {
776
+ console.error('用法: sillyspec verify-probes --change <name> [--init] [--json] [--spec-dir <path>]\n 跑机械探针(TODO 标记/测试覆盖/API 对账/删除对账)输出 markdown;--init 生成 verify-result.md 骨架(探针预填,已存在不覆盖)');
777
+ process.exit(2);
778
+ }
779
+ assertSafeChangeName(vpChange, '--change 变更名');
780
+ const { runVerifyProbes, renderVerifyProbesReport, generateVerifyResultSkeleton } = await import('./verify-probes.js');
781
+ const vpResult = runVerifyProbes({ cwd: dir, changeName: vpChange, specDir });
782
+ if (json) {
783
+ console.log(JSON.stringify({ command: 'verify-probes', change: vpChange, ...vpResult }, null, 2));
784
+ break;
785
+ }
786
+ console.log(renderVerifyProbesReport(vpResult));
787
+ if (vpInit) {
788
+ const vpSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
789
+ const vpReportPath = join(vpSpecBase, 'changes', vpChange, 'verify-result.md');
790
+ if (existsSync(vpReportPath)) {
791
+ console.log(`\nℹ️ verify-result.md 已存在,不覆盖: ${vpReportPath}`);
792
+ } else {
793
+ writeFileSync(vpReportPath, generateVerifyResultSkeleton(vpResult));
794
+ console.log(`\n📄 已生成 verify-result.md 骨架: ${vpReportPath}(探针已预填;结论必须写明 PASS/FAIL,留待填会被 gate 判不过)`);
795
+ }
796
+ }
797
+ break;
798
+ }
799
+ case 'module-impact': {
800
+ // archive 模块影响矩阵骨架(2026-08-21 审计第三批 D3):文件×模块归属按 _module-map.yaml
801
+ // paths 前缀匹配预填(机械),影响类型/review 标记留 <!--TODO-->(语义)。已存在不覆盖。
802
+ const miChangeIdx = args.indexOf('--change');
803
+ const miChange = miChangeIdx >= 0 && args[miChangeIdx + 1] ? args[miChangeIdx + 1] : null;
804
+ if (!miChange) {
805
+ console.error('用法: sillyspec module-impact --change <name> [--json] [--spec-dir <path>]\n 生成 module-impact.md 骨架(模块影响矩阵按 module-map 预填 + 未匹配文件清单;已存在不覆盖)');
806
+ process.exit(2);
807
+ }
808
+ assertSafeChangeName(miChange, '--change 变更名');
809
+ const { generateModuleImpactSkeleton } = await import('./module-impact.js');
810
+ const miResult = generateModuleImpactSkeleton({ cwd: dir, changeName: miChange, specDir });
811
+ if (!miResult) {
812
+ console.log('ℹ️ 无 module-map 或无 diff 文件可归类——骨架无从生成,agent 全手写(模块索引缺失可先跑 sillyspec modules rebuild)');
813
+ break;
814
+ }
815
+ const miSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
816
+ const miPath = join(miSpecBase, 'changes', miChange, 'module-impact.md');
817
+ if (json) {
818
+ console.log(JSON.stringify({ command: 'module-impact', change: miChange, path: miPath, ...miResult }, null, 2));
819
+ break;
820
+ }
821
+ if (existsSync(miPath)) {
822
+ console.log(`ℹ️ module-impact.md 已存在,不覆盖: ${miPath}`);
823
+ break;
824
+ }
825
+ writeFileSync(miPath, miResult.markdown);
826
+ console.log(`✅ 已生成 module-impact.md 骨架: ${miPath}`);
827
+ console.log(` 归类 ${miResult.matchedCount} 个文件,未匹配 ${miResult.unmatchedCount} 个;影响类型列逐行替换 <!--TODO-->。`);
828
+ break;
829
+ }
830
+ case 'endpoints': {
831
+ // endpoints artifact 生成(2026-08-21 审计第三批 H6):后端 router task 的 endpoints.json
832
+ // 此前靠 agent 手扫装饰器手写(易漏 endpoint)。CLI 复用 endpoint-extractor 全套提取器
833
+ // (FastAPI @router.* / Express router.* / Spring @*Mapping)静态扫描生成,
834
+ // verify 探针 5(verifyApiParity)直接消费该产物。
835
+ const epSub = filteredArgs[1];
836
+ if (epSub !== 'extract') {
837
+ console.error('用法: sillyspec endpoints extract --change <name> [--task task-NN] [--dir <backendRoot> | --files <a.py,b.js...>] [--spec-dir <path>]\n 静态扫描后端路由装饰器生成 endpoints.json(FastAPI/Express/Spring);verify 探针 5 消费');
838
+ process.exit(2);
839
+ }
840
+ const epChangeIdx = args.indexOf('--change');
841
+ const epChange = epChangeIdx >= 0 && args[epChangeIdx + 1] ? args[epChangeIdx + 1] : null;
842
+ const epTaskIdx = args.indexOf('--task');
843
+ const epTask = epTaskIdx >= 0 && args[epTaskIdx + 1] ? args[epTaskIdx + 1] : null;
844
+ const epDirIdx = args.indexOf('--dir');
845
+ const epDir = epDirIdx >= 0 && args[epDirIdx + 1] ? args[epDirIdx + 1] : null;
846
+ const epFilesIdx = args.indexOf('--files');
847
+ const epFilesRaw = epFilesIdx >= 0 && args[epFilesIdx + 1] ? args[epFilesIdx + 1] : null;
848
+ if (!epChange) {
849
+ console.error('❌ 缺 --change <变更名>');
850
+ process.exit(2);
851
+ }
852
+ assertSafeChangeName(epChange, '--change 变更名');
853
+ const { extractFastApiEndpoints, extractExpressEndpoints, extractSpringEndpoints, scanBackendEndpoints } = await import('./endpoint-extractor.js');
854
+ const endpoints = [];
855
+ const epSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
856
+ const epChangeDir = join(epSpecBase, 'changes', epChange);
857
+ if (!existsSync(epChangeDir)) {
858
+ console.error(`❌ 变更目录不存在: ${epChangeDir}`);
859
+ process.exit(1);
860
+ }
861
+ const collectFile = (f) => {
862
+ const abs = resolve(f)
863
+ if (!existsSync(abs)) return
864
+ const ext = extname(abs).toLowerCase()
865
+ try {
866
+ if (ext === '.py') endpoints.push(...extractFastApiEndpoints(abs))
867
+ else if (ext === '.js' || ext === '.ts') endpoints.push(...extractExpressEndpoints(abs))
868
+ else if (ext === '.java') endpoints.push(...extractSpringEndpoints(abs))
869
+ } catch { /* 单文件解析失败不阻断整体 */ }
870
+ }
871
+ if (epDir) {
872
+ endpoints.push(...scanBackendEndpoints(resolve(epDir)))
873
+ } else if (epFilesRaw) {
874
+ for (const f of epFilesRaw.split(',').map(s => s.trim()).filter(Boolean)) collectFile(f)
875
+ } else {
876
+ // 默认扫描面:task 卡 allowed_paths ∪ design §6 清单里的具体源码文件
877
+ const { parseFileChangeListDetailed } = await import('./change-list.js')
878
+ const candidates = new Set()
879
+ if (epTask) {
880
+ const cardPath = join(epChangeDir, 'tasks', `${epTask}.md`)
881
+ if (existsSync(cardPath)) {
882
+ const { parseAllowedPaths } = await import('./stages/plan-postcheck.js')
883
+ for (const p of parseAllowedPaths(readFileSync(cardPath, 'utf8'))) candidates.add(p)
884
+ }
885
+ }
886
+ const designPath = join(epChangeDir, 'design.md')
887
+ if (existsSync(designPath)) {
888
+ for (const e of parseFileChangeListDetailed(designPath)) {
889
+ if (!e.path.startsWith('.sillyspec/') && !/[*?[\]]/.test(e.path)) candidates.add(e.path)
890
+ }
891
+ }
892
+ let scanned = 0
893
+ for (const p of candidates) {
894
+ const abs = join(dir, p)
895
+ if (!existsSync(abs)) continue
896
+ scanned++
897
+ collectFile(p)
898
+ }
899
+ if (candidates.size === 0) {
900
+ console.error('❌ 无扫描面:--task 卡无 allowed_paths 且 design.md 无具体文件清单——显式传 --dir <backendRoot> 或 --files <a.py,b.js>');
901
+ process.exit(1);
902
+ }
903
+ console.log(`ℹ️ 扫描 ${scanned}/${candidates.size} 个清单文件(不存在的跳过)`);
904
+ }
905
+ const artifact = {
906
+ generated_at: new Date().toISOString().slice(0, 19).replace('T', ' '),
907
+ generator: 'sillyspec endpoints extract',
908
+ endpoints: endpoints.map(e => ({ method: e.method, path: e.path, source: e.source })),
909
+ }
910
+ const epRuntimeRoot = (await import('./run/shared.js')).resolveRuntimeRoot({ specRoot: specDir }, epSpecBase);
911
+ const epOutDir = join(epRuntimeRoot, 'contract-artifacts', epChange, epTask || 'scan');
912
+ mkdirSync(epOutDir, { recursive: true });
913
+ const epOutPath = join(epOutDir, 'endpoints.json');
914
+ writeFileSync(epOutPath, JSON.stringify(artifact, null, 2) + '\n');
915
+ console.log(`✅ 提取 ${endpoints.length} 个端点 → ${epOutPath}`);
916
+ console.log(` verify 阶段探针 5(API 契约对账)自动消费该产物;FastAPI/Express/Spring 装饰器均支持。`);
917
+ break;
918
+ }
919
+ case 'scan-fix-headers': {
920
+ // scan 文档 header 幂等补填(2026-08-21 审计第三批 E3):scan-postcheck 对缺
921
+ // author/created_at 只 warning,agent 手改还常忘。本命令补齐(git user + 当前时间),
922
+ // 已有两键的文件不动;有 frontmatter 就地插入,无则补一段。
923
+ const sfhProjectIdx = args.indexOf('--project');
924
+ const sfhProject = sfhProjectIdx >= 0 && args[sfhProjectIdx + 1] ? args[sfhProjectIdx + 1] : null;
925
+ const { fixScanDocHeaders } = await import('./scan-postcheck.js');
926
+ const sfhResult = fixScanDocHeaders({ cwd: dir, specDir, project: sfhProject });
927
+ if (json) {
928
+ console.log(JSON.stringify({ command: 'scan-fix-headers', ...sfhResult }, null, 2));
929
+ break;
930
+ }
931
+ if (sfhResult.fixed.length > 0) {
932
+ for (const f of sfhResult.fixed) console.log(`✅ 已补 header: ${f}`);
933
+ }
934
+ if (sfhResult.skipped.length > 0) console.log(`ℹ️ ${sfhResult.skipped.length} 份已含 author/created_at,未动`);
935
+ if (sfhResult.fixed.length === 0 && sfhResult.skipped.length === 0) console.log('📭 未找到 scan 文档(无 docs/<project>/scan/ 目录)');
936
+ break;
937
+ }
938
+ case 'symbol-impact': {
939
+ // symbol-impact.md 骨架生成(2026-08-21 审计项⑤「报错即生成」的命令入口):gate 此前
940
+ // 只报错不代填 → agent 从零手写整份报告,忘一个 task 就被拦。本命令从 tasks.md(回退
941
+ // plan.md)注册表生成逐 task <!--TODO--> 骨架;gate 拒绝未替换的占位(防骨架直接过门),
942
+ // agent 只需逐行填结论。已存在不覆盖(幂等,agent 产出优先)。
943
+ const siChangeIdx = args.indexOf('--change');
944
+ const siChange = siChangeIdx >= 0 && args[siChangeIdx + 1] ? args[siChangeIdx + 1] : null;
945
+ if (!siChange) {
946
+ console.error('用法: sillyspec symbol-impact --change <name> [--spec-dir <path>]\n 生成 symbol-impact.md 逐 task <!--TODO--> 骨架(已存在不覆盖);gate 拒绝未替换的占位行');
947
+ process.exit(2);
948
+ }
949
+ assertSafeChangeName(siChange, '--change 变更名');
950
+ const siSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
951
+ const siChangeDir = join(siSpecBase, 'changes', siChange);
952
+ const siTasksPath = join(siChangeDir, 'tasks.md');
953
+ const siPlanPath = join(siChangeDir, 'plan.md');
954
+ const siRegistryPath = existsSync(siTasksPath) ? siTasksPath : (existsSync(siPlanPath) ? siPlanPath : null);
955
+ if (!siRegistryPath) {
956
+ console.error(`❌ 变更目录缺 tasks.md/plan.md: ${siChangeDir}`);
957
+ process.exit(1);
958
+ }
959
+ const { generateSymbolImpactSkeleton } = await import('./run/gates.js');
960
+ const siSkeleton = generateSymbolImpactSkeleton(readFileSync(siRegistryPath, 'utf8'));
961
+ if (!siSkeleton) {
962
+ console.error(`❌ 任务清单无 checkbox task 行: ${siRegistryPath}`);
963
+ process.exit(1);
964
+ }
965
+ const siReportPath = join(siChangeDir, 'symbol-impact.md');
966
+ if (existsSync(siReportPath)) {
967
+ console.log(`ℹ️ symbol-impact.md 已存在,不覆盖: ${siReportPath}`);
968
+ break;
969
+ }
970
+ writeFileSync(siReportPath, siSkeleton);
971
+ console.log(`✅ 已生成逐 task 骨架: ${siReportPath}`);
972
+ console.log(' 逐行替换 <!--TODO--> 为结论(无签名级变更也显式写「无」);gate 拒绝未替换的占位行。');
563
973
  break;
564
974
  }
565
975
  case 'register-stage-review': {
@@ -574,8 +984,14 @@ async function main() {
574
984
  const rsrStage = rsrStageIdx >= 0 && args[rsrStageIdx + 1] ? args[rsrStageIdx + 1] : null;
575
985
  const rsrFromIdx = args.indexOf('--from');
576
986
  const rsrFrom = rsrFromIdx >= 0 && args[rsrFromIdx + 1] ? args[rsrFromIdx + 1] : null;
577
- if (!rsrChange || !rsrStage) {
578
- console.error('用法: sillyspec register-stage-review --change <名> --stage <brainstorm|plan|execute> [--from <review.json>] [--spec-dir <path>] [--json]\n 生成/adopt stage 级 review.json(docHash 自动算 + 写 marker),治 tier=independent marker 死锁');
987
+ // docHash 联动(坑 stage-review-dochash-manual-resync,2026-08-21 实证):改一版 design 要
988
+ // 重算 2-3 个 stage review 的 docHash,手工易漏、gate 报错才补。--refresh-hash 就地刷新单个
989
+ // 既有 review 的 hash(保留结论);--all 一次处理三个 stage(有 review 刷 hash,无 review
990
+ // 生成骨架——改版后一条命令完成全部联动)。
991
+ const rsrRefreshHash = filteredArgs.includes('--refresh-hash');
992
+ const rsrAll = filteredArgs.includes('--all');
993
+ if (!rsrChange || (!rsrStage && !rsrAll) || (rsrAll && rsrStage)) {
994
+ console.error('用法: sillyspec register-stage-review --change <名> (--stage <brainstorm|plan|execute> | --all) [--from <review.json>] [--refresh-hash] [--spec-dir <path>] [--json]\n 生成/adopt/刷新 stage 级 review.json(docHash 自动算 + 写 marker);--all 一次处理三个 stage(有 review 刷新 hash、无 review 生成骨架);--refresh-hash 就地刷新既有 review 的 docHash(保留 verdict 待确认续用)');
579
995
  process.exit(2);
580
996
  }
581
997
  // 与 run 入口同源消毒(防路径穿越;register 下游写 marker、拼 changes/review 路径)
@@ -583,20 +999,38 @@ async function main() {
583
999
  const { registerStageReview } = await import('./stage-review.js');
584
1000
  const rsrPlatformOpts = {};
585
1001
  if (specDir) rsrPlatformOpts.specRoot = specDir;
586
- try {
587
- const result = registerStageReview({ changeName: rsrChange, stage: rsrStage, fromFile: rsrFrom, cwd: dir, platformOpts: rsrPlatformOpts });
588
- if (json) {
589
- process.stdout.write(JSON.stringify({ ok: true, command: 'register-stage-review', change: rsrChange, stage: rsrStage, ...result }));
590
- } else {
591
- console.log(`✅ 已注册 ${rsrStage} stage review [${result.reviewRunId}] → ${result.reviewPath}(mode: ${result.mode})`);
592
- console.log(` marker → ${result.markerPath}`);
593
- console.log(` 下一步:独立审查子代理对照 ${result.mainDoc} 填 verdict/checklist 后重跑 --done`);
1002
+ const rsrStages = rsrAll ? ['brainstorm', 'plan', 'execute'] : [rsrStage];
1003
+ const rsrResults = [];
1004
+ let rsrFailed = 0;
1005
+ for (const st of rsrStages) {
1006
+ try {
1007
+ // --all 的单 stage 语义:有既有 review → refresh-hash;无 → 骨架。显式 --refresh-hash
1008
+ // 则强制 refresh(无既有 review 时报错,提示先生成骨架)
1009
+ const { getLatestStageReviewRunId, stageReviewMarkerPath } = await import('./stage-review.js');
1010
+ const { resolveRuntimeRoot } = await import('./run/shared.js');
1011
+ const rsrRuntimeRoot = resolveRuntimeRoot(rsrPlatformOpts, rsrPlatformOpts.specRoot || join(dir, '.sillyspec'));
1012
+ const hasExisting = !!getLatestStageReviewRunId(rsrRuntimeRoot, st, rsrChange);
1013
+ const wantRefresh = rsrRefreshHash || (rsrAll && hasExisting);
1014
+ const result = registerStageReview({ changeName: rsrChange, stage: st, fromFile: rsrFrom, cwd: dir, platformOpts: rsrPlatformOpts, refreshHash: wantRefresh });
1015
+ rsrResults.push({ stage: st, ...result });
1016
+ if (json) {
1017
+ process.stdout.write(JSON.stringify({ ok: true, command: 'register-stage-review', change: rsrChange, stage: st, ...result }) + '\n');
1018
+ } else {
1019
+ console.log(`✅ 已注册 ${st} stage review [${result.reviewRunId}] → ${result.reviewPath}(mode: ${result.mode})`);
1020
+ console.log(` marker → ${result.markerPath}`);
1021
+ if (result.mode === 'refreshed') {
1022
+ console.log(` docHash 已按当前 ${result.mainDoc} 重算(verdict 结论是否仍适用需人工确认)`);
1023
+ } else {
1024
+ console.log(` 下一步:独立审查子代理对照 ${result.mainDoc} 填 verdict/checklist 后重跑 --done`);
1025
+ }
1026
+ }
1027
+ } catch (e) {
1028
+ rsrFailed++;
1029
+ if (json) process.stdout.write(JSON.stringify({ ok: false, command: 'register-stage-review', change: rsrChange, stage: st, error: e.message }) + '\n');
1030
+ else console.error(`❌ [${st}] ` + e.message);
594
1031
  }
595
- } catch (e) {
596
- if (json) process.stdout.write(JSON.stringify({ ok: false, command: 'register-stage-review', error: e.message }));
597
- else console.error('❌ ' + e.message);
598
- process.exitCode = 1;
599
1032
  }
1033
+ if (rsrFailed > 0) process.exitCode = 1;
600
1034
  break;
601
1035
  }
602
1036
  case 'docs': {
@@ -1179,6 +1613,17 @@ SillySpec worktree — git worktree 隔离管理
1179
1613
  // 模块文档(.sillyspec/docs/)= 交付物,会 auto-apply 到主工作区;变更文档/运行时/quicklog 不 apply。
1180
1614
  // 须告知 agent 哪些未落地,否则它期望这些文件落地却找不到(memory: worktree-apply-excludes-module-docs)。
1181
1615
  console.log(` ℹ️ 注:.sillyspec/changes、.sillyspec/.runtime、.sillyspec/quicklog 下的文件按规则不自动 apply(模块文档 .sillyspec/docs/ 会自动 apply),如需请手动从 worktree 分支或 dangling commit 取(git show sillyspec/${wtName}:<path>)。`);
1616
+ // 提交复用 pathspec(坑 apply-commit-pathspec-sweep):主仓无关未提交文件易被目录级
1617
+ // git add 误扫——给精确清单,勿用 git add <目录>/ 全扫
1618
+ if (Array.isArray(result.commitPathspec) && result.commitPathspec.length > 0 && result.pathspecFile) {
1619
+ console.log(`\n📌 提交时用本变更精确 pathspec(勿目录级 add 误扫无关文件):`);
1620
+ if (result.commitPathspec.length <= 30) {
1621
+ console.log(` git add -- ${result.commitPathspec.join(' ')}`);
1622
+ } else {
1623
+ console.log(` git add --pathspec-from-file="${result.pathspecFile}"`);
1624
+ }
1625
+ console.log(` (完整清单 ${result.commitPathspec.length} 个文件已存 ${result.pathspecFile},可 --pathspec-from-file 复用)`);
1626
+ }
1182
1627
  }
1183
1628
  if (result.warnings && result.warnings.length > 0) {
1184
1629
  for (const w of result.warnings) {
@@ -2013,8 +2458,52 @@ SillySpec modules — 模块文档管理
2013
2458
  // 本地配置探测(task-04 / D-001@v1):纯 fs 嗅探项目类型 → 生成 local.yaml。
2014
2459
  // 轻量独立路由,不跑 scan、不消耗 token。探测逻辑归属 local-detect.js(task-02)。
2015
2460
  const localSubCmd = filteredArgs[1];
2461
+ // register-repo(2026-08-21 审计项④):跨仓 repo key 注册命令化。此前靠 agent 手编
2462
+ // local.yaml repos: 段(正是 agent 满目录找/根目录乱建 local.yaml 的上游);execute
2463
+ // 启动 fail-closed 报错已列出缺的 key,本命令外科手术式写入——只动 repos: 段,
2464
+ // 注释/凭据段逐行保留。写目标解析 worktree→主仓(config-cat.js 同源候选链)。
2465
+ if (localSubCmd === 'register-repo') {
2466
+ const rrKey = filteredArgs[2];
2467
+ const rrPath = filteredArgs[3];
2468
+ if (!rrKey || !rrPath || rrPath.startsWith('-')) {
2469
+ console.error('用法: sillyspec local register-repo <key> <path>\n 注册跨仓仓到 local.yaml repos: 段(跨仓 task 卡 repo: 的 key 必须先注册,execute 启动 fail-closed 校验)\n key 限字母数字._-;path 为跨仓仓根目录(相对路径按当前 cwd 解析,须是 git 仓);main 隐式不用注册');
2470
+ process.exit(2);
2471
+ }
2472
+ let rrSpecBase = null;
2473
+ try {
2474
+ rrSpecBase = resolvePlatformSpecDir(dir, specDir);
2475
+ } catch (e) {
2476
+ // fail-closed:平台 pointer 异常时不盲写(写错 local.yaml 比不写更糟),报修复引导
2477
+ console.error(`❌ 无法解析 local.yaml 目标路径(平台指针异常): ${e.message}`);
2478
+ process.exit(1);
2479
+ }
2480
+ const absRepo = resolve(rrPath);
2481
+ if (!existsSync(absRepo)) {
2482
+ console.error(`❌ 路径不存在: ${absRepo}`);
2483
+ process.exit(1);
2484
+ }
2485
+ if (!(await import('./git-helper.js')).gitQuiet(absRepo, ['rev-parse', '--git-dir'])) {
2486
+ console.error(`❌ 不是 git 仓库: ${absRepo}(repos: 段注册的跨仓仓必须可 rev-parse,MultiRepoContext 会在其中跑 git)`);
2487
+ process.exit(1);
2488
+ }
2489
+ const { resolveLocalYamlWriteTarget } = await import('./config-cat.js');
2490
+ const target = resolveLocalYamlWriteTarget(dir, { specBase: rrSpecBase });
2491
+ const { registerRepoInLocalYaml } = await import('./local-register.js');
2492
+ let rrResult;
2493
+ try {
2494
+ rrResult = registerRepoInLocalYaml(target.path, rrKey, absRepo);
2495
+ } catch (e) {
2496
+ console.error(`❌ 注册失败: ${e.message}`);
2497
+ process.exit(1);
2498
+ }
2499
+ const verb = rrResult.replaced ? '已更新(覆盖旧路径)' : '已注册';
2500
+ console.log(`✅ repos.${rrKey} ${verb}: ${absRepo.replace(/\\/g, '/')}`);
2501
+ console.log(` 写入: ${target.path}${rrResult.fileCreated ? '(新建文件)' : ''}${rrResult.sectionCreated ? '(新建 repos: 段)' : ''}`);
2502
+ console.log(' 查看: sillyspec config cat(真实配置); main 隐式=主仓不用注册');
2503
+ break;
2504
+ }
2016
2505
  if (localSubCmd !== 'detect') {
2017
- console.error('用法: sillyspec local detect [--dir <path>]\n 纯 fs 嗅探项目类型并生成 local.yaml(不跑 scan、零 token)');
2506
+ console.error('用法: sillyspec local detect [--dir <path>]\n sillyspec local register-repo <key> <path>\n detect: 纯 fs 嗅探项目类型并生成 local.yaml(不跑 scan、零 token)\n register-repo: 注册跨仓仓到 local.yaml repos: 段(外科手术式写入,其余内容保留)');
2018
2507
  process.exit(2);
2019
2508
  }
2020
2509
 
@@ -2075,19 +2564,56 @@ SillySpec config — local.yaml 配置键速查
2075
2564
  sillyspec config schema 同上(显式子命令)
2076
2565
  sillyspec config --json 机读 JSON(程序化消费)
2077
2566
  sillyspec config schema --json 同上
2567
+ sillyspec config cat 读取 local.yaml 实际内容(候选链:spec 根 → cwd 祖先链 →
2568
+ git common-dir 主仓根;worktree 内 .sillyspec 是 checkout
2569
+ 副本无此文件,cat 自动解析到主仓)
2570
+ sillyspec config cat --json 机读 JSON(dir/path/source/content;未找到退出 1)
2078
2571
 
2079
2572
  说明:
2080
2573
  - 数据源:src/config-schema.js(唯一真相;reader 见各键「读取点」)
2081
2574
  - 键分两类:【生效】配了即生效;【声明但未接线】代码/JSDoc 提及但无 reader,配了不生效
2082
2575
  - 脱敏示例文件:sillyspec init 生成 local.yaml.example(可提交;真实 local.yaml 是 gitignored)
2576
+ - local.yaml 恒在 .sillyspec/ 下——项目根目录没有这个文件,别去那里找(写 .sillyspec/
2577
+ 之外的同名文件会被 hook 拦截)
2083
2578
  `);
2084
2579
  break;
2085
2580
  }
2581
+ // config cat(2026-08-21 root-local-yaml 治理):schema 只列键不含值,agent 拿实际配置
2582
+ // 只能翻文件——worktree 内 .sillyspec 是 checkout 副本(gitignored 的 local.yaml 不随
2583
+ // checkout 出现),找不到就漂去项目根乱找/乱建。cat = 唯一权威读取口,按「文件存在」
2584
+ // 解析真实路径(config-cat.js:spec 根 → cwd 祖先链 → git common-dir 主仓根)。
2585
+ // agent 本就有 shell 权限可自行 cat,CLI 回显明文不增加暴露面。
2586
+ if (configSub === 'cat') {
2587
+ let specBase = null
2588
+ try { specBase = resolvePlatformSpecDir(dir, specDir) } catch { /* 平台 pointer 异常 → 走祖先链(真实 reader 各自 fail-closed,cat 只读不加重) */ }
2589
+ const { resolveLocalYaml } = await import('./config-cat.js')
2590
+ const r = resolveLocalYaml(dir, { specBase })
2591
+ if (json) {
2592
+ const payload = { cmd: 'config cat', dir: resolve(dir), exists: r.exists, path: r.path, source: r.source, searched: r.searched }
2593
+ if (r.exists) payload.content = readFileSync(r.path, 'utf8')
2594
+ console.log(JSON.stringify(payload, null, 2))
2595
+ if (!r.exists) process.exitCode = 1
2596
+ break
2597
+ }
2598
+ if (!r.exists) {
2599
+ console.error(`📭 未找到 local.yaml(候选链均无此文件:${r.searched.slice(0, 3).join(' ; ')} …)`)
2600
+ console.error(' 注意:local.yaml 恒在 .sillyspec/ 下,项目根目录没有这个文件。')
2601
+ console.error(' 生成:sillyspec local detect(本地嗅探)/ sillyspec platform connect(平台凭据)')
2602
+ console.error(' 可用键清单:sillyspec config schema')
2603
+ process.exitCode = 1
2604
+ break
2605
+ }
2606
+ console.log(`📄 ${r.path}(来源:${r.source})`)
2607
+ const content = readFileSync(r.path, 'utf8')
2608
+ process.stdout.write(content)
2609
+ if (!content.endsWith('\n')) process.stdout.write('\n')
2610
+ break;
2611
+ }
2086
2612
  if (!isSchema) {
2087
- const sug = didYouMean(configSub, ['schema']);
2613
+ const sug = didYouMean(configSub, ['schema', 'cat']);
2088
2614
  console.error(`❌ 未知子命令: config ${configSub}`);
2089
2615
  if (sug) console.error(` 你是想输入「config ${sug}」吗?`);
2090
- console.error('用法: sillyspec config [schema] [--json] 打印 local.yaml 配置键');
2616
+ console.error('用法: sillyspec config [schema|cat] [--json] 打印 local.yaml 配置键 / 读取实际内容');
2091
2617
  process.exit(2);
2092
2618
  }
2093
2619
  const { renderSchemaHuman, renderSchemaJson } = await import('./config-schema.js');