sillyspec 3.25.8 → 3.25.11

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 (71) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +8 -13
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +8 -6
  3. package/.claude/skills/sillyspec-continue/SKILL.md +3 -3
  4. package/.claude/skills/sillyspec-doctor/SKILL.md +1 -1
  5. package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
  6. package/.claude/skills/sillyspec-plan/SKILL.md +1 -1
  7. package/.claude/skills/sillyspec-verify/SKILL.md +1 -1
  8. package/README.md +9 -0
  9. package/package.json +2 -2
  10. package/src/change-list.js +11 -8
  11. package/src/change-risk-profile.js +22 -0
  12. package/src/db.js +160 -159
  13. package/src/dispatch/backends/local-agent.js +152 -0
  14. package/src/dispatch/backends/sillyhub-mcp.js +242 -0
  15. package/src/dispatch/probe.js +208 -0
  16. package/src/dispatch/strategy.js +121 -0
  17. package/src/doctor-diagnostics.js +19 -25
  18. package/src/fs-atomic.js +69 -69
  19. package/src/git-helper.js +66 -0
  20. package/src/hooks/worktree-guard.js +64 -55
  21. package/src/index.js +215 -34
  22. package/src/init.js +7 -14
  23. package/src/machine-interface.js +54 -19
  24. package/src/migrate.js +14 -4
  25. package/src/modules.js +16 -5
  26. package/src/progress/change-registry.js +72 -70
  27. package/src/progress/consistency-doctor.js +71 -14
  28. package/src/progress/shared.js +6 -3
  29. package/src/progress/stage-machine.js +61 -65
  30. package/src/progress/step-store.js +62 -67
  31. package/src/progress.js +155 -233
  32. package/src/quicklog.js +13 -5
  33. package/src/run/command.js +65 -37
  34. package/src/run/complete-handlers.js +1005 -889
  35. package/src/run/complete.js +115 -208
  36. package/src/run/concurrent-detect.js +132 -0
  37. package/src/run/gates.js +289 -17
  38. package/src/run/prompt.js +22 -68
  39. package/src/run/quick-audit.js +10 -9
  40. package/src/run/shared.js +648 -600
  41. package/src/run/stage.js +43 -24
  42. package/src/scan-postcheck.js +9 -31
  43. package/src/sillyhub-mcp/client.js +389 -0
  44. package/src/spec-dir-typo.js +71 -0
  45. package/src/stage-contract-engine.js +22 -1
  46. package/src/stage-contract-spec.js +11 -4
  47. package/src/stage-contract.js +26 -3
  48. package/src/stage-review.js +13 -9
  49. package/src/stages/archive.js +4 -6
  50. package/src/stages/brainstorm-auto.js +26 -20
  51. package/src/stages/brainstorm.js +29 -5
  52. package/src/stages/cmd-existence.js +145 -0
  53. package/src/stages/doctor.js +9 -6
  54. package/src/stages/execute.js +61 -20
  55. package/src/stages/plan-postcheck.js +152 -0
  56. package/src/stages/plan.js +16 -31
  57. package/src/stages/quick.js +1 -1
  58. package/src/stages/scan.js +9 -3
  59. package/src/stages/status.js +0 -1
  60. package/src/stages/verify.js +12 -7
  61. package/src/sync.js +4 -4
  62. package/src/task-review.js +30 -1
  63. package/src/verify-postcheck.js +933 -862
  64. package/src/version.js +19 -0
  65. package/src/worktree-apply.js +85 -68
  66. package/src/worktree-deps.js +89 -5
  67. package/src/worktree.js +213 -111
  68. package/templates/prompts/taskcard-rules.md +27 -0
  69. package/templates/prompts/verify-probes.md +3 -3
  70. package/.claude/skills/sillyspec-propose/SKILL.md +0 -87
  71. package/src/stages/propose.js +0 -181
package/src/index.js CHANGED
@@ -10,14 +10,14 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync,
10
10
  import { writeAtomicSync } from './fs-atomic.js';
11
11
  import { join, resolve } from 'path';
12
12
  import { execSync } from 'child_process';
13
- import { cmdInit, getVersion } from './init.js';
13
+ import { git } from './git-helper.js';
14
+ import { getVersion } from './version.js';
14
15
  import { ProgressManager, resolvePlatformSpecDir } from './progress.js';
15
- import { detectLocalYaml } from './local-detect.js';
16
16
 
17
17
  // ── CLI 入口 ──
18
18
 
19
19
  // did-you-mean 从 ./run/shared.js 复用(命令级 + flag 级 typo 建议)。
