sillyspec 3.25.9 → 3.26.0

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 (76) 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-quick/SKILL.md +3 -1
  8. package/.claude/skills/sillyspec-verify/SKILL.md +1 -1
  9. package/.sillyspec-platform-cleaned +1 -0
  10. package/.sillyspec-platform.json +7 -0
  11. package/README.md +9 -0
  12. package/package.json +2 -2
  13. package/src/change-list.js +11 -8
  14. package/src/change-risk-profile.js +22 -0
  15. package/src/db.js +170 -161
  16. package/src/dispatch/backends/local-agent.js +152 -0
  17. package/src/dispatch/backends/sillyhub-mcp.js +242 -0
  18. package/src/dispatch/probe.js +210 -0
  19. package/src/dispatch/strategy.js +121 -0
  20. package/src/doctor-diagnostics.js +19 -25
  21. package/src/fs-atomic.js +69 -69
  22. package/src/git-helper.js +66 -0
  23. package/src/hooks/worktree-guard.js +64 -55
  24. package/src/index.js +326 -36
  25. package/src/init.js +7 -14
  26. package/src/local-detect.js +120 -110
  27. package/src/machine-interface.js +54 -19
  28. package/src/migrate.js +14 -4
  29. package/src/modules.js +16 -5
  30. package/src/progress/change-registry.js +85 -70
  31. package/src/progress/consistency-doctor.js +71 -14
  32. package/src/progress/shared.js +8 -5
  33. package/src/progress/stage-machine.js +67 -65
  34. package/src/progress/step-store.js +72 -67
  35. package/src/progress.js +448 -236
  36. package/src/quicklog.js +59 -12
  37. package/src/run/command.js +78 -39
  38. package/src/run/complete-handlers.js +1028 -889
  39. package/src/run/complete.js +132 -212
  40. package/src/run/concurrent-detect.js +132 -0
  41. package/src/run/gates.js +302 -19
  42. package/src/run/prompt.js +22 -68
  43. package/src/run/quick-audit.js +10 -9
  44. package/src/run/shared.js +722 -600
  45. package/src/run/stage.js +43 -24
  46. package/src/scan-postcheck.js +9 -31
  47. package/src/sillyhub-mcp/client.js +394 -0
  48. package/src/sillyhub-mcp/config.js +62 -0
  49. package/src/spec-dir-typo.js +71 -0
  50. package/src/stage-contract-engine.js +22 -1
  51. package/src/stage-contract-spec.js +11 -4
  52. package/src/stage-contract.js +26 -3
  53. package/src/stage-review.js +118 -10
  54. package/src/stages/archive.js +4 -6
  55. package/src/stages/brainstorm-auto.js +26 -20
  56. package/src/stages/brainstorm.js +29 -5
  57. package/src/stages/cmd-existence.js +145 -0
  58. package/src/stages/doctor.js +10 -7
  59. package/src/stages/execute.js +64 -22
  60. package/src/stages/plan-postcheck.js +152 -0
  61. package/src/stages/plan.js +16 -31
  62. package/src/stages/quick.js +25 -9
  63. package/src/stages/scan.js +53 -8
  64. package/src/stages/status.js +0 -1
  65. package/src/stages/verify.js +14 -9
  66. package/src/sync.js +433 -16
  67. package/src/task-review.js +59 -1
  68. package/src/verify-postcheck.js +938 -862
  69. package/src/version.js +19 -0
  70. package/src/worktree-apply.js +218 -93
  71. package/src/worktree-deps.js +89 -5
  72. package/src/worktree.js +213 -111
  73. package/templates/prompts/taskcard-rules.md +27 -0
  74. package/templates/prompts/verify-probes.md +3 -3
  75. package/.claude/skills/sillyspec-propose/SKILL.md +0 -87
  76. 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,6 +76,7 @@ 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/)
@@ -83,6 +84,8 @@ SillySpec CLI — 规范驱动开发工具包
83
84
  sillyspec gate <stage> --change <name> [--json] 机器门控:阶段能否标记完成(只读)
84
85
  sillyspec derive <facet> --change <name> [--json] 单项事实核验(facet: execute-evidence|verify-test|task-reviews|artifacts)
85
86
  sillyspec backfill-reviews --change <name> [--json] 为手动补的 task 生成 review.json 草稿(cannot_verify,解 archive 客观完成度阻断)
87
+ sillyspec register-stage-review --change <name> --stage <brainstorm|plan|execute> [--from <review.json>] [--json]
88
+ 生成/adopt stage 级 review.json(docHash 自动算 + 写 marker,治 tier=independent marker 死锁)
86
89
 
87
90
  sillyspec doctor [子命令] 进度库健康检查 + 修复(顶层命令,非 worktree doctor)
88
91
  (无子命令) 跑诊断,列出问题
