sillyspec 3.26.11 → 3.26.14
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-archive/SKILL.md +1 -1
- package/.claude/skills/sillyspec-execute/SKILL.md +1 -1
- package/.claude/skills/sillyspec-knowledge/SKILL.md +1 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +5 -5
- package/.claude/skills/sillyspec-quick/SKILL.md +5 -1
- package/README.md +7 -5
- package/SKILL.md +5 -4
- package/package.json +1 -1
- package/packages/dashboard/server/executor.js +10 -1
- package/packages/dashboard/server/index.js +108 -46
- package/src/config-schema.js +3 -0
- package/src/contract-matrix.js +22 -13
- package/src/db.js +24 -4
- package/src/dispatch/probe.js +99 -63
- package/src/docs-check.js +58 -13
- package/src/docs-debt.js +6 -2
- package/src/docs-gate.js +1 -0
- package/src/doctor-diagnostics.js +208 -21
- package/src/fs-atomic.js +3 -1
- package/src/git-helper.js +7 -4
- package/src/hooks/worktree-guard.js +8 -2
- package/src/index.js +200 -27
- package/src/init.js +3 -2
- package/src/local-detect.js +9 -1
- package/src/modules.js +7 -5
- package/src/progress/stage-machine.js +589 -578
- package/src/progress/step-store.js +1 -1
- package/src/progress.js +187 -30
- package/src/quicklog.js +47 -16
- package/src/run/command.js +88 -46
- package/src/run/complete-handlers.js +27 -15
- package/src/run/complete.js +1186 -1121
- package/src/run/concurrent-detect.js +3 -1
- package/src/run/gates.js +172 -25
- package/src/run/prompt.js +17 -9
- package/src/run/scan-profile.js +35 -23
- package/src/run/shared.js +164 -63
- package/src/run/stage.js +41 -1
- package/src/setup.js +8 -10
- package/src/sillyhub-mcp/client.js +74 -6
- package/src/spec-dir-typo.js +3 -24
- package/src/spec-sync.js +43 -8
- package/src/stage-contract.js +12 -38
- package/src/stage-review.js +40 -1
- package/src/stages/archive.js +4 -3
- package/src/stages/brainstorm-auto.js +1 -1
- package/src/stages/brainstorm.js +27 -13
- package/src/stages/doctor.js +29 -16
- package/src/stages/execute.js +170 -154
- package/src/stages/knowledge.js +18 -3
- package/src/stages/plan-postcheck.js +72 -37
- package/src/stages/plan.js +70 -48
- package/src/stages/quick.js +3 -1
- package/src/stages/verify.js +1 -1
- package/src/sync.js +339 -225
- package/src/task-review.js +281 -64
- package/src/taskcard.js +178 -0
- package/src/verify-postcheck.js +48 -35
- package/src/worktree-apply.js +58 -34
- package/src/worktree-deps.js +120 -20
- package/src/worktree.js +55 -9
- package/.npmrc-publish +0 -2
package/src/index.js
CHANGED
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs';
|
|
10
10
|
import { writeAtomicSync } from './fs-atomic.js';
|
|
11
11
|
import { basename, join, resolve } from 'path';
|
|
12
|
-
import {
|
|
13
|
-
import { git } from './git-helper.js';
|
|
12
|
+
import { safeGit, git } from './git-helper.js';
|
|
14
13
|
import { getVersion } from './version.js';
|
|
15
14
|
|
|
16
15
|
// E22(性能#2):progress.js(拖 db.js→node:sqlite 链 ~48ms)与 run/shared.js(拖 stages 全家
|
|
@@ -78,6 +77,7 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
78
77
|
list | meta <change> 列出 / 读取 meta.json
|
|
79
78
|
cleanup <change> [--force] 清理 worktree
|
|
80
79
|
doctor [--fix] [--stale-hours N] 健康检查 + 修复
|
|
80
|
+
--cleanup-ghosts [--confirm] 归档幽灵 active 记录(db 有行无目录 / 空壳目录 0 文件超 7 天,SS-2/2b)
|
|
81
81
|
|
|
82
82
|
sillyspec local detect [--dir <path>] 生成本地配置 local.yaml(纯 fs 嗅探,零 token、不跑 scan)
|
|
83
83
|
sillyspec config [schema] [--json] 打印 local.yaml 全部已知键 + 生效状态 + 读取点(堵外部 agent 配置发现缺口)
|
|
@@ -90,7 +90,7 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
90
90
|
sillyspec gate <stage> --change <name> [--json] 机器门控:阶段能否标记完成(只读)
|
|
91
91
|
sillyspec derive <facet> --change <name> [--json] 单项事实核验(facet: execute-evidence|verify-test|task-reviews|artifacts)
|
|
92
92
|
sillyspec backfill-reviews --change <name> [--json] 为手动补的 task 生成 review.json 草稿(cannot_verify,解 archive 客观完成度阻断)
|
|
93
|
-
sillyspec register-stage-review --change <name> --stage <brainstorm|plan|execute> [--from <review.json>] [--json]
|
|
93
|
+
sillyspec register-stage-review --change <name> (--stage <brainstorm|plan|execute> | --all) [--from <review.json>] [--refresh-hash] [--json]
|
|
94
94
|
生成/adopt stage 级 review.json(docHash 自动算 + 写 marker,治 tier=independent marker 死锁)
|
|
95
95
|
|
|
96
96
|
sillyspec doctor [子命令] 进度库健康检查 + 修复(顶层命令,非 worktree doctor)
|
|
@@ -100,6 +100,9 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
100
100
|
--dump-db --path <db 路径> dump 指定 db 内容到文件(取证用)
|
|
101
101
|
--json 结构化诊断 + 落盘 .sillyspec/.runtime/doctor-diagnosis.json
|
|
102
102
|
sillyspec modules <rebuild | status | migrate>
|
|
103
|
+
sillyspec taskcard <change> --task task-01[,task-02...] | --all [--force]
|
|
104
|
+
生成 Windows 安全 TaskCard 骨架(LF 行尾 + frontmatter 闭合 +
|
|
105
|
+
硬校验 9 字段齐全,标题自动取自 plan.md,Edit 填占位符即可)
|
|
103
106
|
sillyspec change-rename <旧变更名> <新变更名>
|
|
104
107
|
sillyspec knowledge <search --query "..." --limit N
|
|
105
108
|
| inspect --id "..."
|
|
@@ -242,12 +245,9 @@ async function main() {
|
|
|
242
245
|
// 导致 sillyspec 命令找不到 .sillyspec。此函数尝试从 git root 解析。
|
|
243
246
|
function resolveEffectiveDir(baseDir) {
|
|
244
247
|
if (existsSync(join(baseDir, '.sillyspec'))) return baseDir
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}).trim()
|
|
249
|
-
if (gitRoot && existsSync(join(gitRoot, '.sillyspec'))) return gitRoot
|
|
250
|
-
} catch {}
|
|
248
|
+
// QUAL-01 收口:原 execSync 字符串拼接(经 shell)→ git-helper 数组形式
|
|
249
|
+
const r = safeGit(baseDir, ['rev-parse', '--show-toplevel'])
|
|
250
|
+
if (r.value && existsSync(join(r.value, '.sillyspec'))) return r.value
|
|
251
251
|
return baseDir
|
|
252
252
|
}
|
|
253
253
|
|
|
@@ -373,8 +373,34 @@ async function main() {
|
|
|
373
373
|
}
|
|
374
374
|
break;
|
|
375
375
|
}
|
|
376
|
+
case 'dump': {
|
|
377
|
+
// 只读进度导出(design §6.2):daemon 消费的机器接口 JSON。
|
|
378
|
+
// --json 模式下劫持 console.log 到 stderr,stdout 留给纯 JSON(与 gate/derive 同纪律)。
|
|
379
|
+
const dumpData = pm.dump(progDir);
|
|
380
|
+
if (json) {
|
|
381
|
+
const { buildEnvelope } = await import('./machine-interface.js');
|
|
382
|
+
const envelope = buildEnvelope({
|
|
383
|
+
command: 'progress dump',
|
|
384
|
+
ok: dumpData !== null,
|
|
385
|
+
data: dumpData,
|
|
386
|
+
errors: dumpData === null ? ['无活跃变更或进度数据不存在'] : [],
|
|
387
|
+
});
|
|
388
|
+
console.log(JSON.stringify(envelope));
|
|
389
|
+
} else {
|
|
390
|
+
if (!dumpData) {
|
|
391
|
+
console.log('📭 无活跃变更或进度数据不存在');
|
|
392
|
+
} else {
|
|
393
|
+
console.log(`项目: ${dumpData.project}`);
|
|
394
|
+
console.log(`当前变更: ${dumpData.current_change || '(无)'}`);
|
|
395
|
+
console.log(`当前阶段: ${dumpData.current_stage || '(无)'}`);
|
|
396
|
+
console.log(`最后活跃: ${dumpData.last_active || '(无)'}`);
|
|
397
|
+
console.log(`产物数: ${dumpData.artifacts?.length || 0}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
376
402
|
default:
|
|
377
|
-
console.log('用法: sillyspec progress <init|show|validate|reset|set-stage|add-step|update-step|complete-stage>');
|
|
403
|
+
console.log('用法: sillyspec progress <init|show|validate|reset|set-stage|add-step|update-step|complete-stage|dump>');
|
|
378
404
|
process.exit(2);
|
|
379
405
|
}
|
|
380
406
|
break;
|
|
@@ -548,8 +574,14 @@ async function main() {
|
|
|
548
574
|
const rsrStage = rsrStageIdx >= 0 && args[rsrStageIdx + 1] ? args[rsrStageIdx + 1] : null;
|
|
549
575
|
const rsrFromIdx = args.indexOf('--from');
|
|
550
576
|
const rsrFrom = rsrFromIdx >= 0 && args[rsrFromIdx + 1] ? args[rsrFromIdx + 1] : null;
|
|
551
|
-
|
|
552
|
-
|
|
577
|
+
// docHash 联动(坑 stage-review-dochash-manual-resync,2026-08-21 实证):改一版 design 要
|
|
578
|
+
// 重算 2-3 个 stage review 的 docHash,手工易漏、gate 报错才补。--refresh-hash 就地刷新单个
|
|
579
|
+
// 既有 review 的 hash(保留结论);--all 一次处理三个 stage(有 review 刷 hash,无 review
|
|
580
|
+
// 生成骨架——改版后一条命令完成全部联动)。
|
|
581
|
+
const rsrRefreshHash = filteredArgs.includes('--refresh-hash');
|
|
582
|
+
const rsrAll = filteredArgs.includes('--all');
|
|
583
|
+
if (!rsrChange || (!rsrStage && !rsrAll) || (rsrAll && rsrStage)) {
|
|
584
|
+
console.error('用法: sillyspec register-stage-review --change <名> (--stage <brainstorm|plan|execute> | --all) [--from <review.json>] [--refresh-hash] [--spec-dir <path>] [--json]\n 生成/adopt/刷新 stage 级 review.json(docHash 自动算 + 写 marker);--all 一次处理三个 stage(有 review 刷新 hash、无 review 生成骨架);--refresh-hash 就地刷新既有 review 的 docHash(保留 verdict 待确认续用)');
|
|
553
585
|
process.exit(2);
|
|
554
586
|
}
|
|
555
587
|
// 与 run 入口同源消毒(防路径穿越;register 下游写 marker、拼 changes/review 路径)
|
|
@@ -557,20 +589,38 @@ async function main() {
|
|
|
557
589
|
const { registerStageReview } = await import('./stage-review.js');
|
|
558
590
|
const rsrPlatformOpts = {};
|
|
559
591
|
if (specDir) rsrPlatformOpts.specRoot = specDir;
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
592
|
+
const rsrStages = rsrAll ? ['brainstorm', 'plan', 'execute'] : [rsrStage];
|
|
593
|
+
const rsrResults = [];
|
|
594
|
+
let rsrFailed = 0;
|
|
595
|
+
for (const st of rsrStages) {
|
|
596
|
+
try {
|
|
597
|
+
// --all 的单 stage 语义:有既有 review → refresh-hash;无 → 骨架。显式 --refresh-hash
|
|
598
|
+
// 则强制 refresh(无既有 review 时报错,提示先生成骨架)
|
|
599
|
+
const { getLatestStageReviewRunId, stageReviewMarkerPath } = await import('./stage-review.js');
|
|
600
|
+
const { resolveRuntimeRoot } = await import('./run/shared.js');
|
|
601
|
+
const rsrRuntimeRoot = resolveRuntimeRoot(rsrPlatformOpts, rsrPlatformOpts.specRoot || join(dir, '.sillyspec'));
|
|
602
|
+
const hasExisting = !!getLatestStageReviewRunId(rsrRuntimeRoot, st, rsrChange);
|
|
603
|
+
const wantRefresh = rsrRefreshHash || (rsrAll && hasExisting);
|
|
604
|
+
const result = registerStageReview({ changeName: rsrChange, stage: st, fromFile: rsrFrom, cwd: dir, platformOpts: rsrPlatformOpts, refreshHash: wantRefresh });
|
|
605
|
+
rsrResults.push({ stage: st, ...result });
|
|
606
|
+
if (json) {
|
|
607
|
+
process.stdout.write(JSON.stringify({ ok: true, command: 'register-stage-review', change: rsrChange, stage: st, ...result }) + '\n');
|
|
608
|
+
} else {
|
|
609
|
+
console.log(`✅ 已注册 ${st} stage review [${result.reviewRunId}] → ${result.reviewPath}(mode: ${result.mode})`);
|
|
610
|
+
console.log(` marker → ${result.markerPath}`);
|
|
611
|
+
if (result.mode === 'refreshed') {
|
|
612
|
+
console.log(` docHash 已按当前 ${result.mainDoc} 重算(verdict 结论是否仍适用需人工确认)`);
|
|
613
|
+
} else {
|
|
614
|
+
console.log(` 下一步:独立审查子代理对照 ${result.mainDoc} 填 verdict/checklist 后重跑 --done`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
} catch (e) {
|
|
618
|
+
rsrFailed++;
|
|
619
|
+
if (json) process.stdout.write(JSON.stringify({ ok: false, command: 'register-stage-review', change: rsrChange, stage: st, error: e.message }) + '\n');
|
|
620
|
+
else console.error(`❌ [${st}] ` + e.message);
|
|
568
621
|
}
|
|
569
|
-
} catch (e) {
|
|
570
|
-
if (json) process.stdout.write(JSON.stringify({ ok: false, command: 'register-stage-review', error: e.message }));
|
|
571
|
-
else console.error('❌ ' + e.message);
|
|
572
|
-
process.exitCode = 1;
|
|
573
622
|
}
|
|
623
|
+
if (rsrFailed > 0) process.exitCode = 1;
|
|
574
624
|
break;
|
|
575
625
|
}
|
|
576
626
|
case 'docs': {
|
|
@@ -627,6 +677,7 @@ async function main() {
|
|
|
627
677
|
paths: cliPaths || cfg.paths,
|
|
628
678
|
skip: cfg.skip,
|
|
629
679
|
keywordAssert: cfg.keywordAssert,
|
|
680
|
+
crossRepoRoots: cfg.crossRepoRoots,
|
|
630
681
|
});
|
|
631
682
|
// task-03 修复链路(FR-04,platform-map-auto-anchors):fixActive 才构造 fixes——
|
|
632
683
|
// 仅 fix.fixable===true 且 newLine 为整数的条目可重锚;newRef = ref 的行号部分替换为
|
|
@@ -654,6 +705,9 @@ async function main() {
|
|
|
654
705
|
console.log(JSON.stringify(result, null, 2));
|
|
655
706
|
} else {
|
|
656
707
|
for (const w of result.warnings) console.warn(`⚠️ ${w}`);
|
|
708
|
+
if (result.crossRepoSkipped > 0) {
|
|
709
|
+
console.log(`ℹ️ 跨仓 repo:// 引用跳过 ${result.crossRepoSkipped} 处(未配 docs-check.cross_repo_roots 映射;配置后走同款行号+关键词校验)`);
|
|
710
|
+
}
|
|
657
711
|
if (result.ok) {
|
|
658
712
|
console.log(`✅ docs check: ${result.total} 处引用全通过(其中 ${result.kwChecked} 处带关键词断言)`);
|
|
659
713
|
} else {
|
|
@@ -793,8 +847,9 @@ async function main() {
|
|
|
793
847
|
// slice(1) 去掉的是 'run' 字面量,这里 command 本身就是 stage 名不能丢。
|
|
794
848
|
case 'doctor': {
|
|
795
849
|
const doctorEffectiveDir = specDir ? dir : resolveEffectiveDir(dir);
|
|
796
|
-
// 执行流:--cleanup-remnant / --dump-db(结构化诊断之外的修复/取证动作)
|
|
850
|
+
// 执行流:--cleanup-remnant / --cleanup-ghosts / --dump-db(结构化诊断之外的修复/取证动作)
|
|
797
851
|
const cleanupRemnant = filteredArgs.includes('--cleanup-remnant');
|
|
852
|
+
const cleanupGhosts = filteredArgs.includes('--cleanup-ghosts');
|
|
798
853
|
const dumpDbFlag = filteredArgs.includes('--dump-db');
|
|
799
854
|
const doctorConfirm = filteredArgs.includes('--confirm');
|
|
800
855
|
const pathIdx = filteredArgs.indexOf('--path');
|
|
@@ -825,6 +880,20 @@ async function main() {
|
|
|
825
880
|
// 复用顶层静态 import(line 12),不动态 await import
|
|
826
881
|
const pm = new ProgressManager({ specDir: resolvePlatformSpecDir(doctorEffectiveDir, specDir) });
|
|
827
882
|
const specBase = resolvePlatformSpecDir(doctorEffectiveDir, specDir) || join(doctorEffectiveDir, '.sillyspec');
|
|
883
|
+
// 坑 doctor-align-bypass-review-gate(2026-08-20 实证):align 直写 completed 绕过
|
|
884
|
+
// completeStep,此前 execute 的 Stage/Task Review Gate 被整体跳过——worktree 清理后的
|
|
885
|
+
// 恢复场景恰是最需要审计的时刻。confirm 落盘前跑同源只读校验(dry-run 不拦,保持只读语义)。
|
|
886
|
+
if (doctorConfirm) {
|
|
887
|
+
const { enforceAlignExecuteReviewGate } = await import('./run/gates.js');
|
|
888
|
+
const gateBlocked = await enforceAlignExecuteReviewGate({
|
|
889
|
+
cwd: doctorEffectiveDir, changeName: alignChange, specBase,
|
|
890
|
+
});
|
|
891
|
+
if (gateBlocked) {
|
|
892
|
+
console.error(' 对齐未执行(review 校验未过)。补齐 review 后重跑本命令。');
|
|
893
|
+
process.exitCode = 1;
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
828
897
|
let r;
|
|
829
898
|
try {
|
|
830
899
|
r = pm.alignExecuteToPlan(doctorEffectiveDir, alignChange, specBase, { confirm: doctorConfirm });
|
|
@@ -866,6 +935,26 @@ async function main() {
|
|
|
866
935
|
process.exitCode = r.errors.length > 0 ? 1 : 0;
|
|
867
936
|
break;
|
|
868
937
|
}
|
|
938
|
+
if (cleanupGhosts) {
|
|
939
|
+
// SS-2(2026-08-20):归档幽灵行(db active 无目录)。默认 dry-run,--confirm 才写。
|
|
940
|
+
// SS-2b:扩空壳目录(active + 目录 0 文件 + last_active 超 7 天),归档行并移除空目录。
|
|
941
|
+
const { cleanupGhostChanges } = await import('./doctor-diagnostics.js');
|
|
942
|
+
const r = await cleanupGhostChanges({ cwd: doctorEffectiveDir, specDir, confirm: doctorConfirm });
|
|
943
|
+
if (json) {
|
|
944
|
+
console.log(JSON.stringify(r, null, 2));
|
|
945
|
+
} else {
|
|
946
|
+
console.log(`👻 幽灵 active 记录(db active 无目录 / 空壳目录超 7 天)${doctorConfirm ? '已归档' : '待清理(dry-run)'}:${r.count} 个 [db: ${r.db_path || '(未找到)'}]`);
|
|
947
|
+
for (const n of r.ghosts) console.log(` ${doctorConfirm ? '✅' : '-'} ${n}`);
|
|
948
|
+
for (const n of (r.empty_shells || [])) console.log(` ${doctorConfirm ? '✅' : '🫙'} ${n}(空壳目录${doctorConfirm ? ',空目录已移除' : ''})`);
|
|
949
|
+
for (const n of (r.skipped_nonempty || [])) console.log(` ⏭️ ${n}(复查时目录已非空,已放过——可能是活跃变更)`);
|
|
950
|
+
for (const e of r.errors) console.log(` ❌ ${e.name || ''}: ${e.error}`);
|
|
951
|
+
if (!doctorConfirm && r.count > 0) console.log(`
|
|
952
|
+
加 --confirm 执行归档(仅改 status=archived,可逆;空壳同时移除 0 文件空目录,不删任何有内容的目录)。`);
|
|
953
|
+
if (r.reason) console.log(`ℹ️ ${r.reason}`);
|
|
954
|
+
}
|
|
955
|
+
process.exitCode = r.errors.length > 0 ? 1 : 0;
|
|
956
|
+
break;
|
|
957
|
+
}
|
|
869
958
|
if (dumpDbFlag) {
|
|
870
959
|
if (!dbPath) { console.error('❌ --dump-db 需要 --path <db 路径>'); process.exitCode = 2; break; }
|
|
871
960
|
const { dumpDb } = await import('./doctor-diagnostics.js');
|
|
@@ -950,6 +1039,10 @@ async function main() {
|
|
|
950
1039
|
for (let i = 1; i < args.length; i++) {
|
|
951
1040
|
if (args[i] === '--port' && args[i + 1]) {
|
|
952
1041
|
port = parseInt(args[i + 1], 10);
|
|
1042
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
1043
|
+
console.error(`❌ 无效端口: ${args[i + 1]}(应为 1-65535 整数)`);
|
|
1044
|
+
process.exit(2);
|
|
1045
|
+
}
|
|
953
1046
|
i++;
|
|
954
1047
|
} else if (args[i] === '--no-open') {
|
|
955
1048
|
openBrowser = false;
|
|
@@ -1298,7 +1391,7 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
1298
1391
|
const staleHours = staleIdx !== -1 && args[staleIdx + 1] ? parseInt(args[staleIdx + 1], 10) : 24;
|
|
1299
1392
|
const changeIdx = args.indexOf('--change');
|
|
1300
1393
|
const changeName = changeIdx !== -1 && args[changeIdx + 1] ? args[changeIdx + 1] : null;
|
|
1301
|
-
const diag = wm.doctor({ fix: fixFlag, staleHours, changeName });
|
|
1394
|
+
const diag = await wm.doctor({ fix: fixFlag, staleHours, changeName });
|
|
1302
1395
|
if (diag.issues.length === 0) {
|
|
1303
1396
|
console.log('✅ worktree 健康检查通过,无异常');
|
|
1304
1397
|
} else {
|
|
@@ -1437,6 +1530,15 @@ SillySpec platform — SillyHub 平台同步
|
|
|
1437
1530
|
process.exit(1);
|
|
1438
1531
|
}
|
|
1439
1532
|
|
|
1533
|
+
// 体检 SEC-05:platform 子命令族统一校验 --change 值——此前只有 run 命令族走
|
|
1534
|
+
// assertSafeChangeName,platform sync/pull/resolve 直接透传,冲突文件名
|
|
1535
|
+
// join(runtimeDir, `sync-conflict-${changeName}.json`) 可被 ../ 穿越出 .runtime
|
|
1536
|
+
{
|
|
1537
|
+
const pChangeIdx = args.indexOf('--change');
|
|
1538
|
+
const pChangeVal = pChangeIdx >= 0 && args[pChangeIdx + 1] ? args[pChangeIdx + 1] : null;
|
|
1539
|
+
if (pChangeVal) assertSafeChangeName(pChangeVal, 'platform --change 变更名');
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1440
1542
|
switch (platformSub) {
|
|
1441
1543
|
case 'pointer': {
|
|
1442
1544
|
// 指针状态检查(不依赖 sync 模块)
|
|
@@ -1503,7 +1605,11 @@ SillySpec platform — SillyHub 平台同步
|
|
|
1503
1605
|
const tokenIdx = args.indexOf('--token');
|
|
1504
1606
|
const token = tokenIdx >= 0 && args[tokenIdx + 1] ? args[tokenIdx + 1] : undefined;
|
|
1505
1607
|
if (!token) {
|
|
1506
|
-
|
|
1608
|
+
// 缺 token 直接终止(体检 HUB-02):交互式输入尚未实现(task-11),此前
|
|
1609
|
+
// 继续执行会把 undefined 序列化成字符串 "undefined" 写入 local.yaml,
|
|
1610
|
+
// 报"连接成功"后所有同步持续 401(Bearer undefined)
|
|
1611
|
+
console.error('❌ 缺少 --token <token>(交互式输入暂未实现);请在 SillyHub 平台获取 token 后重试');
|
|
1612
|
+
process.exit(1);
|
|
1507
1613
|
}
|
|
1508
1614
|
await syncModule.connect(url, token, dir);
|
|
1509
1615
|
break;
|
|
@@ -1626,6 +1732,8 @@ SillySpec platform — SillyHub 平台同步
|
|
|
1626
1732
|
console.error('❌ 必须恰好指定 --keep-local / --take-platform / --abort 之一');
|
|
1627
1733
|
process.exit(1);
|
|
1628
1734
|
}
|
|
1735
|
+
// 位置参数变更名与 --change 同规格校验(SEC-05:resolve 会按该名重推/删冲突文件)
|
|
1736
|
+
assertSafeChangeName(resolveName, 'platform resolve 变更名');
|
|
1629
1737
|
const modeMap = { '--keep-local': 'keep-local', '--take-platform': 'take-platform', '--abort': 'abort' };
|
|
1630
1738
|
const resolveMode = modeMap[resolveFlags[0]];
|
|
1631
1739
|
const r = await syncModule.resolve(resolveName, resolveMode, dir);
|
|
@@ -1673,6 +1781,71 @@ SillySpec platform — SillyHub 平台同步
|
|
|
1673
1781
|
}
|
|
1674
1782
|
break;
|
|
1675
1783
|
}
|
|
1784
|
+
case 'taskcard': {
|
|
1785
|
+
const tcName = filteredArgs.slice(1).find(a => !a.startsWith('-'));
|
|
1786
|
+
if (!tcName || tcName === 'help' || tcName === '--help' || tcName === '-h') {
|
|
1787
|
+
console.log(`
|
|
1788
|
+
SillySpec taskcard — 生成 Windows 安全的 TaskCard 骨架
|
|
1789
|
+
|
|
1790
|
+
用法:
|
|
1791
|
+
sillyspec taskcard <change-name> --task task-01[,task-02...] [--title <t>] [--title-zh <t>] [--force]
|
|
1792
|
+
sillyspec taskcard <change-name> --all [--force]
|
|
1793
|
+
|
|
1794
|
+
骨架由 CLI 直写(LF 行尾 + frontmatter 闭合 + 硬校验 9 字段齐全),标题自动取自 plan.md
|
|
1795
|
+
checkbox 行;之后用 Edit 填充占位符即可,勿手写整卡(CRLF/漏闭合 ---/漏字段的源头治理)。
|
|
1796
|
+
`);
|
|
1797
|
+
break;
|
|
1798
|
+
}
|
|
1799
|
+
try { assertSafeChangeName(tcName, '变更名'); }
|
|
1800
|
+
catch (e) { console.error(`❌ ${e.message}`); process.exit(2); }
|
|
1801
|
+
|
|
1802
|
+
const force = filteredArgs.includes('--force');
|
|
1803
|
+
const all = filteredArgs.includes('--all');
|
|
1804
|
+
const getVal = (flag) => {
|
|
1805
|
+
const idx = filteredArgs.indexOf(flag);
|
|
1806
|
+
return idx !== -1 && filteredArgs[idx + 1] && !filteredArgs[idx + 1].startsWith('-')
|
|
1807
|
+
? filteredArgs[idx + 1] : null;
|
|
1808
|
+
};
|
|
1809
|
+
const taskVal = getVal('--task');
|
|
1810
|
+
const titleVal = getVal('--title');
|
|
1811
|
+
const titleZhVal = getVal('--title-zh');
|
|
1812
|
+
|
|
1813
|
+
if (all && taskVal) {
|
|
1814
|
+
console.error('❌ --all 与 --task 互斥(--all 取 plan.md 全部任务,--task 显式指定)');
|
|
1815
|
+
process.exit(1);
|
|
1816
|
+
}
|
|
1817
|
+
if (!all && !taskVal) {
|
|
1818
|
+
console.error('❌ 用法: sillyspec taskcard <change-name> --task task-01[,task-02...] | --all [--force]');
|
|
1819
|
+
process.exit(1);
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
let taskIds;
|
|
1823
|
+
if (all) {
|
|
1824
|
+
taskIds = 'all';
|
|
1825
|
+
} else {
|
|
1826
|
+
const { normalizeTaskId } = await import('./taskcard.js');
|
|
1827
|
+
try {
|
|
1828
|
+
taskIds = taskVal.split(',').map(s => s.trim()).filter(Boolean).map(normalizeTaskId);
|
|
1829
|
+
} catch (e) {
|
|
1830
|
+
console.error(`❌ ${e.message}`);
|
|
1831
|
+
process.exit(1);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
const { cmdTaskcard } = await import('./taskcard.js');
|
|
1836
|
+
try {
|
|
1837
|
+
const result = cmdTaskcard(tcName, { cwd: dir, specDir, taskIds, title: titleVal, titleZh: titleZhVal, force });
|
|
1838
|
+
for (const f of result.created) console.log(`✅ 已生成: ${f}`);
|
|
1839
|
+
for (const f of result.skipped) console.log(`⏭️ 已存在,跳过(--force 覆盖): ${f}`);
|
|
1840
|
+
if (result.created.length > 0) {
|
|
1841
|
+
console.log(` 下一步:用 Edit tool 填充占位符(allowed_paths/goal/implementation/acceptance/verify/constraints),勿用 Write 整文件重写。`);
|
|
1842
|
+
}
|
|
1843
|
+
} catch (e) {
|
|
1844
|
+
console.error(`❌ ${e.message}`);
|
|
1845
|
+
process.exit(1);
|
|
1846
|
+
}
|
|
1847
|
+
break;
|
|
1848
|
+
}
|
|
1676
1849
|
case 'change-rename': {
|
|
1677
1850
|
const oldName = filteredArgs[1];
|
|
1678
1851
|
const newName = filteredArgs[2];
|
package/src/init.js
CHANGED
|
@@ -5,6 +5,7 @@ import { checkbox, confirm, input } from '@inquirer/prompts';
|
|
|
5
5
|
import { ProgressManager } from './progress.js';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import { getVersion } from './version.js';
|
|
8
|
+
import { gitQuiet } from './git-helper.js';
|
|
8
9
|
import { renderExample } from './config-schema.js';
|
|
9
10
|
// 向后兼容:getVersion 已抽到轻量 version.js(避免 index.js 为 --version 静态加载 init.js 的 inquirer 税),
|
|
10
11
|
// 此处 re-export 保持 init.js 既有导出 API 不破坏(如 test/init-claude-injection.test.mjs)。
|
|
@@ -557,9 +558,9 @@ export async function cmdInit(projectDir, options = {}) {
|
|
|
557
558
|
|
|
558
559
|
let repo = '';
|
|
559
560
|
try {
|
|
560
|
-
|
|
561
|
+
// QUAL-01 收口:裸 execFileSync → git-helper(safe.directory + 数组形式)
|
|
561
562
|
const absPath = resolve(projectDir, subPath.trim() || `./${name.trim()}`);
|
|
562
|
-
repo =
|
|
563
|
+
repo = gitQuiet(absPath, ['remote', 'get-url', 'origin']) || '';
|
|
563
564
|
} catch {}
|
|
564
565
|
|
|
565
566
|
subprojects.push({ name: name.trim(), path: subPath.trim() || `./${name.trim()}`, role: role.trim(), repo });
|
package/src/local-detect.js
CHANGED
|
@@ -90,7 +90,15 @@ export function detectLocalYaml(workdir) {
|
|
|
90
90
|
|
|
91
91
|
// 3. gradle
|
|
92
92
|
if (existsSync(join(workdir, 'build.gradle'))) {
|
|
93
|
-
|
|
93
|
+
// Windows 上 cmd.exe 执行不了 ./gradlew(shell 脚本)——优先 gradlew.bat;
|
|
94
|
+
// 仅有 POSIX gradlew 且在 win32 时退回全局 gradle(体检 BUG-08)
|
|
95
|
+
const isWin = process.platform === 'win32'
|
|
96
|
+
let gradlePrefix = 'gradle'
|
|
97
|
+
if (isWin) {
|
|
98
|
+
gradlePrefix = existsSync(join(workdir, 'gradlew.bat')) ? 'gradlew.bat' : 'gradle'
|
|
99
|
+
} else {
|
|
100
|
+
gradlePrefix = existsSync(join(workdir, 'gradlew')) ? './gradlew' : 'gradle'
|
|
101
|
+
}
|
|
94
102
|
return {
|
|
95
103
|
project: { type: 'gradle' },
|
|
96
104
|
commands: {
|
package/src/modules.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { gitQuiet } from './git-helper.js';
|
|
4
|
+
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* 查找项目下的 _module-map.yaml 路径
|
|
@@ -114,8 +115,8 @@ export async function rebuildModuleMap(cwd, { force = false } = {}) {
|
|
|
114
115
|
const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
115
116
|
let headCommit = '';
|
|
116
117
|
try {
|
|
117
|
-
|
|
118
|
-
headCommit =
|
|
118
|
+
// QUAL-01 收口:走 git-helper 统一入口(safe.directory + 数组形式)
|
|
119
|
+
headCommit = gitQuiet(cwd, ['rev-parse', '--short', 'HEAD']) || '';
|
|
119
120
|
} catch { /* ignore */ }
|
|
120
121
|
|
|
121
122
|
let yaml = `schema_version: 2\n`;
|
|
@@ -193,8 +194,9 @@ export async function showModuleStatus(cwd) {
|
|
|
193
194
|
|
|
194
195
|
const content = readFileSync(mapPath, 'utf8');
|
|
195
196
|
|
|
196
|
-
//
|
|
197
|
-
|
|
197
|
+
// 简单解析(CRLF 兼容:坑 verify-modules-crlf-blanket-fallback 同类——`$` 锚定正则在
|
|
198
|
+
// Windows 编辑器写出的 CRLF 文件上模块条目全失配,模块数恒显示 0)
|
|
199
|
+
const lines = content.split(/\r?\n/);
|
|
198
200
|
const modules = [];
|
|
199
201
|
let currentModule = null;
|
|
200
202
|
let needsReview = false;
|