20
- import { didYouMean, assertSafeChangeName } from './run/shared.js'
20
+ import { didYouMean, assertSafeChangeName, resolveSpecDir, detectWorktreeSpecDrift } from './run/shared.js'
21
21
  function printUsage() {
22
22
  console.log(`
23
23
  SillySpec CLI — 规范驱动开发工具包
@@ -76,12 +76,14 @@ SillySpec CLI — 规范驱动开发工具包
76
76
 
77
77
  sillyspec local detect [--dir <path>] 生成本地配置 local.yaml(纯 fs 嗅探,零 token、不跑 scan)
78
78
  sillyspec runtime list [--json] 枚举 .sillyspec/.runtime/ 运行时产物(只读,看手上有哪些证据/状态文件)
79
+ sillyspec dispatch <probe | hint> SillyHub 派发能力探测 + 策略生成(agent 调用桥,仅渲染不执行 tool)
79
80
 
80
81
  sillyspec workflow check <name> [--project <p>] [--change <c>] [--json] [--save]
81
82
  全局 workflow 校验(--save 归档到 .sillyspec/.runtime/workflow-runs/)
82
83
  sillyspec workflow list
83
84
  sillyspec gate <stage> --change <name> [--json] 机器门控:阶段能否标记完成(只读)
84
85
  sillyspec derive <facet> --change <name> [--json] 单项事实核验(facet: execute-evidence|verify-test|task-reviews|artifacts)
86
+ sillyspec backfill-reviews --change <name> [--json] 为手动补的 task 生成 review.json 草稿(cannot_verify,解 archive 客观完成度阻断)
85
87
 
86
88
  sillyspec doctor [子命令] 进度库健康检查 + 修复(顶层命令,非 worktree doctor)
87
89
  (无子命令) 跑诊断,列出问题
@@ -227,7 +229,7 @@ async function main() {
227
229
 
228
230
  switch (command) {
229
231
  case 'init':
230
- await cmdInit(dir, { tool, interactive, specDir });
232
+ await (await import('./init.js')).cmdInit(dir, { tool, interactive, specDir });
231
233
  break;
232
234
  case 'setup':
233
235
  const setupList = filteredArgs.includes('--list') || filteredArgs.includes('-l');
@@ -252,7 +254,7 @@ async function main() {
252
254
  pm.show(progDir, progChangeName);
253
255
  break;
254
256
  case 'check': {
255
- const result = await pm.checkConsistency(progDir, progChangeName);
257
+ const result = pm.checkConsistency(progDir, progChangeName);
256
258
  // checkConsistency 只返回 {ok, issues, warnings},不打印——必须在此输出,
257
259
  // 否则 agent 跑只读健康检查得到一片寂静,无法分辨「正常」还是「已损坏」。
258
260
  if (!result || result.ok) {
@@ -266,11 +268,11 @@ async function main() {
266
268
  }
267
269
  case 'repair': {
268
270
  const repairApply = filteredArgs.includes('--apply');
269
- await pm.repairConsistency(progDir, { apply: repairApply, changeName: progChangeName });
271
+ pm.repairConsistency(progDir, { apply: repairApply, changeName: progChangeName });
270
272
  break;
271
273
  }
272
274
  case 'validate':
273
- await pm.validate(dir, progChangeName);
275
+ pm.validate(dir, progChangeName);
274
276
  break;
275
277
  case 'reset':
276
278
  pm.reset(dir, stage, progChangeName);
@@ -298,18 +300,18 @@ async function main() {
298
300
  if (args[ai] === '--output' && args[ai + 1]) { updOutput = args[ai + 1]; ai++; }
299
301
  }
300
302
  const updForce = args.includes('--force');
301
- await pm.updateStep(dir, updStepStage, updStepName, { status: updStatus, output: updOutput, force: updForce }, progChangeName);
303
+ pm.updateStep(dir, updStepStage, updStepName, { status: updStatus, output: updOutput, force: updForce }, progChangeName);
302
304
  break;
303
305
  }
304
306
  case 'complete-stage': {
305
307
  const compStageName = filteredArgs[2];
306
308
  if (!compStageName) { console.log('❌ 用法: sillyspec progress complete-stage <stage> [--force]'); break; }
307
- await pm.completeStage(dir, compStageName, progChangeName, { force: args.includes('--force') });
309
+ pm.completeStage(dir, compStageName, progChangeName, { force: args.includes('--force') });
308
310
  break;
309
311
  }
310
312
  case 'batch': {
311
313
  if (filteredArgs.includes('--status')) {
312
- const bp = await pm.readBatchProgress(dir, progChangeName);
314
+ const bp = pm.readBatchProgress(dir, progChangeName);
313
315
  if (!bp) { console.log('📭 无批量进度数据'); break; }
314
316
  const line = pm._renderBatchProgress(bp);
315
317
  console.log(line || '📭 无批量进度数据');
@@ -328,7 +330,7 @@ async function main() {
328
330
  console.log(' sillyspec progress batch --status');
329
331
  break;
330
332
  }
331
- await pm.updateBatchProgress(dir, batchData, progChangeName);
333
+ pm.updateBatchProgress(dir, batchData, progChangeName);
332
334
  console.log('✅ 批量进度已更新');
333
335
  }
334
336
  break;
@@ -355,6 +357,15 @@ async function main() {
355
357
  // resolveSpecDir(cwd),向后兼容。
356
358
  const gateOpts = { cwd: dir };
357
359
  if (specDir) gateOpts.specBase = specDir;
360
+ // drift 锚定(坑 execute-runs-isolation,对齐 command.js execute drift 守卫 + machine-interface
361
+ // runGate 已扩展的 specDriftAnchor 入参):gate 是顶层命令不走 runCommand/drift 守卫,worktree cwd
362
+ // 下 execute 段 marker 读取会落副本 .runtime。仅在未显式 --spec-dir 时算(对齐守卫条件 !specDir),
363
+ // 命中则 specDriftAnchor 锚主仓——runGate execute 段的 resolveRuntimeRoot 读主仓
364
+ // current-execute-run-id marker,不读 worktree 副本(副本随 cleanup 整目录删消失)。
365
+ if (!specDir) {
366
+ const wt = detectWorktreeSpecDrift(resolveSpecDir(dir));
367
+ if (wt) gateOpts.specDriftAnchor = wt.mainSpecBase;
368
+ }
358
369
  const { envelope, exitCode } = await withJsonOutput(json, () => runGate(gateStage, gateChange, gateOpts));
359
370
  if (json) {
360
371
  process.stdout.write(JSON.stringify(envelope));
@@ -389,6 +400,13 @@ async function main() {
389
400
  // 下需透传;不传 --spec-dir 时走默认 resolveSpecDir(cwd),向后兼容。
390
401
  const deriveOpts = { cwd: dir };
391
402
  if (specDir) deriveOpts.specBase = specDir;
403
+ // drift 锚定(同 gate case,对齐 command.js drift 守卫 + machine-interface runDerive 的 specDriftAnchor
404
+ // 入参):derive 顶层命令不走 drift 守卫,worktree cwd 下 task-reviews facet 的 execute-run-id marker
405
+ // 读取会落副本。未显式 --spec-dir 时算 anchor,runDerive task-reviews 的 resolveRuntimeRoot 读主仓 marker。
406
+ if (!specDir) {
407
+ const wt = detectWorktreeSpecDrift(resolveSpecDir(dir));
408
+ if (wt) deriveOpts.specDriftAnchor = wt.mainSpecBase;
409
+ }
392
410
  const { envelope, exitCode } = await withJsonOutput(json, () => runDerive(facet, deriveChange, deriveOpts));
393
411
  if (json) {
394
412
  process.stdout.write(JSON.stringify(envelope));
@@ -402,6 +420,44 @@ async function main() {
402
420
  process.exitCode = exitCode;
403
421
  break;
404
422
  }
423
+ case 'backfill-reviews': {
424
+ // 手动补 task(reopen execute / 直接实现)缺 review.json 的官方入口(坑 verify-archive-flow-pitfalls 坑2)。
425
+ // 复用 generateTaskReviewDrafts(execute --done 同源兜底,幂等、fail-open):据 git diff base..head + working-tree
426
+ // 按 task allowed_paths 归属生成 cannot_verify 草稿,agent 复核后升级 pass/fail。execute 完成后手动补的 task
427
+ // 无子代理 review 落盘 → archive step1 客观完成度(真相源=review.json verdict)判缺 → 阻断归档;本命令补齐
428
+ // 缺失草稿,独立可随时跑(不必再 execute --done)。缺数据(无 tasks/ 目录 / 改动未 commit / 无 worktree meta)
429
+ // 时 generateTaskReviewDrafts 提前返回 reason,如实打印不报错。
430
+ const brChangeIdx = args.indexOf('--change');
431
+ const brChange = brChangeIdx >= 0 && args[brChangeIdx + 1] ? args[brChangeIdx + 1] : null;
432
+ if (!brChange) {
433
+ console.error('用法: sillyspec backfill-reviews --change <name> [--json] [--spec-dir <path>]\n 为手动补的 task 生成 review.json 草稿(cannot_verify),解 archive 客观完成度阻断');
434
+ process.exit(2);
435
+ }
436
+ const { generateTaskReviewDrafts } = await import('./task-review.js');
437
+ // --spec-dir 透传 platformOpts.specRoot(与 gate/derive 对称);不传走默认 join(cwd,'.sillyspec')。
438
+ const brPlatformOpts = {};
439
+ if (specDir) brPlatformOpts.specRoot = specDir;
440
+ const result = await generateTaskReviewDrafts({ changeName: brChange, cwd: dir, platformOpts: brPlatformOpts });
441
+ if (json) {
442
+ process.stdout.write(JSON.stringify({ ok: true, command: 'backfill-reviews', change: brChange, ...result }));
443
+ } else {
444
+ if (result.generated > 0) {
445
+ console.log('📄 已补写 ' + result.generated + ' 个 per-task review.json 草稿(cannot_verify,需 agent 复核后升级 pass/fail) [' + brChange + ']');
446
+ } else {
447
+ console.log('ℹ️ 无草稿可补 [' + brChange + ']' + (result.reason ? ':' + result.reason : ''));
448
+ }
449
+ if (result.skipped > 0) {
450
+ console.log(' 跳过 ' + result.skipped + ' 个 task(review.json 已存在 / 空 diff 无从归属)');
451
+ }
452
+ if (result.unattributed && result.unattributed.length > 0) {
453
+ console.warn(' ⚠️ ' + result.unattributed.length + ' 个变更文件未归属任何 task(顺带修复/非源码),草稿未覆盖:' + result.unattributed.join(', '));
454
+ }
455
+ if (result.executeRunId) {
456
+ console.log(' execute run id: ' + result.executeRunId);
457
+ }
458
+ }
459
+ break;
460
+ }
405
461
  case 'docs': {
406
462
  const docsSubCmd = filteredArgs[1];
407
463
  if (docsSubCmd === 'migrate') {
@@ -474,7 +530,7 @@ async function main() {
474
530
  const specBase = resolvePlatformSpecDir(doctorEffectiveDir, specDir) || join(doctorEffectiveDir, '.sillyspec');
475
531
  let r;
476
532
  try {
477
- r = await pm.alignExecuteToPlan(doctorEffectiveDir, alignChange, specBase, { confirm: doctorConfirm });
533
+ r = pm.alignExecuteToPlan(doctorEffectiveDir, alignChange, specBase, { confirm: doctorConfirm });
478
534
  } catch (e) {
479
535
  console.error(`❌ 对齐失败:${e.message}`);
480
536
  process.exitCode = 1;
@@ -595,9 +651,9 @@ async function main() {
595
651
  const pm = new ProgressManager({ specDir: resolvePlatformSpecDir(dir, specDir) });
596
652
 
597
653
  // isolation 写入 DB 的辅助函数
598
- async function _writeIsolationToDB(cwd, changeName, info) {
654
+ function _writeIsolationToDB(cwd, changeName, info) {
599
655
  if (info.blocked) {
600
- await pm.updateChangeIsolation(cwd, changeName, {
656
+ pm.updateChangeIsolation(cwd, changeName, {
601
657
  status: 'blocked',
602
658
  mode: null,
603
659
  reason: info.reason,
@@ -605,7 +661,7 @@ async function main() {
605
661
  } else {
606
662
  const mode = info.mode || 'worktree';
607
663
  const statusMap = { 'worktree': 'verified', 'native-worktree': 'verified', 'in-place-fallback': 'degraded' };
608
- await pm.updateChangeIsolation(cwd, changeName, {
664
+ pm.updateChangeIsolation(cwd, changeName, {
609
665
  status: statusMap[mode] || 'verified',
610
666
  mode,
611
667
  });
@@ -620,10 +676,11 @@ SillySpec worktree — git worktree 隔离管理
620
676
  sillyspec worktree create <change-name> [--base <branch>] 创建隔离 worktree
621
677
  sillyspec worktree apply <change-name> [--check-only] 校验并应用变更到主工作区
622
678
  sillyspec worktree assess <change-name> 风险审计 + 自动 apply
679
+ sillyspec worktree diff <change-name> [--base <commit>] 查看 worktree 相对 base 的变更
623
680
  sillyspec worktree list 列出所有活跃 worktree
624
681
  sillyspec worktree meta <change-name> 读取 worktree meta.json
625
682
  sillyspec worktree cleanup <change-name> [--force] 强制清理 worktree
626
- sillyspec worktree doctor [--fix] [--stale-hours N] 健康检查 + 修复
683
+ sillyspec worktree doctor [--fix] [--stale-hours N] [--change <name>] 健康检查 + 修复(--change 仅扫指定 change)
627
684
 
628
685
  选项:
629
686
  --base <branch> create: 指定基础分支(默认当前 HEAD)
@@ -652,11 +709,11 @@ SillySpec worktree — git worktree 隔离管理
652
709
  if (info.mode) {
653
710
  console.log(` 模式: ${info.mode}`);
654
711
  }
655
- // 写入 isolation 信息到 gate-status.json
712
+ // 写入 isolation 信息到 sillyspec.db(_writeIsolationToDB)
656
713
  await _writeIsolationToDB(dir, wtName, info);
657
714
  } catch (e) {
658
715
  console.error(`❌ ${e.message}`);
659
- // 写入 blocked 状态到 gate-status.json
716
+ // 写入 blocked 状态到 sillyspec.db(_writeIsolationToDB)
660
717
  await _writeIsolationToDB(dir, wtName, { blocked: true, reason: e.message });
661
718
  process.exit(1);
662
719
  }
@@ -697,10 +754,10 @@ SillySpec worktree — git worktree 隔离管理
697
754
  for (const f of result.changedFiles) {
698
755
  console.log(` ${f}`);
699
756
  }
700
- // filterDeliverableFiles 一刀切排除 .sillyspec/ + meta.json:
701
- // 模块文档/变更文档等不会 auto-apply 到主工作区。须告知 agent,
702
- // 否则它期望这些文件落地却找不到(memory: worktree-apply-excludes-module-docs)。
703
- console.log(` ℹ️ 注:.sillyspec/ 下的文件(模块文档/变更文档等)按规则不自动 apply,如需请手动从 worktree 分支或 dangling commit 取(git show sillyspec/${wtName}:<path>)。`);
757
+ // filterDeliverableFiles 精细化排除(保留 .sillyspec/docs/,排 changes/+.runtime/+quicklog/ + meta.json):
758
+ // 模块文档(.sillyspec/docs/)= 交付物,会 auto-apply 到主工作区;变更文档/运行时/quicklog 不 apply。
759
+ // 须告知 agent 哪些未落地,否则它期望这些文件落地却找不到(memory: worktree-apply-excludes-module-docs)。
760
+ console.log(` ℹ️ 注:.sillyspec/changes、.sillyspec/.runtime、.sillyspec/quicklog 下的文件按规则不自动 apply(模块文档 .sillyspec/docs/ 会自动 apply),如需请手动从 worktree 分支或 dangling commit 取(git show sillyspec/${wtName}:<path>)。`);
704
761
  }
705
762
  if (result.warnings && result.warnings.length > 0) {
706
763
  for (const w of result.warnings) {
@@ -748,7 +805,7 @@ SillySpec worktree — git worktree 隔离管理
748
805
  } else {
749
806
  console.log(`✅ 已自动应用 ${applyResult.changedFiles.length} 个文件变更:`);
750
807
  for (const f of applyResult.changedFiles) console.log(` ${f}`);
751
- console.log(` ℹ️ 注:.sillyspec/ 下的文件按规则不自动 apply,如需请手动从分支取(git show sillyspec/${wtName}:<path>)。`);
808
+ console.log(` ℹ️ 注:.sillyspec/changes、.sillyspec/.runtime、.sillyspec/quicklog 下的文件按规则不自动 apply(模块文档 .sillyspec/docs/ 会自动 apply),如需请手动从分支取(git show sillyspec/${wtName}:<path>)。`);
752
809
  }
753
810
  } else {
754
811
  console.log('Action: blocked');
@@ -779,6 +836,42 @@ SillySpec worktree — git worktree 隔离管理
779
836
  console.log(JSON.stringify(meta, null, 2));
780
837
  break;
781
838
  }
839
+ case 'diff': {
840
+ if (!wtName) {
841
+ console.error('❌ 用法: sillyspec worktree diff <change-name> [--base <commit>]');
842
+ process.exit(1);
843
+ }
844
+ const meta = wm.getMeta(wtName);
845
+ if (!meta) {
846
+ console.error(`❌ 未找到 worktree meta: ${wtName}(可能未创建或已被清理)`);
847
+ process.exit(1);
848
+ }
849
+ if (!meta.worktreePath || !existsSync(meta.worktreePath)) {
850
+ console.error(`❌ worktree 目录不存在: ${meta.worktreePath || '(未设置)'}(可能已被清理,可跑 sillyspec worktree doctor)`);
851
+ process.exit(1);
852
+ }
853
+ // base 优先级:显式 --base > meta.baseHash(create 时锚的主仓 HEAD)> 当前 HEAD。
854
+ // diff 比较 worktree 工作区(含未提交)vs base,与 _changesAlreadyOnMain 同口径。
855
+ const baseIdx = filteredArgs.indexOf('--base');
856
+ const explicitBase = baseIdx >= 0 ? filteredArgs[baseIdx + 1] : null;
857
+ const base = explicitBase || meta.baseHash || 'HEAD';
858
+ let out = '';
859
+ try {
860
+ // base 作为独立 argv 元素传入,不经 shell 拆词/注入;worktreePath 经 cwd,不插值。
861
+ // trim:false 保留 git diff 原始输出(含尾换行)供 stdout.write;空检查靠临时 .trim()。
862
+ // timeout 30s:diff 属潜在长操作(对齐 design R3 与 verify-postcheck 口径)。
863
+ out = git(meta.worktreePath, ['--no-pager', 'diff', '--no-renames', base], { trim: false, timeout: 30000 });
864
+ } catch (e) {
865
+ console.error(`❌ git diff 失败(base=${String(base).slice(0, 8)}): ${e.message}`);
866
+ process.exit(1);
867
+ }
868
+ if (!out.trim()) {
869
+ console.log(`(无变更:worktree 工作区与 base ${String(base).slice(0, 8)} 一致)`);
870
+ } else {
871
+ process.stdout.write(out);
872
+ }
873
+ break;
874
+ }
782
875
  case 'list': {
783
876
  const items = wm.list();
784
877
  if (items.length === 0) {
@@ -829,7 +922,9 @@ SillySpec worktree — git worktree 隔离管理
829
922
  const fixFlag = args.includes('--fix');
830
923
  const staleIdx = args.indexOf('--stale-hours');
831
924
  const staleHours = staleIdx !== -1 && args[staleIdx + 1] ? parseInt(args[staleIdx + 1], 10) : 24;
832
- const diag = wm.doctor({ fix: fixFlag, staleHours });
925
+ const changeIdx = args.indexOf('--change');
926
+ const changeName = changeIdx !== -1 && args[changeIdx + 1] ? args[changeIdx + 1] : null;
927
+ const diag = wm.doctor({ fix: fixFlag, staleHours, changeName });
833
928
  if (diag.issues.length === 0) {
834
929
  console.log('✅ worktree 健康检查通过,无异常');
835
930
  } else {
@@ -860,6 +955,83 @@ SillySpec worktree — git worktree 隔离管理
860
955
  }
861
956
  break;
862
957
  }
958
+ case 'dispatch': {
959
+ // SillyHub 派发抽象层的 agent 调用桥(design.md §Phase2 / D-007@v1):
960
+ // 仅做能力探测(probe)与派发策略生成(hint),**不执行任何 tool 调用**——
961
+ // 实际 tool 调用(本机 Agent tool / SillyHub MCP tool)由 agent 据指令执行。
962
+ // 仿 worktree 子命令的参数解析(filteredArgs[1] 取子命令)与错误处理模式。
963
+ const dispatchSubCmd = filteredArgs[1];
964
+ const dispatchSubs = ['probe', 'hint'];
965
+
966
+ if (!dispatchSubCmd || dispatchSubCmd === 'help' || dispatchSubCmd === '--help' || dispatchSubCmd === '-h') {
967
+ console.log(`
968
+ SillySpec dispatch — SillyHub 派发能力探测与策略生成(agent 调用桥,仅渲染与探测)
969
+
970
+ 用法:
971
+ sillyspec dispatch probe [--json] 能力探测,输出 ProbeResult
972
+ sillyspec dispatch hint --contract <json> [--json] 派发策略,输出 {instruction, backend}
973
+
974
+ 子命令:
975
+ probe 探测 SillyHub MCP 是否可用(无配置返回 available=false,正常降级不报错)
976
+ hint 据 contract + probe 结果生成派发指令文本(agent 拿指令后执行实际 tool 调用)
977
+
978
+ 选项:
979
+ --contract <json> hint 必填:DispatchContract(brief/worktreePath/branch/allowedPaths/readOnly/...)
980
+ --json 程序化输出(probe → ProbeResult;hint → {backend, instruction})
981
+ `);
982
+ break;
983
+ }
984
+
985
+ if (dispatchSubCmd === 'probe') {
986
+ const { probeSillyHub } = await import('./dispatch/probe.js');
987
+ const result = await probeSillyHub();
988
+ // result = { available, reason? };无配置时 {available:false,reason:'no-config'} 不报错(正常降级)
989
+ if (json) {
990
+ process.stdout.write(JSON.stringify(result));
991
+ } else if (result.available) {
992
+ console.log('✅ SillyHub 可用');
993
+ } else {
994
+ console.log(`⚠️ SillyHub 不可用:${result.reason || 'unknown'}`);
995
+ }
996
+ break;
997
+ }
998
+
999
+ if (dispatchSubCmd === 'hint') {
1000
+ // 解析 --contract <json>(仿 worktree 用 args.indexOf 取 flag 后一个值)
1001
+ const contractIdx = args.indexOf('--contract');
1002
+ const contractRaw = contractIdx >= 0 && args[contractIdx + 1] ? args[contractIdx + 1] : null;
1003
+ if (!contractRaw) {
1004
+ console.error('用法: sillyspec dispatch hint --contract <json> [--json]\n 生成派发指令(DispatchContract:brief/worktreePath/branch/allowedPaths/readOnly/...)');
1005
+ process.exit(2);
1006
+ }
1007
+ let contract;
1008
+ try {
1009
+ contract = JSON.parse(contractRaw);
1010
+ } catch (e) {
1011
+ console.error(`❌ --contract JSON 解析失败: ${e.message}`);
1012
+ process.exit(2);
1013
+ }
1014
+ const { probeSillyHub } = await import('./dispatch/probe.js');
1015
+ const { renderDispatchInstruction } = await import('./dispatch/strategy.js');
1016
+ const probe = await probeSillyHub();
1017
+ const { instruction, backend } = renderDispatchInstruction(contract, probe);
1018
+ if (json) {
1019
+ process.stdout.write(JSON.stringify({ backend, instruction }));
1020
+ } else {
1021
+ console.log(`Backend: ${backend}`);
1022
+ console.log(instruction);
1023
+ }
1024
+ break;
1025
+ }
1026
+
1027
+ // 未知子命令 → did-you-mean 建议 + 退出 1(仿 worktree default)
1028
+ const sug = didYouMean(dispatchSubCmd, dispatchSubs);
1029
+ console.error(`❌ 未知子命令: dispatch ${dispatchSubCmd}`);
1030
+ if (sug) console.error(` 你是想输入「dispatch ${sug}」吗?`);
1031
+ console.error(` 可用子命令:${dispatchSubs.join(' | ')}(运行 sillyspec dispatch 查看用法)`);
1032
+ process.exit(1);
1033
+ break;
1034
+ }
863
1035
  case 'platform': {
864
1036
  const platformSub = filteredArgs[1];
865
1037
  const platformArgs = filteredArgs.slice(2);
@@ -1019,7 +1191,7 @@ SillySpec platform — SillyHub 平台同步
1019
1191
  process.exit(2);
1020
1192
  }
1021
1193
  const pm = new ProgressManager({ specDir: resolvePlatformSpecDir(dir, specDir) });
1022
- await pm.renameChange(dir, oldName, newName);
1194
+ pm.renameChange(dir, oldName, newName);
1023
1195
  break;
1024
1196
  }
1025
1197
  case 'workflow': {
@@ -1164,7 +1336,7 @@ SillySpec workflow — 工作流管理
1164
1336
  SillySpec modules — 模块文档管理
1165
1337
 
1166
1338
  用法:
1167
- sillyspec modules rebuild 从模块卡片 + 源码重建 _module-map.yaml
1339
+ sillyspec modules rebuild [--force] 从模块卡片 + 源码重建 _module-map.yaml(默认 dry-run 预览,--force 才覆盖)
1168
1340
  sillyspec modules status 显示模块索引状态
1169
1341
  sillyspec modules migrate 旧格式模块文档迁移到新格式
1170
1342
  `);
@@ -1172,7 +1344,8 @@ SillySpec modules — 模块文档管理
1172
1344
  }