@@ -228,7 +231,7 @@ async function main() {
228
231
 
229
232
  switch (command) {
230
233
  case 'init':
231
- await cmdInit(dir, { tool, interactive, specDir });
234
+ await (await import('./init.js')).cmdInit(dir, { tool, interactive, specDir });
232
235
  break;
233
236
  case 'setup':
234
237
  const setupList = filteredArgs.includes('--list') || filteredArgs.includes('-l');
@@ -253,7 +256,7 @@ async function main() {
253
256
  pm.show(progDir, progChangeName);
254
257
  break;
255
258
  case 'check': {
256
- const result = await pm.checkConsistency(progDir, progChangeName);
259
+ const result = pm.checkConsistency(progDir, progChangeName);
257
260
  // checkConsistency 只返回 {ok, issues, warnings},不打印——必须在此输出,
258
261
  // 否则 agent 跑只读健康检查得到一片寂静,无法分辨「正常」还是「已损坏」。
259
262
  if (!result || result.ok) {
@@ -267,11 +270,11 @@ async function main() {
267
270
  }
268
271
  case 'repair': {
269
272
  const repairApply = filteredArgs.includes('--apply');
270
- await pm.repairConsistency(progDir, { apply: repairApply, changeName: progChangeName });
273
+ pm.repairConsistency(progDir, { apply: repairApply, changeName: progChangeName });
271
274
  break;
272
275
  }
273
276
  case 'validate':
274
- await pm.validate(dir, progChangeName);
277
+ pm.validate(dir, progChangeName);
275
278
  break;
276
279
  case 'reset':
277
280
  pm.reset(dir, stage, progChangeName);
@@ -299,18 +302,18 @@ async function main() {
299
302
  if (args[ai] === '--output' && args[ai + 1]) { updOutput = args[ai + 1]; ai++; }
300
303
  }
301
304
  const updForce = args.includes('--force');
302
- await pm.updateStep(dir, updStepStage, updStepName, { status: updStatus, output: updOutput, force: updForce }, progChangeName);
305
+ pm.updateStep(dir, updStepStage, updStepName, { status: updStatus, output: updOutput, force: updForce }, progChangeName);
303
306
  break;
304
307
  }
305
308
  case 'complete-stage': {
306
309
  const compStageName = filteredArgs[2];
307
310
  if (!compStageName) { console.log('❌ 用法: sillyspec progress complete-stage <stage> [--force]'); break; }
308
- await pm.completeStage(dir, compStageName, progChangeName, { force: args.includes('--force') });
311
+ pm.completeStage(dir, compStageName, progChangeName, { force: args.includes('--force') });
309
312
  break;
310
313
  }
311
314
  case 'batch': {
312
315
  if (filteredArgs.includes('--status')) {
313
- const bp = await pm.readBatchProgress(dir, progChangeName);
316
+ const bp = pm.readBatchProgress(dir, progChangeName);
314
317
  if (!bp) { console.log('📭 无批量进度数据'); break; }
315
318
  const line = pm._renderBatchProgress(bp);
316
319
  console.log(line || '📭 无批量进度数据');
@@ -329,7 +332,7 @@ async function main() {
329
332
  console.log(' sillyspec progress batch --status');
330
333
  break;
331
334
  }
332
- await pm.updateBatchProgress(dir, batchData, progChangeName);
335
+ pm.updateBatchProgress(dir, batchData, progChangeName);
333
336
  console.log('✅ 批量进度已更新');
334
337
  }
335
338
  break;
@@ -356,6 +359,15 @@ async function main() {
356
359
  // resolveSpecDir(cwd),向后兼容。
357
360
  const gateOpts = { cwd: dir };
358
361
  if (specDir) gateOpts.specBase = specDir;
362
+ // drift 锚定(坑 execute-runs-isolation,对齐 command.js execute drift 守卫 + machine-interface
363
+ // runGate 已扩展的 specDriftAnchor 入参):gate 是顶层命令不走 runCommand/drift 守卫,worktree cwd
364
+ // 下 execute 段 marker 读取会落副本 .runtime。仅在未显式 --spec-dir 时算(对齐守卫条件 !specDir),
365
+ // 命中则 specDriftAnchor 锚主仓——runGate execute 段的 resolveRuntimeRoot 读主仓
366
+ // current-execute-run-id marker,不读 worktree 副本(副本随 cleanup 整目录删消失)。
367
+ if (!specDir) {
368
+ const wt = detectWorktreeSpecDrift(resolveSpecDir(dir));
369
+ if (wt) gateOpts.specDriftAnchor = wt.mainSpecBase;
370
+ }
359
371
  const { envelope, exitCode } = await withJsonOutput(json, () => runGate(gateStage, gateChange, gateOpts));
360
372
  if (json) {
361
373
  process.stdout.write(JSON.stringify(envelope));
@@ -390,6 +402,13 @@ async function main() {
390
402
  // 下需透传;不传 --spec-dir 时走默认 resolveSpecDir(cwd),向后兼容。
391
403
  const deriveOpts = { cwd: dir };
392
404
  if (specDir) deriveOpts.specBase = specDir;
405
+ // drift 锚定(同 gate case,对齐 command.js drift 守卫 + machine-interface runDerive 的 specDriftAnchor
406
+ // 入参):derive 顶层命令不走 drift 守卫,worktree cwd 下 task-reviews facet 的 execute-run-id marker
407
+ // 读取会落副本。未显式 --spec-dir 时算 anchor,runDerive task-reviews 的 resolveRuntimeRoot 读主仓 marker。
408
+ if (!specDir) {
409
+ const wt = detectWorktreeSpecDrift(resolveSpecDir(dir));
410
+ if (wt) deriveOpts.specDriftAnchor = wt.mainSpecBase;
411
+ }
393
412
  const { envelope, exitCode } = await withJsonOutput(json, () => runDerive(facet, deriveChange, deriveOpts));
394
413
  if (json) {
395
414
  process.stdout.write(JSON.stringify(envelope));
@@ -441,6 +460,41 @@ async function main() {
441
460
  }
442
461
  break;
443
462
  }
463
+ case 'register-stage-review': {
464
+ // 手动注册 stage 级 review(brainstorm/plan/execute-acceptance)的确定性 writer ——
465
+ // 生成 run 目录 + review.json 骨架(cannot_verify,待独立审查子代理填 verdict)或 adopt
466
+ // agent 草稿(--from,保留 verdict/checklist 重算 docHash)+ 写 marker。治 tier=independent 时
467
+ // 调度者手动派独立子代理不写 marker 的死锁。docHash 由 CLI computeDocHash 算(部分实现 P6.1b defer)。
468
+ // 纯新增,不改 gate 语义。与 task 级 backfill-reviews 对称(stage 级等价物)。
469
+ const rsrChangeIdx = args.indexOf('--change');
470
+ const rsrChange = rsrChangeIdx >= 0 && args[rsrChangeIdx + 1] ? args[rsrChangeIdx + 1] : null;
471
+ const rsrStageIdx = args.indexOf('--stage');
472
+ const rsrStage = rsrStageIdx >= 0 && args[rsrStageIdx + 1] ? args[rsrStageIdx + 1] : null;
473
+ const rsrFromIdx = args.indexOf('--from');
474
+ const rsrFrom = rsrFromIdx >= 0 && args[rsrFromIdx + 1] ? args[rsrFromIdx + 1] : null;
475
+ if (!rsrChange || !rsrStage) {
476
+ 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 死锁');
477
+ process.exit(2);
478
+ }
479
+ const { registerStageReview } = await import('./stage-review.js');
480
+ const rsrPlatformOpts = {};
481
+ if (specDir) rsrPlatformOpts.specRoot = specDir;
482
+ try {
483
+ const result = registerStageReview({ changeName: rsrChange, stage: rsrStage, fromFile: rsrFrom, cwd: dir, platformOpts: rsrPlatformOpts });
484
+ if (json) {
485
+ process.stdout.write(JSON.stringify({ ok: true, command: 'register-stage-review', change: rsrChange, stage: rsrStage, ...result }));
486
+ } else {
487
+ console.log(`✅ 已注册 ${rsrStage} stage review [${result.reviewRunId}] → ${result.reviewPath}(mode: ${result.mode})`);
488
+ console.log(` marker → ${result.markerPath}`);
489
+ console.log(` 下一步:独立审查子代理对照 ${result.mainDoc} 填 verdict/checklist 后重跑 --done`);
490
+ }
491
+ } catch (e) {
492
+ if (json) process.stdout.write(JSON.stringify({ ok: false, command: 'register-stage-review', error: e.message }));
493
+ else console.error('❌ ' + e.message);
494
+ process.exitCode = 1;
495
+ }
496
+ break;
497
+ }
444
498
  case 'docs': {
445
499
  const docsSubCmd = filteredArgs[1];
446
500
  if (docsSubCmd === 'migrate') {
@@ -513,7 +567,7 @@ async function main() {
513
567
  const specBase = resolvePlatformSpecDir(doctorEffectiveDir, specDir) || join(doctorEffectiveDir, '.sillyspec');
514
568
  let r;
515
569
  try {
516
- r = await pm.alignExecuteToPlan(doctorEffectiveDir, alignChange, specBase, { confirm: doctorConfirm });
570
+ r = pm.alignExecuteToPlan(doctorEffectiveDir, alignChange, specBase, { confirm: doctorConfirm });
517
571
  } catch (e) {
518
572
  console.error(`❌ 对齐失败:${e.message}`);
519
573
  process.exitCode = 1;
@@ -595,6 +649,10 @@ async function main() {
595
649
  const { runCommand } = await import('./run.js')
596
650
  const stageArgs = [command, ...filteredArgs.slice(1)]
597
651
  const effectiveDir = specDir ? dir : resolveEffectiveDir(dir)
652
+ // 下行 pull:stage 命令(run/--done,含 archive)启动时拉一次(task-10 / D-009 / FR-04)
653
+ // 不在每步 pull,仅低频边界点;未连接平台静默跳过
654
+ const { triggerPullActiveChange } = await import('./run/shared.js')
655
+ await triggerPullActiveChange(effectiveDir)
598
656
  await runCommand(stageArgs, effectiveDir, specDir, { json })
599
657
  break
600
658
  }
@@ -634,9 +692,9 @@ async function main() {
634
692
  const pm = new ProgressManager({ specDir: resolvePlatformSpecDir(dir, specDir) });
635
693
 
636
694
  // isolation 写入 DB 的辅助函数
637
- async function _writeIsolationToDB(cwd, changeName, info) {
695
+ function _writeIsolationToDB(cwd, changeName, info) {
638
696
  if (info.blocked) {
639
- await pm.updateChangeIsolation(cwd, changeName, {
697
+ pm.updateChangeIsolation(cwd, changeName, {
640
698
  status: 'blocked',
641
699
  mode: null,
642
700
  reason: info.reason,
@@ -644,7 +702,7 @@ async function main() {
644
702
  } else {
645
703
  const mode = info.mode || 'worktree';
646
704
  const statusMap = { 'worktree': 'verified', 'native-worktree': 'verified', 'in-place-fallback': 'degraded' };
647
- await pm.updateChangeIsolation(cwd, changeName, {
705
+ pm.updateChangeIsolation(cwd, changeName, {
648
706
  status: statusMap[mode] || 'verified',
649
707
  mode,
650
708
  });
@@ -659,10 +717,11 @@ SillySpec worktree — git worktree 隔离管理
659
717
  sillyspec worktree create <change-name> [--base <branch>] 创建隔离 worktree
660
718
  sillyspec worktree apply <change-name> [--check-only] 校验并应用变更到主工作区
661
719
  sillyspec worktree assess <change-name> 风险审计 + 自动 apply
720
+ sillyspec worktree diff <change-name> [--base <commit>] 查看 worktree 相对 base 的变更
662
721
  sillyspec worktree list 列出所有活跃 worktree
663
722
  sillyspec worktree meta <change-name> 读取 worktree meta.json
664
723
  sillyspec worktree cleanup <change-name> [--force] 强制清理 worktree
665
- sillyspec worktree doctor [--fix] [--stale-hours N] 健康检查 + 修复
724
+ sillyspec worktree doctor [--fix] [--stale-hours N] [--change <name>] 健康检查 + 修复(--change 仅扫指定 change)
666
725
 
667
726
  选项:
668
727
  --base <branch> create: 指定基础分支(默认当前 HEAD)
@@ -691,11 +750,11 @@ SillySpec worktree — git worktree 隔离管理
691
750
  if (info.mode) {
692
751
  console.log(` 模式: ${info.mode}`);
693
752
  }
694
- // 写入 isolation 信息到 gate-status.json
753
+ // 写入 isolation 信息到 sillyspec.db(_writeIsolationToDB)
695
754
  await _writeIsolationToDB(dir, wtName, info);
696
755
  } catch (e) {
697
756
  console.error(`❌ ${e.message}`);
698
- // 写入 blocked 状态到 gate-status.json
757
+ // 写入 blocked 状态到 sillyspec.db(_writeIsolationToDB)
699
758
  await _writeIsolationToDB(dir, wtName, { blocked: true, reason: e.message });
700
759
  process.exit(1);
701
760
  }
@@ -716,6 +775,12 @@ SillySpec worktree — git worktree 隔离管理
716
775
  for (const err of result.errors) {
717
776
  console.error(` ${err}`);
718
777
  }
778
+ if (result.rescueCommands) {
779
+ console.error('');
780
+ console.error(`🆘 Rescue commands (${result.rescueCommands.cpFileCount} safe / ${result.rescueCommands.excludedCount} excluded,旁路 git apply,cp 后需手动 sillyspec worktree cleanup ${wtName}):`);
781
+ for (const c of result.rescueCommands.commands) console.error(` ${c}`);
782
+ for (const w of result.rescueCommands.warnings) console.error(` ⚠️ ${w}`);
783
+ }
719
784
  process.exit(1);
720
785
  }
721
786
 
@@ -736,10 +801,10 @@ SillySpec worktree — git worktree 隔离管理
736
801
  for (const f of result.changedFiles) {
737
802
  console.log(` ${f}`);
738
803
  }
739
- // filterDeliverableFiles 一刀切排除 .sillyspec/ + meta.json:
740
- // 模块文档/变更文档等不会 auto-apply 到主工作区。须告知 agent,
741
- // 否则它期望这些文件落地却找不到(memory: worktree-apply-excludes-module-docs)。
742
- console.log(` ℹ️ 注:.sillyspec/ 下的文件(模块文档/变更文档等)按规则不自动 apply,如需请手动从 worktree 分支或 dangling commit 取(git show sillyspec/${wtName}:<path>)。`);
804
+ // filterDeliverableFiles 精细化排除(保留 .sillyspec/docs/,排 changes/+.runtime/+quicklog/ + meta.json):
805
+ // 模块文档(.sillyspec/docs/)= 交付物,会 auto-apply 到主工作区;变更文档/运行时/quicklog 不 apply。
806
+ // 须告知 agent 哪些未落地,否则它期望这些文件落地却找不到(memory: worktree-apply-excludes-module-docs)。
807
+ console.log(` ℹ️ 注:.sillyspec/changes、.sillyspec/.runtime、.sillyspec/quicklog 下的文件按规则不自动 apply(模块文档 .sillyspec/docs/ 会自动 apply),如需请手动从 worktree 分支或 dangling commit 取(git show sillyspec/${wtName}:<path>)。`);
743
808
  }
744
809
  if (result.warnings && result.warnings.length > 0) {
745
810
  for (const w of result.warnings) {
@@ -777,6 +842,12 @@ SillySpec worktree — git worktree 隔离管理
777
842
  for (const w of assessment.warnings) console.log(` ⚠️ ${w}`);
778
843
  }
779
844
 
845
+ if (assessment.rescueCommands) {
846
+ console.log('');
847
+ console.log(`🆘 Rescue commands (${assessment.rescueCommands.cpFileCount} safe / ${assessment.rescueCommands.excludedCount} excluded,旁路 git apply,cp 后需手动 sillyspec worktree cleanup ${wtName}):`);
848
+ for (const c of assessment.rescueCommands.commands) console.log(` ${c}`);
849
+ for (const w of assessment.rescueCommands.warnings) console.log(` ⚠️ ${w}`);
850
+ }
780
851
  console.log('');
781
852
  if (assessment.decision === 'SAFE' || assessment.decision === 'WARNING') {
782
853
  console.log('Action: auto-applying...');
@@ -787,7 +858,7 @@ SillySpec worktree — git worktree 隔离管理
787
858
  } else {
788
859
  console.log(`✅ 已自动应用 ${applyResult.changedFiles.length} 个文件变更:`);
789
860
  for (const f of applyResult.changedFiles) console.log(` ${f}`);
790
- console.log(` ℹ️ 注:.sillyspec/ 下的文件按规则不自动 apply,如需请手动从分支取(git show sillyspec/${wtName}:<path>)。`);
861
+ console.log(` ℹ️ 注:.sillyspec/changes、.sillyspec/.runtime、.sillyspec/quicklog 下的文件按规则不自动 apply(模块文档 .sillyspec/docs/ 会自动 apply),如需请手动从分支取(git show sillyspec/${wtName}:<path>)。`);
791
862
  }
792
863
  } else {
793
864
  console.log('Action: blocked');
@@ -818,6 +889,42 @@ SillySpec worktree — git worktree 隔离管理
818
889
  console.log(JSON.stringify(meta, null, 2));
819
890
  break;
820
891
  }
892
+ case 'diff': {
893
+ if (!wtName) {
894
+ console.error('❌ 用法: sillyspec worktree diff <change-name> [--base <commit>]');
895
+ process.exit(1);
896
+ }
897
+ const meta = wm.getMeta(wtName);
898
+ if (!meta) {
899
+ console.error(`❌ 未找到 worktree meta: ${wtName}(可能未创建或已被清理)`);
900
+ process.exit(1);
901
+ }
902
+ if (!meta.worktreePath || !existsSync(meta.worktreePath)) {
903
+ console.error(`❌ worktree 目录不存在: ${meta.worktreePath || '(未设置)'}(可能已被清理,可跑 sillyspec worktree doctor)`);
904
+ process.exit(1);
905
+ }
906
+ // base 优先级:显式 --base > meta.baseHash(create 时锚的主仓 HEAD)> 当前 HEAD。
907
+ // diff 比较 worktree 工作区(含未提交)vs base,与 _changesAlreadyOnMain 同口径。
908
+ const baseIdx = filteredArgs.indexOf('--base');
909
+ const explicitBase = baseIdx >= 0 ? filteredArgs[baseIdx + 1] : null;
910
+ const base = explicitBase || meta.baseHash || 'HEAD';
911
+ let out = '';
912
+ try {
913
+ // base 作为独立 argv 元素传入,不经 shell 拆词/注入;worktreePath 经 cwd,不插值。
914
+ // trim:false 保留 git diff 原始输出(含尾换行)供 stdout.write;空检查靠临时 .trim()。
915
+ // timeout 30s:diff 属潜在长操作(对齐 design R3 与 verify-postcheck 口径)。
916
+ out = git(meta.worktreePath, ['--no-pager', 'diff', '--no-renames', base], { trim: false, timeout: 30000 });
917
+ } catch (e) {
918
+ console.error(`❌ git diff 失败(base=${String(base).slice(0, 8)}): ${e.message}`);
919
+ process.exit(1);
920
+ }
921
+ if (!out.trim()) {
922
+ console.log(`(无变更:worktree 工作区与 base ${String(base).slice(0, 8)} 一致)`);
923
+ } else {
924
+ process.stdout.write(out);
925
+ }
926
+ break;
927
+ }
821
928
  case 'list': {
822
929
  const items = wm.list();
823
930
  if (items.length === 0) {
@@ -868,7 +975,9 @@ SillySpec worktree — git worktree 隔离管理
868
975
  const fixFlag = args.includes('--fix');
869
976
  const staleIdx = args.indexOf('--stale-hours');
870
977
  const staleHours = staleIdx !== -1 && args[staleIdx + 1] ? parseInt(args[staleIdx + 1], 10) : 24;
871
- const diag = wm.doctor({ fix: fixFlag, staleHours });
978
+ const changeIdx = args.indexOf('--change');
979
+ const changeName = changeIdx !== -1 && args[changeIdx + 1] ? args[changeIdx + 1] : null;
980
+ const diag = wm.doctor({ fix: fixFlag, staleHours, changeName });
872
981
  if (diag.issues.length === 0) {
873
982
  console.log('✅ worktree 健康检查通过,无异常');
874
983
  } else {
@@ -899,6 +1008,83 @@ SillySpec worktree — git worktree 隔离管理
899
1008
  }
900
1009
  break;
901
1010
  }
1011
+ case 'dispatch': {
1012
+ // SillyHub 派发抽象层的 agent 调用桥(design.md §Phase2 / D-007@v1):
1013
+ // 仅做能力探测(probe)与派发策略生成(hint),**不执行任何 tool 调用**——
1014
+ // 实际 tool 调用(本机 Agent tool / SillyHub MCP tool)由 agent 据指令执行。
1015
+ // 仿 worktree 子命令的参数解析(filteredArgs[1] 取子命令)与错误处理模式。
1016
+ const dispatchSubCmd = filteredArgs[1];
1017
+ const dispatchSubs = ['probe', 'hint'];
1018
+
1019
+ if (!dispatchSubCmd || dispatchSubCmd === 'help' || dispatchSubCmd === '--help' || dispatchSubCmd === '-h') {
1020
+ console.log(`
1021
+ SillySpec dispatch — SillyHub 派发能力探测与策略生成(agent 调用桥,仅渲染与探测)
1022
+
1023
+ 用法:
1024
+ sillyspec dispatch probe [--json] 能力探测,输出 ProbeResult
1025
+ sillyspec dispatch hint --contract <json> [--json] 派发策略,输出 {instruction, backend}
1026
+
1027
+ 子命令:
1028
+ probe 探测 SillyHub MCP 是否可用(无配置返回 available=false,正常降级不报错)
1029
+ hint 据 contract + probe 结果生成派发指令文本(agent 拿指令后执行实际 tool 调用)
1030
+
1031
+ 选项:
1032
+ --contract <json> hint 必填:DispatchContract(brief/worktreePath/branch/allowedPaths/readOnly/...)
1033
+ --json 程序化输出(probe → ProbeResult;hint → {backend, instruction})
1034
+ `);
1035
+ break;
1036
+ }
1037
+
1038
+ if (dispatchSubCmd === 'probe') {
1039
+ const { probeSillyHub } = await import('./dispatch/probe.js');
1040
+ const result = await probeSillyHub();
1041
+ // result = { available, reason? };无配置时 {available:false,reason:'no-config'} 不报错(正常降级)
1042
+ if (json) {
1043
+ process.stdout.write(JSON.stringify(result));
1044
+ } else if (result.available) {
1045
+ console.log('✅ SillyHub 可用');
1046
+ } else {
1047
+ console.log(`⚠️ SillyHub 不可用:${result.reason || 'unknown'}`);
1048
+ }
1049
+ break;
1050
+ }
1051
+
1052
+ if (dispatchSubCmd === 'hint') {
1053
+ // 解析 --contract <json>(仿 worktree 用 args.indexOf 取 flag 后一个值)
1054
+ const contractIdx = args.indexOf('--contract');
1055
+ const contractRaw = contractIdx >= 0 && args[contractIdx + 1] ? args[contractIdx + 1] : null;
1056
+ if (!contractRaw) {
1057
+ console.error('用法: sillyspec dispatch hint --contract <json> [--json]\n 生成派发指令(DispatchContract:brief/worktreePath/branch/allowedPaths/readOnly/...)');
1058
+ process.exit(2);
1059
+ }
1060
+ let contract;
1061
+ try {
1062
+ contract = JSON.parse(contractRaw);
1063
+ } catch (e) {
1064
+ console.error(`❌ --contract JSON 解析失败: ${e.message}`);
1065
+ process.exit(2);
1066
+ }
1067
+ const { probeSillyHub } = await import('./dispatch/probe.js');
1068
+ const { renderDispatchInstruction } = await import('./dispatch/strategy.js');
1069
+ const probe = await probeSillyHub();
1070
+ const { instruction, backend } = renderDispatchInstruction(contract, probe);
1071
+ if (json) {
1072
+ process.stdout.write(JSON.stringify({ backend, instruction }));
1073
+ } else {
1074
+ console.log(`Backend: ${backend}`);
1075
+ console.log(instruction);
1076
+ }
1077
+ break;
1078
+ }
1079
+
1080
+ // 未知子命令 → did-you-mean 建议 + 退出 1(仿 worktree default)
1081
+ const sug = didYouMean(dispatchSubCmd, dispatchSubs);
1082
+ console.error(`❌ 未知子命令: dispatch ${dispatchSubCmd}`);
1083
+ if (sug) console.error(` 你是想输入「dispatch ${sug}」吗?`);
1084
+ console.error(` 可用子命令:${dispatchSubs.join(' | ')}(运行 sillyspec dispatch 查看用法)`);
1085
+ process.exit(1);
1086
+ break;
1087
+ }
902
1088
  case 'platform': {
903
1089
  const platformSub = filteredArgs[1];
904
1090
  const platformArgs = filteredArgs.slice(2);
@@ -912,6 +1098,8 @@ SillySpec platform — SillyHub 平台同步
912
1098
  sillyspec platform disconnect
913
1099
  sillyspec platform sync [--change <name>]
914
1100
  sillyspec platform sync-docs [--change <name>]
1101
+ sillyspec platform pull [--change <name>]
1102
+ sillyspec platform resolve <change-name> <--keep-local|--take-platform|--abort>
915
1103
  sillyspec platform status
916
1104
  sillyspec platform pointer [--cleanup]
917
1105
  sillyspec platform approve <change-name>
@@ -1012,15 +1200,108 @@ SillySpec platform — SillyHub 平台同步
1012
1200
  await syncModule.syncDocuments(syncDocsChangeName, dir);
1013
1201
  break;
1014
1202
  }
1015
- case 'status':
1016
- await syncModule.status(dir);
1203
+ case 'status': {
1204
+ // task-14 / FR-05:扩展 status(落后标记 + 未决冲突列表);未连接输出与现状一致
1205
+ const st = await syncModule.collectStatus(dir);
1206
+ if (!st.connected) {
1207
+ console.log('平台: 未连接');
1208
+ break;
1209
+ }
1210
+ console.log(`平台: ${st.url}`);
1211
+ console.log(`上次连接: ${st.lastSync || '未知'}`);
1212
+ if (st.listFailed) {
1213
+ console.log('ℹ️ 无法连接平台获取落后状态');
1214
+ } else if (st.behind.length > 0) {
1215
+ console.log(`⚠️ 本地可能落后(${st.behind.length} 个变更平台有更新):`);
1216
+ for (const b of st.behind) {
1217
+ console.log(` - ${b.name}: 平台 ${b.platform_pushed_at} > 本地 ${b.local_synced}`);
1218
+ }
1219
+ } else {
1220
+ console.log('✅ 本地与平台进度同步');
1221
+ }
1222
+ if (st.conflicts.length > 0) {
1223
+ console.log(`⚠️ 未决冲突(${st.conflicts.length} 个,请 platform resolve 处理):`);
1224
+ for (const c of st.conflicts) {
1225
+ console.log(` - ${c.change}${c.created_at ? ` (创建于 ${c.created_at})` : ''}`);
1226
+ }
1227
+ }
1228
+ break;
1229
+ }
1230
+ case 'pull': {
1231
+ // task-11 / D-006 / D-009 / FR-03:手动下行拉取平台进度到本地。
1232
+ // 与自动 triggerPull 共用 syncModule.pull(SyncManager.pull 实例方法),行为一致。
1233
+ const pullChangeIdx = args.indexOf('--change');
1234
+ const pullChangeName = pullChangeIdx >= 0 && args[pullChangeIdx + 1] ? args[pullChangeIdx + 1] : null;
1235
+ // 未连接明确提示不崩(acceptance:未连接平台输出明确提示)
1236
+ const smForCheck = new syncModule.SyncManager(dir);
1237
+ if (!smForCheck._getPlatform()) {
1238
+ console.error('❌ 未连接平台,请先 sillyspec platform connect');
1239
+ process.exit(1);
1240
+ }
1241
+ if (pullChangeName) {
1242
+ // 单变更完整 pull(两级 pull 第二级)
1243
+ const r = await syncModule.pull(pullChangeName, {}, dir);
1244
+ if (r.conflict) {
1245
+ console.log(`⚠️ ${pullChangeName}: 冲突(已写 sync-conflict 文件),请 sillyspec platform resolve 处理`);
1246
+ } else if (r.imported) {
1247
+ console.log(`✅ ${pullChangeName}: 已拉取并 import`);
1248
+ } else {
1249
+ console.log(`⏭️ ${pullChangeName}: 未 import(${r.reason || '无变更'})`);
1250
+ }
1251
+ } else {
1252
+ // 无 --change:先拉轻量列表(两级 pull 第一级),再对每个 change 按需完整 pull
1253
+ const list = await syncModule.pullList(dir);
1254
+ if (!list.ok) {
1255
+ console.error(`❌ 拉取变更列表失败: ${list.reason || '未知'}`);
1256
+ process.exit(1);
1257
+ }
1258
+ console.log(`平台变更 ${list.changes.length} 个:`);
1259
+ let importedCount = 0, conflictCount = 0;
1260
+ for (const ch of list.changes) {
1261
+ const name = typeof ch === 'string' ? ch : ch.name;
1262
+ if (!name) continue;
1263
+ const r = await syncModule.pull(name, {}, dir);
1264
+ if (r.imported) { importedCount++; console.log(` ✅ ${name}: 已 import`); }
1265
+ else if (r.conflict) { conflictCount++; console.log(` ⚠️ ${name}: 冲突,请 platform resolve`); }
1266
+ else { console.log(` ⏭️ ${name}: ${r.reason || '未 import'}`); }
1267
+ }
1268
+ console.log(`拉取完成:imported ${importedCount},冲突 ${conflictCount}`);
1269
+ }
1017
1270
  break;
1271
+ }
1272
+ case 'resolve': {
1273
+ // task-13 / D-002 / D-010 / D-013 / FR-05:冲突解决三选一(绝不字段级 auto-merge)
1274
+ const resolveName = platformArgs[0];
1275
+ if (!resolveName) {
1276
+ console.error('❌ 用法: sillyspec platform resolve <change-name> <--keep-local|--take-platform|--abort>');
1277
+ process.exit(1);
1278
+ }
1279
+ // 解析 mode flag(三选一互斥,多传/不传均报错)
1280
+ const resolveFlags = ['--keep-local', '--take-platform', '--abort'].filter((f) => args.includes(f));
1281
+ if (resolveFlags.length !== 1) {
1282
+ console.error('❌ 必须恰好指定 --keep-local / --take-platform / --abort 之一');
1283
+ process.exit(1);
1284
+ }
1285
+ const modeMap = { '--keep-local': 'keep-local', '--take-platform': 'take-platform', '--abort': 'abort' };
1286
+ const resolveMode = modeMap[resolveFlags[0]];
1287
+ const r = await syncModule.resolve(resolveName, resolveMode, dir);
1288
+ if (r.ok && r.resolved) {
1289
+ console.log(`✅ ${resolveName} [${resolveMode}]: ${r.reason}`);
1290
+ } else {
1291
+ console.error(`❌ ${resolveName}: ${r.reason}`);
1292
+ process.exit(1);
1293
+ }
1294
+ break;
1295
+ }
1018
1296
  case 'approve': {
1019
1297
  const approveName = platformArgs[0];
1020
1298
  if (!approveName) {
1021
1299
  console.error('❌ 用法: sillyspec platform approve <change-name>');
1022
1300
  process.exit(1);
1023
1301
  }
1302
+ // 审批前下行 pull(task-10 / D-009 / FR-06):拉最新进度避免基于过期状态审批;未连接跳过
1303
+ const { triggerPull } = await import('./run/shared.js');
1304
+ await triggerPull(dir, approveName);
1024
1305
  await syncModule.approve(approveName, dir);
1025
1306
  break;
1026
1307
  }
@@ -1058,7 +1339,7 @@ SillySpec platform — SillyHub 平台同步
1058
1339
  process.exit(2);
1059
1340
  }
1060
1341
  const pm = new ProgressManager({ specDir: resolvePlatformSpecDir(dir, specDir) });
1061
- await pm.renameChange(dir, oldName, newName);
1342
+ pm.renameChange(dir, oldName, newName);
1062
1343
  break;
1063
1344
  }
1064
1345
  case 'workflow': {
@@ -1203,7 +1484,7 @@ SillySpec workflow — 工作流管理
1203
1484
  SillySpec modules — 模块文档管理
1204
1485
 
1205
1486
  用法:
1206
- sillyspec modules rebuild 从模块卡片 + 源码重建 _module-map.yaml
1487
+ sillyspec modules rebuild [--force] 从模块卡片 + 源码重建 _module-map.yaml(默认 dry-run 预览,--force 才覆盖)
1207
1488
  sillyspec modules status 显示模块索引状态
1208
1489
  sillyspec modules migrate 旧格式模块文档迁移到新格式
1209
1490
  `);
@@ -1211,7 +1492,8 @@ SillySpec modules — 模块文档管理
1211
1492
  }
1212
1493
  if (modulesSub === 'rebuild') {
1213
1494
  const { rebuildModuleMap } = await import('./modules.js');
1214
- await rebuildModuleMap(dir);
1495
+ const isForce = filteredArgs.includes('--force');
1496
+ await rebuildModuleMap(dir, { force: isForce });
1215
1497
  } else if (modulesSub === 'status') {
1216
1498
  const { showModuleStatus } = await import('./modules.js');
1217
1499
  await showModuleStatus(dir);
@@ -1237,7 +1519,7 @@ SillySpec modules — 模块文档管理
1237
1519
  process.exit(2);
1238
1520
  }
1239
1521
 
1240
- const detected = detectLocalYaml(dir);
1522
+ const detected = (await import('./local-detect.js')).detectLocalYaml(dir);
1241
1523
  const specRoot = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
1242
1524
  const localYamlPath = join(specRoot, 'local.yaml');
1243
1525
 
@@ -1280,7 +1562,7 @@ SillySpec modules — 模块文档管理
1280
1562
  }
1281
1563
  case 'runtime': {
1282
1564
  // D2:.runtime/ 产物索引。CLI 往 .runtime/ 写各种证据/状态文件(db、doctor-diagnosis、
1283
- // workflow-runs、gate-status、user-inputs…),却没有命令让 agent 一眼看到「我手上有哪些产物」。
1565
+ // workflow-runs、user-inputs…),却没有命令让 agent 一眼看到「我手上有哪些产物」。
1284
1566
  // 多会话/压缩后尤其痛:上轮写的证据文件,下轮不知去哪找。纯只读枚举,不写盘。
1285
1567
  const runtimeSubCmd = filteredArgs[1];
1286
1568
  if (runtimeSubCmd && runtimeSubCmd !== 'list') {
@@ -1305,7 +1587,6 @@ SillySpec modules — 模块文档管理
1305
1587
  'doctor-diagnosis.json': 'doctor --json 结构化诊断快照',
1306
1588
  'doctor-dumps': 'doctor --dump-db 取证输出目录',
1307
1589
  'workflow-runs': 'workflow check --save 归档目录',
1308
- 'gate-status.json': '门控结果缓存',
1309
1590
  'scan-guard.json': 'scan 覆盖保护记录',
1310
1591
  'quick-sessions': 'quick 会话记录目录',
1311
1592
  'quick-guard.json': 'quick baseline 守卫(旧位置)',
@@ -1348,10 +1629,15 @@ SillySpec modules — 模块文档管理
1348
1629
  break;
1349
1630
  }
1350
1631
  default: {
1351
- const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'local', 'workflow', 'gate', 'derive', 'backfill-reviews', 'modules', 'change-rename', 'knowledge', 'platform', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime'];
1632
+ const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'dispatch', 'local', 'workflow', 'gate', 'derive', 'backfill-reviews', 'register-stage-review', 'modules', 'change-rename', 'knowledge', 'platform', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime'];
1352
1633
  const suggestion = didYouMean(command, topCommands);
1353
1634
  console.error(`❌ 未知命令: ${command}`);
1354
- if (suggestion) console.error(` 你是想输入「${suggestion}」吗?`);
1635
+ if (command === '--status') {
1636
+ // --status 是阶段内 flag(sillyspec run <stage> --status),裸用会落到 default
1637
+ console.error(' --status 是阶段内 flag。查看进度请用:sillyspec status 或 sillyspec progress show');
1638
+ } else if (suggestion) {
1639
+ console.error(` 你是想输入「${suggestion}」吗?`);
1640
+ }
1355
1641
  printUsage();
1356
1642
  process.exit(1);
1357
1643
  }
@@ -1364,6 +1650,10 @@ main().catch((err) => {
1364
1650
  console.error(`❌ ${err.message}`);
1365
1651
  process.exit(1);
1366
1652
  }
1367
- console.error(err);
1368
- process.exit(1);
1653
+ // message 提首行(Agent 可直接解析 stderr 首行判断成败),堆栈降级为附件;
1654
+ // 用 exitCode 而非 exit(1) 让事件循环自然 drain,缓解 Windows libuv teardown
1655
+ // 把退出码污染成 127(与 shell "command not found" 撞码)的问题。
1656
+ console.error(`❌ SillySpec 发生错误:${err?.message || err}`);
1657
+ if (err?.stack) console.error(err.stack);
1658
+ process.exitCode = 1;
1369
1659
  });