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.
- package/.claude/skills/sillyspec-auto/SKILL.md +8 -13
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +8 -6
- package/.claude/skills/sillyspec-continue/SKILL.md +3 -3
- package/.claude/skills/sillyspec-doctor/SKILL.md +1 -1
- package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
- package/.claude/skills/sillyspec-plan/SKILL.md +1 -1
- package/.claude/skills/sillyspec-verify/SKILL.md +1 -1
- package/README.md +9 -0
- package/package.json +2 -2
- package/src/change-list.js +11 -8
- package/src/change-risk-profile.js +22 -0
- package/src/db.js +160 -159
- package/src/dispatch/backends/local-agent.js +152 -0
- package/src/dispatch/backends/sillyhub-mcp.js +242 -0
- package/src/dispatch/probe.js +208 -0
- package/src/dispatch/strategy.js +121 -0
- package/src/doctor-diagnostics.js +19 -25
- package/src/fs-atomic.js +69 -69
- package/src/git-helper.js +66 -0
- package/src/hooks/worktree-guard.js +64 -55
- package/src/index.js +215 -34
- package/src/init.js +7 -14
- package/src/machine-interface.js +54 -19
- package/src/migrate.js +14 -4
- package/src/modules.js +16 -5
- package/src/progress/change-registry.js +72 -70
- package/src/progress/consistency-doctor.js +71 -14
- package/src/progress/shared.js +6 -3
- package/src/progress/stage-machine.js +61 -65
- package/src/progress/step-store.js +62 -67
- package/src/progress.js +155 -233
- package/src/quicklog.js +13 -5
- package/src/run/command.js +65 -37
- package/src/run/complete-handlers.js +1005 -889
- package/src/run/complete.js +115 -208
- package/src/run/concurrent-detect.js +132 -0
- package/src/run/gates.js +289 -17
- package/src/run/prompt.js +22 -68
- package/src/run/quick-audit.js +10 -9
- package/src/run/shared.js +648 -600
- package/src/run/stage.js +43 -24
- package/src/scan-postcheck.js +9 -31
- package/src/sillyhub-mcp/client.js +389 -0
- package/src/spec-dir-typo.js +71 -0
- package/src/stage-contract-engine.js +22 -1
- package/src/stage-contract-spec.js +11 -4
- package/src/stage-contract.js +26 -3
- package/src/stage-review.js +13 -9
- package/src/stages/archive.js +4 -6
- package/src/stages/brainstorm-auto.js +26 -20
- package/src/stages/brainstorm.js +29 -5
- package/src/stages/cmd-existence.js +145 -0
- package/src/stages/doctor.js +9 -6
- package/src/stages/execute.js +61 -20
- package/src/stages/plan-postcheck.js +152 -0
- package/src/stages/plan.js +16 -31
- package/src/stages/quick.js +1 -1
- package/src/stages/scan.js +9 -3
- package/src/stages/status.js +0 -1
- package/src/stages/verify.js +12 -7
- package/src/sync.js +4 -4
- package/src/task-review.js +30 -1
- package/src/verify-postcheck.js +933 -862
- package/src/version.js +19 -0
- package/src/worktree-apply.js +85 -68
- package/src/worktree-deps.js +89 -5
- package/src/worktree.js +213 -111
- package/templates/prompts/taskcard-rules.md +27 -0
- package/templates/prompts/verify-probes.md +3 -3
- package/.claude/skills/sillyspec-propose/SKILL.md +0 -87
- 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 {
|
|
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 =
|
|
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
|
-
|
|
271
|
+
pm.repairConsistency(progDir, { apply: repairApply, changeName: progChangeName });
|
|
270
272
|
break;
|
|
271
273
|
}
|
|
272
274
|
case 'validate':
|
|
273
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
654
|
+
function _writeIsolationToDB(cwd, changeName, info) {
|
|
599
655
|
if (info.blocked) {
|
|
600
|
-
|
|
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
|
-
|
|
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 信息到
|
|
712
|
+
// 写入 isolation 信息到 sillyspec.db(_writeIsolationToDB)
|
|
656
713
|
await _writeIsolationToDB(dir, wtName, info);
|
|
657
714
|
} catch (e) {
|
|
658
715
|
console.error(`❌ ${e.message}`);
|
|
659
|
-
// 写入 blocked 状态到
|
|
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
|
|
701
|
-
//
|
|
702
|
-
//
|
|
703
|
-
console.log(` ℹ️ 注:.sillyspec/
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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、
|
|
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 (
|
|
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
|
-
|
|
1329
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|