1173
1345
  if (modulesSub === 'rebuild') {
1174
1346
  const { rebuildModuleMap } = await import('./modules.js');
1175
- await rebuildModuleMap(dir);
1347
+ const isForce = filteredArgs.includes('--force');
1348
+ await rebuildModuleMap(dir, { force: isForce });
1176
1349
  } else if (modulesSub === 'status') {
1177
1350
  const { showModuleStatus } = await import('./modules.js');
1178
1351
  await showModuleStatus(dir);
@@ -1198,7 +1371,7 @@ SillySpec modules — 模块文档管理
1198
1371
  process.exit(2);
1199
1372
  }
1200
1373
 
1201
- const detected = detectLocalYaml(dir);
1374
+ const detected = (await import('./local-detect.js')).detectLocalYaml(dir);
1202
1375
  const specRoot = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
1203
1376
  const localYamlPath = join(specRoot, 'local.yaml');
1204
1377
 
@@ -1241,7 +1414,7 @@ SillySpec modules — 模块文档管理
1241
1414
  }
1242
1415
  case 'runtime': {
1243
1416
  // D2:.runtime/ 产物索引。CLI 往 .runtime/ 写各种证据/状态文件(db、doctor-diagnosis、
1244
- // workflow-runs、gate-status、user-inputs…),却没有命令让 agent 一眼看到「我手上有哪些产物」。
1417
+ // workflow-runs、user-inputs…),却没有命令让 agent 一眼看到「我手上有哪些产物」。
1245
1418
  // 多会话/压缩后尤其痛:上轮写的证据文件,下轮不知去哪找。纯只读枚举,不写盘。
1246
1419
  const runtimeSubCmd = filteredArgs[1];
1247
1420
  if (runtimeSubCmd && runtimeSubCmd !== 'list') {
@@ -1266,7 +1439,6 @@ SillySpec modules — 模块文档管理
1266
1439
  'doctor-diagnosis.json': 'doctor --json 结构化诊断快照',
1267
1440
  'doctor-dumps': 'doctor --dump-db 取证输出目录',
1268
1441
  'workflow-runs': 'workflow check --save 归档目录',
1269
- 'gate-status.json': '门控结果缓存',
1270
1442
  'scan-guard.json': 'scan 覆盖保护记录',
1271
1443
  'quick-sessions': 'quick 会话记录目录',
1272
1444
  'quick-guard.json': 'quick baseline 守卫(旧位置)',
@@ -1309,10 +1481,15 @@ SillySpec modules — 模块文档管理
1309
1481
  break;
1310
1482
  }
1311
1483
  default: {
1312
- const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'local', 'workflow', 'gate', 'derive', 'modules', 'change-rename', 'knowledge', 'platform', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime'];
1484
+ const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'dispatch', 'local', 'workflow', 'gate', 'derive', 'backfill-reviews', 'modules', 'change-rename', 'knowledge', 'platform', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime'];
1313
1485
  const suggestion = didYouMean(command, topCommands);
1314
1486
  console.error(`❌ 未知命令: ${command}`);
1315
- if (suggestion) console.error(` 你是想输入「${suggestion}」吗?`);
1487
+ if (command === '--status') {
1488
+ // --status 是阶段内 flag(sillyspec run <stage> --status),裸用会落到 default
1489
+ console.error(' --status 是阶段内 flag。查看进度请用:sillyspec status 或 sillyspec progress show');
1490
+ } else if (suggestion) {
1491
+ console.error(` 你是想输入「${suggestion}」吗?`);
1492
+ }
1316
1493
  printUsage();
1317
1494
  process.exit(1);
1318
1495
  }
@@ -1325,6 +1502,10 @@ main().catch((err) => {
1325
1502
  console.error(`❌ ${err.message}`);
1326
1503
  process.exit(1);
1327
1504
  }
1328
- console.error(err);
1329
- process.exit(1);
1505
+ // message 提首行(Agent 可直接解析 stderr 首行判断成败),堆栈降级为附件;
1506
+ // 用 exitCode 而非 exit(1) 让事件循环自然 drain,缓解 Windows libuv teardown
1507
+ // 把退出码污染成 127(与 shell "command not found" 撞码)的问题。
1508
+ console.error(`❌ SillySpec 发生错误:${err?.message || err}`);
1509
+ if (err?.stack) console.error(err.stack);
1510
+ process.exitCode = 1;
1330
1511
  });
