sillyspec 3.25.9 → 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 +176 -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,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/)
|
|
@@ -228,7 +229,7 @@ async function main() {
|
|
|
228
229
|
|
|
229
230
|
switch (command) {
|
|
230
231
|
case 'init':
|
|
231
|
-
await cmdInit(dir, { tool, interactive, specDir });
|
|
232
|
+
await (await import('./init.js')).cmdInit(dir, { tool, interactive, specDir });
|
|
232
233
|
break;
|
|
233
234
|
case 'setup':
|
|
234
235
|
const setupList = filteredArgs.includes('--list') || filteredArgs.includes('-l');
|
|
@@ -253,7 +254,7 @@ async function main() {
|
|
|
253
254
|
pm.show(progDir, progChangeName);
|
|
254
255
|
break;
|
|
255
256
|
case 'check': {
|
|
256
|
-
const result =
|
|
257
|
+
const result = pm.checkConsistency(progDir, progChangeName);
|
|
257
258
|
// checkConsistency 只返回 {ok, issues, warnings},不打印——必须在此输出,
|
|
258
259
|
// 否则 agent 跑只读健康检查得到一片寂静,无法分辨「正常」还是「已损坏」。
|
|
259
260
|
if (!result || result.ok) {
|
|
@@ -267,11 +268,11 @@ async function main() {
|
|
|
267
268
|
}
|
|
268
269
|
case 'repair': {
|
|
269
270
|
const repairApply = filteredArgs.includes('--apply');
|
|
270
|
-
|
|
271
|
+
pm.repairConsistency(progDir, { apply: repairApply, changeName: progChangeName });
|
|
271
272
|
break;
|
|
272
273
|
}
|
|
273
274
|
case 'validate':
|
|
274
|
-
|
|
275
|
+
pm.validate(dir, progChangeName);
|
|
275
276
|
break;
|
|
276
277
|
case 'reset':
|
|
277
278
|
pm.reset(dir, stage, progChangeName);
|
|
@@ -299,18 +300,18 @@ async function main() {
|
|
|
299
300
|
if (args[ai] === '--output' && args[ai + 1]) { updOutput = args[ai + 1]; ai++; }
|
|
300
301
|
}
|
|
301
302
|
const updForce = args.includes('--force');
|
|
302
|
-
|
|
303
|
+
pm.updateStep(dir, updStepStage, updStepName, { status: updStatus, output: updOutput, force: updForce }, progChangeName);
|
|
303
304
|
break;
|
|
304
305
|
}
|
|
305
306
|
case 'complete-stage': {
|
|
306
307
|
const compStageName = filteredArgs[2];
|
|
307
308
|
if (!compStageName) { console.log('❌ 用法: sillyspec progress complete-stage <stage> [--force]'); break; }
|
|
308
|
-
|
|
309
|
+
pm.completeStage(dir, compStageName, progChangeName, { force: args.includes('--force') });
|
|
309
310
|
break;
|
|
310
311
|
}
|
|
311
312
|
case 'batch': {
|
|
312
313
|
if (filteredArgs.includes('--status')) {
|
|
313
|
-
const bp =
|
|
314
|
+
const bp = pm.readBatchProgress(dir, progChangeName);
|
|
314
315
|
if (!bp) { console.log('📭 无批量进度数据'); break; }
|
|
315
316
|
const line = pm._renderBatchProgress(bp);
|
|
316
317
|
console.log(line || '📭 无批量进度数据');
|
|
@@ -329,7 +330,7 @@ async function main() {
|
|
|
329
330
|
console.log(' sillyspec progress batch --status');
|
|
330
331
|
break;
|
|
331
332
|
}
|
|
332
|
-
|
|
333
|
+
pm.updateBatchProgress(dir, batchData, progChangeName);
|
|
333
334
|
console.log('✅ 批量进度已更新');
|
|
334
335
|
}
|
|
335
336
|
break;
|
|
@@ -356,6 +357,15 @@ async function main() {
|
|
|
356
357
|
// resolveSpecDir(cwd),向后兼容。
|
|
357
358
|
const gateOpts = { cwd: dir };
|
|
358
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
|
+
}
|
|
359
369
|
const { envelope, exitCode } = await withJsonOutput(json, () => runGate(gateStage, gateChange, gateOpts));
|
|
360
370
|
if (json) {
|
|
361
371
|
process.stdout.write(JSON.stringify(envelope));
|
|
@@ -390,6 +400,13 @@ async function main() {
|
|
|
390
400
|
// 下需透传;不传 --spec-dir 时走默认 resolveSpecDir(cwd),向后兼容。
|
|
391
401
|
const deriveOpts = { cwd: dir };
|
|
392
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
|
+
}
|
|
393
410
|
const { envelope, exitCode } = await withJsonOutput(json, () => runDerive(facet, deriveChange, deriveOpts));
|
|
394
411
|
if (json) {
|
|
395
412
|
process.stdout.write(JSON.stringify(envelope));
|
|
@@ -513,7 +530,7 @@ async function main() {
|
|
|
513
530
|
const specBase = resolvePlatformSpecDir(doctorEffectiveDir, specDir) || join(doctorEffectiveDir, '.sillyspec');
|
|
514
531
|
let r;
|
|
515
532
|
try {
|
|
516
|
-
r =
|
|
533
|
+
r = pm.alignExecuteToPlan(doctorEffectiveDir, alignChange, specBase, { confirm: doctorConfirm });
|
|
517
534
|
} catch (e) {
|
|
518
535
|
console.error(`❌ 对齐失败:${e.message}`);
|
|
519
536
|
process.exitCode = 1;
|
|
@@ -634,9 +651,9 @@ async function main() {
|
|
|
634
651
|
const pm = new ProgressManager({ specDir: resolvePlatformSpecDir(dir, specDir) });
|
|
635
652
|
|
|
636
653
|
// isolation 写入 DB 的辅助函数
|
|
637
|
-
|
|
654
|
+
function _writeIsolationToDB(cwd, changeName, info) {
|
|
638
655
|
if (info.blocked) {
|
|
639
|
-
|
|
656
|
+
pm.updateChangeIsolation(cwd, changeName, {
|
|
640
657
|
status: 'blocked',
|
|
641
658
|
mode: null,
|
|
642
659
|
reason: info.reason,
|
|
@@ -644,7 +661,7 @@ async function main() {
|
|
|
644
661
|
} else {
|
|
645
662
|
const mode = info.mode || 'worktree';
|
|
646
663
|
const statusMap = { 'worktree': 'verified', 'native-worktree': 'verified', 'in-place-fallback': 'degraded' };
|
|
647
|
-
|
|
664
|
+
pm.updateChangeIsolation(cwd, changeName, {
|
|
648
665
|
status: statusMap[mode] || 'verified',
|
|
649
666
|
mode,
|
|
650
667
|
});
|
|
@@ -659,10 +676,11 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
659
676
|
sillyspec worktree create <change-name> [--base <branch>] 创建隔离 worktree
|
|
660
677
|
sillyspec worktree apply <change-name> [--check-only] 校验并应用变更到主工作区
|
|
661
678
|
sillyspec worktree assess <change-name> 风险审计 + 自动 apply
|
|
679
|
+
sillyspec worktree diff <change-name> [--base <commit>] 查看 worktree 相对 base 的变更
|
|
662
680
|
sillyspec worktree list 列出所有活跃 worktree
|
|
663
681
|
sillyspec worktree meta <change-name> 读取 worktree meta.json
|
|
664
682
|
sillyspec worktree cleanup <change-name> [--force] 强制清理 worktree
|
|
665
|
-
sillyspec worktree doctor [--fix] [--stale-hours N]
|
|
683
|
+
sillyspec worktree doctor [--fix] [--stale-hours N] [--change <name>] 健康检查 + 修复(--change 仅扫指定 change)
|
|
666
684
|
|
|
667
685
|
选项:
|
|
668
686
|
--base <branch> create: 指定基础分支(默认当前 HEAD)
|
|
@@ -691,11 +709,11 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
691
709
|
if (info.mode) {
|
|
692
710
|
console.log(` 模式: ${info.mode}`);
|
|
693
711
|
}
|
|
694
|
-
// 写入 isolation 信息到
|
|
712
|
+
// 写入 isolation 信息到 sillyspec.db(_writeIsolationToDB)
|
|
695
713
|
await _writeIsolationToDB(dir, wtName, info);
|
|
696
714
|
} catch (e) {
|
|
697
715
|
console.error(`❌ ${e.message}`);
|
|
698
|
-
// 写入 blocked 状态到
|
|
716
|
+
// 写入 blocked 状态到 sillyspec.db(_writeIsolationToDB)
|
|
699
717
|
await _writeIsolationToDB(dir, wtName, { blocked: true, reason: e.message });
|
|
700
718
|
process.exit(1);
|
|
701
719
|
}
|
|
@@ -736,10 +754,10 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
736
754
|
for (const f of result.changedFiles) {
|
|
737
755
|
console.log(` ${f}`);
|
|
738
756
|
}
|
|
739
|
-
// filterDeliverableFiles
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
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>)。`);
|
|
743
761
|
}
|
|
744
762
|
if (result.warnings && result.warnings.length > 0) {
|
|
745
763
|
for (const w of result.warnings) {
|
|
@@ -787,7 +805,7 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
787
805
|
} else {
|
|
788
806
|
console.log(`✅ 已自动应用 ${applyResult.changedFiles.length} 个文件变更:`);
|
|
789
807
|
for (const f of applyResult.changedFiles) console.log(` ${f}`);
|
|
790
|
-
console.log(` ℹ️ 注:.sillyspec/ 下的文件按规则不自动 apply
|
|
808
|
+
console.log(` ℹ️ 注:.sillyspec/changes、.sillyspec/.runtime、.sillyspec/quicklog 下的文件按规则不自动 apply(模块文档 .sillyspec/docs/ 会自动 apply),如需请手动从分支取(git show sillyspec/${wtName}:<path>)。`);
|
|
791
809
|
}
|
|
792
810
|
} else {
|
|
793
811
|
console.log('Action: blocked');
|
|
@@ -818,6 +836,42 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
818
836
|
console.log(JSON.stringify(meta, null, 2));
|
|
819
837
|
break;
|
|
820
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
|
+
}
|
|
821
875
|
case 'list': {
|
|
822
876
|
const items = wm.list();
|
|
823
877
|
if (items.length === 0) {
|
|
@@ -868,7 +922,9 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
868
922
|
const fixFlag = args.includes('--fix');
|
|
869
923
|
const staleIdx = args.indexOf('--stale-hours');
|
|
870
924
|
const staleHours = staleIdx !== -1 && args[staleIdx + 1] ? parseInt(args[staleIdx + 1], 10) : 24;
|
|
871
|
-
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 });
|
|
872
928
|
if (diag.issues.length === 0) {
|
|
873
929
|
console.log('✅ worktree 健康检查通过,无异常');
|
|
874
930
|
} else {
|
|
@@ -899,6 +955,83 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
899
955
|
}
|
|
900
956
|
break;
|
|
901
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
|
+
}
|
|
902
1035
|
case 'platform': {
|
|
903
1036
|
const platformSub = filteredArgs[1];
|
|
904
1037
|
const platformArgs = filteredArgs.slice(2);
|
|
@@ -1058,7 +1191,7 @@ SillySpec platform — SillyHub 平台同步
|
|
|
1058
1191
|
process.exit(2);
|
|
1059
1192
|
}
|
|
1060
1193
|
const pm = new ProgressManager({ specDir: resolvePlatformSpecDir(dir, specDir) });
|
|
1061
|
-
|
|
1194
|
+
pm.renameChange(dir, oldName, newName);
|
|
1062
1195
|
break;
|
|
1063
1196
|
}
|
|
1064
1197
|
case 'workflow': {
|
|
@@ -1203,7 +1336,7 @@ SillySpec workflow — 工作流管理
|
|
|
1203
1336
|
SillySpec modules — 模块文档管理
|
|
1204
1337
|
|
|
1205
1338
|
用法:
|
|
1206
|
-
sillyspec modules rebuild
|
|
1339
|
+
sillyspec modules rebuild [--force] 从模块卡片 + 源码重建 _module-map.yaml(默认 dry-run 预览,--force 才覆盖)
|
|
1207
1340
|
sillyspec modules status 显示模块索引状态
|
|
1208
1341
|
sillyspec modules migrate 旧格式模块文档迁移到新格式
|
|
1209
1342
|
`);
|
|
@@ -1211,7 +1344,8 @@ SillySpec modules — 模块文档管理
|
|
|
1211
1344
|
}
|
|
1212
1345
|
if (modulesSub === 'rebuild') {
|
|
1213
1346
|
const { rebuildModuleMap } = await import('./modules.js');
|
|
1214
|
-
|
|
1347
|
+
const isForce = filteredArgs.includes('--force');
|
|
1348
|
+
await rebuildModuleMap(dir, { force: isForce });
|
|
1215
1349
|
} else if (modulesSub === 'status') {
|
|
1216
1350
|
const { showModuleStatus } = await import('./modules.js');
|
|
1217
1351
|
await showModuleStatus(dir);
|
|
@@ -1237,7 +1371,7 @@ SillySpec modules — 模块文档管理
|
|
|
1237
1371
|
process.exit(2);
|
|
1238
1372
|
}
|
|
1239
1373
|
|
|
1240
|
-
const detected = detectLocalYaml(dir);
|
|
1374
|
+
const detected = (await import('./local-detect.js')).detectLocalYaml(dir);
|
|
1241
1375
|
const specRoot = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
|
|
1242
1376
|
const localYamlPath = join(specRoot, 'local.yaml');
|
|
1243
1377
|
|
|
@@ -1280,7 +1414,7 @@ SillySpec modules — 模块文档管理
|
|
|
1280
1414
|
}
|
|
1281
1415
|
case 'runtime': {
|
|
1282
1416
|
// D2:.runtime/ 产物索引。CLI 往 .runtime/ 写各种证据/状态文件(db、doctor-diagnosis、
|
|
1283
|
-
// workflow-runs、
|
|
1417
|
+
// workflow-runs、user-inputs…),却没有命令让 agent 一眼看到「我手上有哪些产物」。
|
|
1284
1418
|
// 多会话/压缩后尤其痛:上轮写的证据文件,下轮不知去哪找。纯只读枚举,不写盘。
|
|
1285
1419
|
const runtimeSubCmd = filteredArgs[1];
|
|
1286
1420
|
if (runtimeSubCmd && runtimeSubCmd !== 'list') {
|
|
@@ -1305,7 +1439,6 @@ SillySpec modules — 模块文档管理
|
|
|
1305
1439
|
'doctor-diagnosis.json': 'doctor --json 结构化诊断快照',
|
|
1306
1440
|
'doctor-dumps': 'doctor --dump-db 取证输出目录',
|
|
1307
1441
|
'workflow-runs': 'workflow check --save 归档目录',
|
|
1308
|
-
'gate-status.json': '门控结果缓存',
|
|
1309
1442
|
'scan-guard.json': 'scan 覆盖保护记录',
|
|
1310
1443
|
'quick-sessions': 'quick 会话记录目录',
|
|
1311
1444
|
'quick-guard.json': 'quick baseline 守卫(旧位置)',
|
|
@@ -1348,10 +1481,15 @@ SillySpec modules — 模块文档管理
|
|
|
1348
1481
|
break;
|
|
1349
1482
|
}
|
|
1350
1483
|
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'];
|
|
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'];
|
|
1352
1485
|
const suggestion = didYouMean(command, topCommands);
|
|
1353
1486
|
console.error(`❌ 未知命令: ${command}`);
|
|
1354
|
-
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
|
+
}
|
|
1355
1493
|
printUsage();
|
|
1356
1494
|
process.exit(1);
|
|
1357
1495
|
}
|
|
@@ -1364,6 +1502,10 @@ main().catch((err) => {
|
|
|
1364
1502
|
console.error(`❌ ${err.message}`);
|
|
1365
1503
|
process.exit(1);
|
|
1366
1504
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
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;
|
|
1369
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
|
|
package/src/machine-interface.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 设计原则(见 change 2026-07-09-machine-interface-v1 design.md §2/§3):
|
|
8
8
|
* - 方案 B:独立模块单点封装,不污染既有 run/progress 输出
|
|
9
9
|
* - 只聚合不新增校验:复用 stage-contract / task-review / verify-postcheck 既有策略引擎
|
|
10
|
-
* - 只读语义边界(D-002@v1):只调 ProgressManager 读路径,不写 sillyspec.db
|
|
10
|
+
* - 只读语义边界(D-002@v1):只调 ProgressManager 读路径,不写 sillyspec.db
|
|
11
11
|
* - stdout 纯 JSON(D-005@v1):--json 模式下由 CLI 层(src/index.js 的 withJsonOutput)
|
|
12
12
|
* 在调用 runGate/runDerive 期间劫持 console.log 到 stderr,防被调模块污染机器输出
|
|
13
13
|
* - 退出码 0/1/2 三段语义(D-004@v1):0=通过,1=事实阻断,2=无法核验
|
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
import { existsSync, readFileSync } from 'fs';
|
|
19
19
|
import { join } from 'path';
|
|
20
20
|
import { ProgressManager, resolveSpecDir } from './progress.js';
|
|
21
|
+
import { resolveRuntimeRoot } from './run/shared.js';
|
|
21
22
|
import { runValidators, checkTransition, checkExecuteCodeEvidence } from './stage-contract.js';
|
|
22
|
-
import { validateTaskReviews } from './task-review.js';
|
|
23
|
+
import { validateTaskReviews, resolveLatestExecuteRunId } from './task-review.js';
|
|
23
24
|
import { runVerifyTestCheck } from './verify-postcheck.js';
|
|
24
25
|
|
|
25
26
|
// ============ 退出码常量(D-004@v1)============
|
|
@@ -101,16 +102,35 @@ export function buildEnvelope({
|
|
|
101
102
|
* @param {object} opts
|
|
102
103
|
* @param {string} opts.cwd - 项目根目录
|
|
103
104
|
* @param {string} [opts.specBase] - .sillyspec(或平台 specRoot)目录;默认 resolveSpecDir(cwd)
|
|
104
|
-
* @param {string} [opts.runtimeRoot] - .runtime 目录;默认
|
|
105
|
+
* @param {string} [opts.runtimeRoot] - .runtime 目录;默认 resolveRuntimeRoot 三级优先级解析
|
|
106
|
+
* @param {string} [opts.specDriftAnchor] - drift 场景主仓 specBase 锚点(坑 execute-runs-isolation)。
|
|
107
|
+
* drift 守卫命中时由调用方传入,task-reviews 的 execute-run-id marker 落点经
|
|
108
|
+
* resolveRuntimeRoot 锚主仓 .runtime,不读 worktree 副本(与 contract-matrix 调用方同语义)。
|
|
105
109
|
* @returns {Promise<{ envelope: object, exitCode: number }>}
|
|
106
110
|
*/
|
|
107
|
-
export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot } = {}) {
|
|
111
|
+
export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, specDriftAnchor } = {}) {
|
|
108
112
|
const specRoot = specBase || resolveSpecDir(cwd);
|
|
109
113
|
const pm = new ProgressManager();
|
|
110
114
|
|
|
111
115
|
try {
|
|
116
|
+
// ── 只读契约守卫(D-002@v1):db 不存在时 fail-closed 返回 exit 2,不触发建库落盘 ──
|
|
117
|
+
// pm.read → _ensureDB → db.init() 在 db 不存在/schema 戳落后时会 _createSchema + _save 落盘
|
|
118
|
+
// (db.js:init),这会让一个声明只读的查询命令在 spec 目录凭空建 sillyspec.db,违反只读契约。
|
|
119
|
+
// db 还没建 = 该变更从未跑过流程,无从核验 → 显式返回「无法核验」而非建库。
|
|
120
|
+
const dbPath = join(resolveSpecDir(cwd), '.runtime', 'sillyspec.db');
|
|
121
|
+
if (!existsSync(dbPath)) {
|
|
122
|
+
const envelope = buildEnvelope({
|
|
123
|
+
command: 'gate',
|
|
124
|
+
stage,
|
|
125
|
+
change: changeName,
|
|
126
|
+
ok: false,
|
|
127
|
+
errors: [`无法核验:进度库不存在(${dbPath})——变更 ${changeName} 尚未运行过任何阶段,只读 gate 不会为其建库`],
|
|
128
|
+
});
|
|
129
|
+
return { envelope, exitCode: EXIT_UNKNOWN };
|
|
130
|
+
}
|
|
131
|
+
|
|
112
132
|
// ── 读进度:变更不存在 → exit 2(D-004@v1)──
|
|
113
|
-
const progress =
|
|
133
|
+
const progress = pm.read(cwd, changeName);
|
|
114
134
|
if (!progress) {
|
|
115
135
|
const envelope = buildEnvelope({
|
|
116
136
|
command: 'gate',
|
|
@@ -181,13 +201,14 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot }
|
|
|
181
201
|
}
|
|
182
202
|
}
|
|
183
203
|
|
|
184
|
-
|
|
185
|
-
|
|
204
|
+
// drift 场景经 resolveRuntimeRoot 三级优先级解析(runtimeRoot > specDriftAnchor > 本地兜底),
|
|
205
|
+
// 锚主仓 .runtime,不读 worktree 副本(坑 execute-runs-isolation,与 contract-matrix 调用方同语义)
|
|
206
|
+
const rtRoot = resolveRuntimeRoot({ runtimeRoot, specDriftAnchor }, specRoot);
|
|
207
|
+
// marker 缺失/为空时先扫描 execute-runs/ 找回真实 runId(与 gates.js #6 同源),
|
|
208
|
+
// 避免拿 '' 进 validateTaskReviews 拼出 execute-runs//tasks/... 产出全量误导 errors。
|
|
186
209
|
let executeRunId = '';
|
|
187
210
|
try {
|
|
188
|
-
|
|
189
|
-
executeRunId = readFileSync(runIdFile, 'utf8').trim();
|
|
190
|
-
}
|
|
211
|
+
executeRunId = resolveLatestExecuteRunId({ runtimeRoot: rtRoot, changeName }) || '';
|
|
191
212
|
} catch {
|
|
192
213
|
executeRunId = '';
|
|
193
214
|
}
|
|
@@ -300,7 +321,7 @@ export const FACETS = ['execute-evidence', 'verify-test', 'task-reviews', 'artif
|
|
|
300
321
|
* 与 gate 的差异:gate 聚合某阶段所有 check 的综合结论;derive 只返回单一 facet 的结构化 data,
|
|
301
322
|
* daemon 用来做细粒度事实采集(如轮询 execute-evidence 判断代码是否变更)。
|
|
302
323
|
*
|
|
303
|
-
* 只读语义边界同 gate(D-002@v1):仅调 ProgressManager.read,不写 sillyspec.db
|
|
324
|
+
* 只读语义边界同 gate(D-002@v1):仅调 ProgressManager.read,不写 sillyspec.db。
|
|
304
325
|
* 唯一例外是 verify-test 会真实执行测试并落盘 test-result.json(产物取证,非状态写入,design §3.3)。
|
|
305
326
|
*
|
|
306
327
|
* 退出码语义(D-004@v1):0=事实通过,1=事实性阻断,2=无法核验(用法错/变更不存在/内部异常)。
|
|
@@ -310,10 +331,12 @@ export const FACETS = ['execute-evidence', 'verify-test', 'task-reviews', 'artif
|
|
|
310
331
|
* @param {object} opts
|
|
311
332
|
* @param {string} opts.cwd - 项目根目录
|
|
312
333
|
* @param {string} [opts.specBase] - .sillyspec(或平台 specRoot)目录;默认 resolveSpecDir(cwd)
|
|
313
|
-
* @param {string} [opts.runtimeRoot] - .runtime 目录;默认
|
|
334
|
+
* @param {string} [opts.runtimeRoot] - .runtime 目录;默认 resolveRuntimeRoot 三级优先级解析
|
|
335
|
+
* @param {string} [opts.specDriftAnchor] - drift 场景主仓 specBase 锚点(坑 execute-runs-isolation),
|
|
336
|
+
* task-reviews facet 的 execute-run-id marker 落点经 resolveRuntimeRoot 锚主仓 .runtime,不读副本。
|
|
314
337
|
* @returns {Promise<{ envelope: object, exitCode: number }>}
|
|
315
338
|
*/
|
|
316
|
-
export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot } = {}) {
|
|
339
|
+
export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot, specDriftAnchor } = {}) {
|
|
317
340
|
// ── 非法 facet:用法错 → exit 2(D-004@v1)──
|
|
318
341
|
if (!FACETS.includes(facet)) {
|
|
319
342
|
const envelope = buildEnvelope({
|
|
@@ -330,8 +353,21 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot
|
|
|
330
353
|
const pm = new ProgressManager();
|
|
331
354
|
|
|
332
355
|
try {
|
|
356
|
+
// ── 只读契约守卫(D-002@v1,同 runGate):db 不存在时 fail-closed 返回 exit 2,不建库落盘 ──
|
|
357
|
+
const dbPath = join(resolveSpecDir(cwd), '.runtime', 'sillyspec.db');
|
|
358
|
+
if (!existsSync(dbPath)) {
|
|
359
|
+
const envelope = buildEnvelope({
|
|
360
|
+
command: 'derive',
|
|
361
|
+
facet,
|
|
362
|
+
change: changeName,
|
|
363
|
+
ok: false,
|
|
364
|
+
errors: [`无法核验:进度库不存在(${dbPath})——变更 ${changeName} 尚未运行过任何阶段,只读 derive 不会为其建库`],
|
|
365
|
+
});
|
|
366
|
+
return { envelope, exitCode: EXIT_UNKNOWN };
|
|
367
|
+
}
|
|
368
|
+
|
|
333
369
|
// ── 读进度:变更不存在 → exit 2 ──
|
|
334
|
-
const progress =
|
|
370
|
+
const progress = pm.read(cwd, changeName);
|
|
335
371
|
if (!progress) {
|
|
336
372
|
const envelope = buildEnvelope({
|
|
337
373
|
command: 'derive',
|
|
@@ -399,13 +435,12 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot
|
|
|
399
435
|
}
|
|
400
436
|
}
|
|
401
437
|
|
|
402
|
-
|
|
403
|
-
const
|
|
438
|
+
// drift 场景经 resolveRuntimeRoot 三级优先级解析(同 runGate execute 段),锚主仓 .runtime 不读副本
|
|
439
|
+
const rtRoot = resolveRuntimeRoot({ runtimeRoot, specDriftAnchor }, specRoot);
|
|
440
|
+
// marker 缺失/为空时先扫描 execute-runs/ 找回真实 runId(同 runGate execute 段),避免拿 '' 误导校验
|
|
404
441
|
let executeRunId = '';
|
|
405
442
|
try {
|
|
406
|
-
|
|
407
|
-
executeRunId = readFileSync(runIdFile, 'utf8').trim();
|
|
408
|
-
}
|
|
443
|
+
executeRunId = resolveLatestExecuteRunId({ runtimeRoot: rtRoot, changeName }) || '';
|
|
409
444
|
} catch {
|
|
410
445
|
executeRunId = '';
|
|
411
446
|
}
|
package/src/migrate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, renameSync, copyFileSync } from 'fs';
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, renameSync, copyFileSync, statSync, cpSync } from 'fs';
|
|
2
2
|
import { basename, join, resolve } from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
|
|
@@ -60,9 +60,19 @@ export function migrateDocs(projectDir) {
|
|
|
60
60
|
const src = join(archiveDir, entry);
|
|
61
61
|
const dest = join(targetDir, entry);
|
|
62
62
|
if (!existsSync(dest)) {
|
|
63
|
-
copyFileSync
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
// 归档变更本身是目录(含 design.md 等),copyFileSync 对目录源抛 EISDIR;
|
|
64
|
+
// 按类型分发:目录用 cpSync 递归、文件用 copyFileSync,单条失败 try/catch 不中断整体迁移
|
|
65
|
+
try {
|
|
66
|
+
if (statSync(src).isDirectory()) {
|
|
67
|
+
cpSync(src, dest, { recursive: true });
|
|
68
|
+
} else {
|
|
69
|
+
copyFileSync(src, dest);
|
|
70
|
+
}
|
|
71
|
+
console.log(chalk.green(' ✅') + ` archive/${entry}`);
|
|
72
|
+
migrated++;
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.log(chalk.yellow(' ⚠️') + ` archive/${entry} 迁移失败: ${err.message}`);
|
|
75
|
+
}
|
|
66
76
|
} else {
|
|
67
77
|
console.log(chalk.yellow(' ⏭️') + ` archive/${entry} (已存在)`);
|
|
68
78
|
}
|