package/src/init.js CHANGED
@@ -4,6 +4,10 @@ import { fileURLToPath } from 'url';
4
4
  import { checkbox, confirm, input } from '@inquirer/prompts';
5
5
  import { ProgressManager } from './progress.js';
6
6
  import chalk from 'chalk';
7
+ import { getVersion } from './version.js';
8
+ // 向后兼容:getVersion 已抽到轻量 version.js(避免 index.js 为 --version 静态加载 init.js 的 inquirer 税),
9
+ // 此处 re-export 保持 init.js 既有导出 API 不破坏(如 test/init-claude-injection.test.mjs)。
10
+ export { getVersion };
7
11
 
8
12
  const __filename = fileURLToPath(import.meta.url);
9
13
  const __dirname = dirname(__filename);
@@ -12,7 +16,7 @@ const __dirname = dirname(__filename);
12
16
  // 平台模式(specRoot 指向外部)下,源码目录的 .sillyspec/.runtime/ 是旧残留,
13
17
  // 应让 specRoot 成为唯一运行时根。但若源码目录的 .sillyspec/ 本身就是该项目
14
18
  // 的 specDir(含真实资产),其 .runtime/ 里的 worktrees/、sillyspec.db、
15
- // global.json、gate-status.json 是真实工作状态,整删会摧毁 worktree meta、
19
+ // global.json 是真实工作状态,整删会摧毁 worktree meta、
16
20
  // 导致 depsStatus 恒为 unknown、branch already exists 死循环。
17
21
  // 详见 docs/sillyspec/runtime-cleanup-destroys-worktree-meta.md
18
22
  //
@@ -21,7 +25,6 @@ const RUNTIME_KEEP = new Set([
21
25
  'worktrees', // worktree 目录 + meta.json(worktree.js:17)
22
26
  'sillyspec.db', // SQLite 进度库(权威状态源,progress.js:7)
23
27
  'global.json', // 项目名/活跃变更缓存(progress.js:8)
24
- 'gate-status.json', // worktree-guard 门禁状态(progress.js:9)
25
28
  'contract-artifacts', // execute endpoint 契约(verify 阶段读取)
26
29
  'execute-runs', // task review 结果(task-review.js)
27
30
  ]);
@@ -209,7 +212,7 @@ function isTTY() {
209
212
 
210
213
  // ── 核心安装逻辑 ──
211
214
 
212
- async function doInstall(projectDir, tools, subprojects = [], specDir = null) {
215
+ function doInstall(projectDir, tools, subprojects = [], specDir = null) {
213
216
  // specDir: 规范目录(默认 projectDir/.sillyspec)
214
217
  // projectDir: 源码项目根目录(用于工具检测、指令注入、.gitignore)
215
218
  const spec = specDir || join(projectDir, '.sillyspec');
@@ -308,7 +311,7 @@ async function doInstall(projectDir, tools, subprojects = [], specDir = null) {
308
311
 
309
312
  // 初始化 SQLite 数据库
310
313
  const pm = new ProgressManager({ specDir: spec });
311
- await pm.init(projectDir);
314
+ pm.init(projectDir);
312
315
 
313
316
  // .gitignore 只在 specDir 在项目内时才修改
314
317
  const isExternalSpec = specDir && resolve(spec) !== resolve(projectDir, '.sillyspec');
@@ -389,16 +392,6 @@ function showSummary(version, tools, specDir) {
389
392
  console.log('');
390
393
  }
391
394
 
392
- // ── 读取版本号 ──
393
-
394
- export function getVersion() {
395
- try {
396
- const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf8'));
397
- return pkg.version;
398
- } catch {
399
- return '?.?.?';
400
- }
401
- }
402
395
 
403
396
  // ── 主命令 ──
404
397