sillyspec 3.26.10 → 3.26.13
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 +85 -85
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-commit/SKILL.md +109 -109
- package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
- package/.claude/skills/sillyspec-doctor/SKILL.md +76 -76
- package/.claude/skills/sillyspec-execute/SKILL.md +200 -200
- package/.claude/skills/sillyspec-explore/SKILL.md +95 -95
- package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
- package/.claude/skills/sillyspec-plan/SKILL.md +148 -148
- package/.claude/skills/sillyspec-quick/SKILL.md +115 -111
- package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
- package/.claude/skills/sillyspec-scan/SKILL.md +79 -79
- package/.claude/skills/sillyspec-state/SKILL.md +64 -64
- package/.claude/skills/sillyspec-status/SKILL.md +54 -54
- package/.claude/skills/sillyspec-verify/SKILL.md +83 -83
- package/.claude/skills/sillyspec-workspace/SKILL.md +169 -169
- package/.gitattributes +2 -0
- package/.husky/pre-push +20 -20
- package/README.md +203 -201
- package/SKILL.md +91 -90
- package/package.json +39 -39
- package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
- package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
- package/packages/dashboard/dist/index.html +16 -16
- package/packages/dashboard/package.json +25 -25
- package/packages/dashboard/server/executor.js +95 -86
- package/packages/dashboard/server/index.js +663 -601
- package/packages/dashboard/server/parser.js +526 -526
- package/src/change-risk-profile.js +254 -254
- package/src/classify-change.js +109 -109
- package/src/config-schema.js +313 -310
- package/src/contract-matrix.js +388 -379
- package/src/db.js +370 -350
- package/src/dispatch/backends/local-agent.js +155 -155
- package/src/dispatch/backends/sillyhub-mcp.js +245 -245
- package/src/dispatch/probe.js +246 -210
- package/src/dispatch/strategy.js +121 -121
- package/src/docs-check.js +188 -16
- package/src/docs-debt.js +6 -2
- package/src/docs-gate.js +1 -0
- package/src/doctor-diagnostics.js +898 -711
- package/src/endpoint-extractor.js +401 -401
- package/src/fs-atomic.js +71 -69
- package/src/git-helper.js +82 -79
- package/src/hooks/claude-pre-tool-use.cjs +125 -125
- package/src/hooks/worktree-guard.js +854 -848
- package/src/index.js +251 -20
- package/src/init.js +624 -623
- package/src/knowledge-match.js +130 -130
- package/src/local-detect.js +128 -120
- package/src/machine-interface.js +543 -543
- package/src/migrate.js +127 -127
- package/src/modules.js +7 -5
- package/src/progress/change-registry.js +297 -260
- package/src/progress/consistency-doctor.js +355 -355
- package/src/progress/shared.js +36 -36
- package/src/progress/stage-machine.js +589 -550
- package/src/progress/step-store.js +261 -261
- package/src/progress.js +1285 -1127
- package/src/quicklog.js +724 -693
- package/src/review-tier.js +77 -77
- package/src/run/command.js +1408 -1364
- package/src/run/complete-handlers.js +1450 -1399
- package/src/run/complete.js +1181 -920
- package/src/run/gates.js +878 -755
- package/src/run/prompt.js +765 -761
- package/src/run/quick-audit.js +155 -146
- package/src/run/shared.js +1348 -1215
- package/src/run/stage.js +527 -479
- package/src/scan-postcheck.js +371 -371
- package/src/setup.js +414 -416
- package/src/sillyhub-mcp/client.js +597 -529
- package/src/spec-dir-typo.js +3 -24
- package/src/spec-sync.js +358 -319
- package/src/stage-contract.js +871 -897
- package/src/stage-review.js +549 -549
- package/src/stages/archive.js +156 -155
- package/src/stages/brainstorm-auto.js +236 -236
- package/src/stages/brainstorm.js +597 -597
- package/src/stages/cmd-existence.js +145 -145
- package/src/stages/doctor.js +386 -371
- package/src/stages/execute.js +1014 -1003
- package/src/stages/explore.js +34 -34
- package/src/stages/index.js +29 -29
- package/src/stages/knowledge.js +498 -498
- package/src/stages/plan-postcheck.js +1323 -1302
- package/src/stages/plan.js +642 -623
- package/src/stages/quick.js +153 -151
- package/src/stages/scan.js +607 -607
- package/src/stages/status.js +64 -64
- package/src/stages/verify.js +264 -264
- package/src/sync.js +1325 -1135
- package/src/task-review.js +1204 -1019
- package/src/taskcard.js +178 -0
- package/src/verify-postcheck.js +1199 -1186
- package/src/workflow.js +600 -600
- package/src/worktree-apply.js +1237 -1136
- package/src/worktree-deps.js +453 -353
- package/src/worktree.js +1527 -1481
- package/templates/workflows/archive-impact.yaml +79 -79
- package/templates/workflows/scan-docs.yaml +132 -132
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 配置发现缺口)
|
|
@@ -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;
|
|
@@ -585,12 +611,19 @@ async function main() {
|
|
|
585
611
|
// json 变量(filteredArgs 不含);分支内 BARE_FLAGS(无值)置位剔除、PAIRED_FLAGS(带值)
|
|
586
612
|
// 成对剔除;未知 --xxx 显式 exit 2——不再静默落入文档路径误报"不存在"
|
|
587
613
|
// (历史两次踩坑:--json 恒失效、--suggest 被当文档路径)。
|
|
588
|
-
|
|
614
|
+
// task-03(2026-08-18 platform-map-auto-anchors):白名单加 --fix / --dry-run——
|
|
615
|
+
// --fix 对 fixable 失效引用自动重锚(消费 task-01 的 inv.fix 分类 + task-02 的 applyFixes
|
|
616
|
+
// 写回);--dry-run 预览不写盘,单独传(无 --fix)也走预览路径——design §5.2 行为矩阵
|
|
617
|
+
// --dry-run 列独立存在(「报告修复预览 + exit 1」),即 dryRun 置位 = fix 语义自动生效
|
|
618
|
+
// 但零写盘(本口径记录于此,防歧义)。
|
|
619
|
+
const BARE_FLAGS = ['--suggest', '--fix', '--dry-run'];
|
|
589
620
|
const PAIRED_FLAGS = ['--paths'];
|
|
590
621
|
const rawDocsArgs = filteredArgs.slice(2);
|
|
591
622
|
const docsCheckFlags = [];
|
|
592
623
|
let cliPaths = null;
|
|
593
624
|
let suggest = false;
|
|
625
|
+
let fix = false;
|
|
626
|
+
let dryRun = false;
|
|
594
627
|
for (let i = 0; i < rawDocsArgs.length; i++) {
|
|
595
628
|
const a = rawDocsArgs[i];
|
|
596
629
|
if (a === '--paths' && rawDocsArgs[i + 1] !== undefined) {
|
|
@@ -601,6 +634,8 @@ async function main() {
|
|
|
601
634
|
process.exit(2);
|
|
602
635
|
} else if (BARE_FLAGS.includes(a)) {
|
|
603
636
|
if (a === '--suggest') suggest = true;
|
|
637
|
+
else if (a === '--fix') fix = true;
|
|
638
|
+
else if (a === '--dry-run') dryRun = true;
|
|
604
639
|
} else if (a.startsWith('--')) {
|
|
605
640
|
console.error(`❌ docs check: 未知 flag「${a}」。已知 flag:${[...BARE_FLAGS, ...PAIRED_FLAGS].join(' ')}(--json 为全局 flag)`);
|
|
606
641
|
process.exit(2);
|
|
@@ -608,7 +643,7 @@ async function main() {
|
|
|
608
643
|
docsCheckFlags.push(a);
|
|
609
644
|
}
|
|
610
645
|
}
|
|
611
|
-
const { runDocsCheck, readDocsCheckConfig, DocsCheckConfigError } = await import('./docs-check.js');
|
|
646
|
+
const { runDocsCheck, readDocsCheckConfig, DocsCheckConfigError, applyFixes } = await import('./docs-check.js');
|
|
612
647
|
try {
|
|
613
648
|
// 配置优先级:--paths > local.yaml docs-check.paths > 缺省 docs/**/*.md
|
|
614
649
|
const cfg = readDocsCheckConfig(dir);
|
|
@@ -618,26 +653,85 @@ async function main() {
|
|
|
618
653
|
paths: cliPaths || cfg.paths,
|
|
619
654
|
skip: cfg.skip,
|
|
620
655
|
keywordAssert: cfg.keywordAssert,
|
|
656
|
+
crossRepoRoots: cfg.crossRepoRoots,
|
|
621
657
|
});
|
|
658
|
+
// task-03 修复链路(FR-04,platform-map-auto-anchors):fixActive 才构造 fixes——
|
|
659
|
+
// 仅 fix.fixable===true 且 newLine 为整数的条目可重锚;newRef = ref 的行号部分替换为
|
|
660
|
+
// newLine(文件名原样保留;范围引用 src/a.js:3-8 重锚后收窄为单行语义,design §5.1
|
|
661
|
+
// 「自动改写为该行号」)。多命中/零命中(needs-manual)只报告不修(D-006),无 --force 逃生口。
|
|
662
|
+
// applyFixes 空列表零 IO 直返 {0,[]},全绿时调用无副作用;fixes 与 invalid 条目一一对应,
|
|
663
|
+
// needs-manual 数 = invalid.length - fixes.length。
|
|
664
|
+
const fixActive = fix || dryRun;
|
|
665
|
+
const fixes = [];
|
|
666
|
+
const newRefByInv = new Map();
|
|
667
|
+
if (fixActive && !result.ok) {
|
|
668
|
+
for (const inv of result.invalid) {
|
|
669
|
+
if (inv.fix && inv.fix.fixable === true && Number.isInteger(inv.fix.newLine)) {
|
|
670
|
+
const newRef = inv.ref.replace(/(\d+)(?:-\d+)?$/, String(inv.fix.newLine));
|
|
671
|
+
fixes.push({ doc: inv.doc, docLine: inv.docLine, ref: inv.ref, newRef });
|
|
672
|
+
newRefByInv.set(inv, newRef);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const fixResult = fixActive ? applyFixes(dir, fixes, dryRun ? { dryRun: true } : {}) : null;
|
|
622
677
|
if (json) {
|
|
678
|
+
// --fix 与 --json 组合保持 stdout 纯 JSON 可解析(constraints):修复统计作 result.fixReport
|
|
679
|
+
// 附加后整体 stringify,主体仍是 runDocsCheck 返回(invalid[].fix 是 task-01 设计内增量)
|
|
680
|
+
if (fixResult) result.fixReport = { applied: fixResult.applied, skipped: fixResult.skipped.length, dryRun };
|
|
623
681
|
console.log(JSON.stringify(result, null, 2));
|
|
624
682
|
} else {
|
|
625
683
|
for (const w of result.warnings) console.warn(`⚠️ ${w}`);
|
|
684
|
+
if (result.crossRepoSkipped > 0) {
|
|
685
|
+
console.log(`ℹ️ 跨仓 repo:// 引用跳过 ${result.crossRepoSkipped} 处(未配 docs-check.cross_repo_roots 映射;配置后走同款行号+关键词校验)`);
|
|
686
|
+
}
|
|
626
687
|
if (result.ok) {
|
|
627
688
|
console.log(`✅ docs check: ${result.total} 处引用全通过(其中 ${result.kwChecked} 处带关键词断言)`);
|
|
628
689
|
} else {
|
|
629
690
|
console.error(`\n❌ docs check: ${result.invalid.length}/${result.total} 处引用失效:`);
|
|
691
|
+
let appliedLeft = fixResult ? fixResult.applied : 0;
|
|
692
|
+
const skippedQueue = fixResult ? [...fixResult.skipped] : [];
|
|
630
693
|
for (const inv of result.invalid) {
|
|
631
|
-
|
|
694
|
+
const newRef = newRefByInv.get(inv);
|
|
695
|
+
if (newRef !== undefined && appliedLeft > 0) {
|
|
696
|
+
// 已应用(dry-run 为将应用):归因按构造顺序消耗 applied 计数——写回 skipped 仅
|
|
697
|
+
// 防御路径(文档消失/行内失配)触发,CLI 正常流 fixable 必然落位
|
|
698
|
+
console.error(` ✅ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${newRef}${dryRun ? '(dry-run 未写盘)' : ''}`);
|
|
699
|
+
appliedLeft--;
|
|
700
|
+
} else if (newRef !== undefined) {
|
|
701
|
+
const s = skippedQueue.shift();
|
|
702
|
+
console.error(` ⚠️ [${inv.doc}:L${inv.docLine}] ${inv.ref} → 写回跳过:${s ? s.reason : '未知原因'}`);
|
|
703
|
+
} else if (fixResult) {
|
|
704
|
+
// needs-manual:多命中歧义/零命中/无 token——fix.reason 含候选行号,交人工
|
|
705
|
+
console.error(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${(inv.fix && inv.fix.reason) || inv.reason}(待人工)`);
|
|
706
|
+
} else {
|
|
707
|
+
console.error(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${inv.reason}`);
|
|
708
|
+
}
|
|
632
709
|
if (suggest && inv.suggest && inv.suggest.length > 0) {
|
|
633
710
|
console.error(` 💡 候选行号: ${inv.suggest.join(', ')}(token 命中行,人工确认后更新文档锚)`);
|
|
634
711
|
}
|
|
635
712
|
}
|
|
713
|
+
if (fixResult) {
|
|
714
|
+
console.error(`\n🔧 重锚报告:${fixResult.applied} 处已${dryRun ? '预览' : '改写'}${dryRun ? '(dry-run 未写盘)' : ''}、${result.invalid.length - fixes.length} 处待人工${fixResult.skipped.length > 0 ? `、${fixResult.skipped.length} 处写回跳过` : ''}。`);
|
|
715
|
+
}
|
|
636
716
|
console.error(`\n修复指引:行号漂移 → 更新文档行号到当前源码;文件删改名 → 更新引用路径;`);
|
|
637
717
|
console.error(`关键词缺失但行号正确 → 确认符号是否改名,改文档 token 或行号。`);
|
|
638
718
|
}
|
|
639
719
|
}
|
|
640
|
-
|
|
720
|
+
// exit code(design §5.2 行为矩阵逐字对齐):全绿 → 0(--fix 无操作);--fix 后全部
|
|
721
|
+
// fixable 修复完且无 needs-manual 残留 → 0;仍有 needs-manual 或写回 skipped → 1;
|
|
722
|
+
// 配置错误 → 2(上方 catch DocsCheckConfigError 维持现状)。判定不重跑 runDocsCheck:
|
|
723
|
+
// fixable 条目数 === applied 且 needs-manual 数为 0 即全修(定点替换确定性成功,复查无
|
|
724
|
+
// 增量信息)。无 --fix/--dry-run 时维持 result.ok ? 0 : 1 现状(D-004 逐字节一致)。
|
|
725
|
+
if (result.ok) {
|
|
726
|
+
process.exit(0);
|
|
727
|
+
} else if (fixActive) {
|
|
728
|
+
const allFixed = fixResult.applied === fixes.length &&
|
|
729
|
+
fixResult.skipped.length === 0 &&
|
|
730
|
+
result.invalid.length - fixes.length === 0;
|
|
731
|
+
process.exit(allFixed ? 0 : 1);
|
|
732
|
+
} else {
|
|
733
|
+
process.exit(1);
|
|
734
|
+
}
|
|
641
735
|
} catch (e) {
|
|
642
736
|
if (e instanceof DocsCheckConfigError) {
|
|
643
737
|
console.error(`❌ docs check 配置错误:${e.message}`);
|
|
@@ -691,7 +785,7 @@ async function main() {
|
|
|
691
785
|
}
|
|
692
786
|
process.exit(g.exitCode);
|
|
693
787
|
} else {
|
|
694
|
-
console.log('用法: sillyspec docs migrate | sillyspec docs check [--paths <glob,...>] [--json] [--suggest] | sillyspec docs gate [--init-baseline] [--json]');
|
|
788
|
+
console.log('用法: sillyspec docs migrate | sillyspec docs check [--paths <glob,...>] [--json] [--suggest] [--fix] [--dry-run] | sillyspec docs gate [--init-baseline] [--json]');
|
|
695
789
|
process.exit(2);
|
|
696
790
|
}
|
|
697
791
|
break;
|
|
@@ -714,6 +808,11 @@ async function main() {
|
|
|
714
808
|
const { runCommand } = await import('./run.js')
|
|
715
809
|
// 平台模式(--spec-dir 已指定)时,--dir 是明确的 source_root,不应被 resolveEffectiveDir 纠正
|
|
716
810
|
const effectiveDir = specDir ? dir : resolveEffectiveDir(dir)
|
|
811
|
+
// 下行 pull:对齐顶层 stage 别名块(task-10 / D-009 / FR-04)。修复前 case 'run' 漏接,
|
|
812
|
+
// sillyspec run <stage>(含 --done)从不 pull,与别名路径行为分裂(ql-20260818-008)。
|
|
813
|
+
// 未连接平台静默跳过;本地脏时 pull 内部 skipIfLocalDirty 保守跳过,不覆盖本地进度。
|
|
814
|
+
const { triggerPullActiveChange } = await import('./run/shared.js')
|
|
815
|
+
await triggerPullActiveChange(effectiveDir)
|
|
717
816
|
await runCommand(filteredArgs.slice(1), effectiveDir, specDir, { json })
|
|
718
817
|
break
|
|
719
818
|
}
|
|
@@ -724,8 +823,9 @@ async function main() {
|
|
|
724
823
|
// slice(1) 去掉的是 'run' 字面量,这里 command 本身就是 stage 名不能丢。
|
|
725
824
|
case 'doctor': {
|
|
726
825
|
const doctorEffectiveDir = specDir ? dir : resolveEffectiveDir(dir);
|
|
727
|
-
// 执行流:--cleanup-remnant / --dump-db(结构化诊断之外的修复/取证动作)
|
|
826
|
+
// 执行流:--cleanup-remnant / --cleanup-ghosts / --dump-db(结构化诊断之外的修复/取证动作)
|
|
728
827
|
const cleanupRemnant = filteredArgs.includes('--cleanup-remnant');
|
|
828
|
+
const cleanupGhosts = filteredArgs.includes('--cleanup-ghosts');
|
|
729
829
|
const dumpDbFlag = filteredArgs.includes('--dump-db');
|
|
730
830
|
const doctorConfirm = filteredArgs.includes('--confirm');
|
|
731
831
|
const pathIdx = filteredArgs.indexOf('--path');
|
|
@@ -756,6 +856,20 @@ async function main() {
|
|
|
756
856
|
// 复用顶层静态 import(line 12),不动态 await import
|
|
757
857
|
const pm = new ProgressManager({ specDir: resolvePlatformSpecDir(doctorEffectiveDir, specDir) });
|
|
758
858
|
const specBase = resolvePlatformSpecDir(doctorEffectiveDir, specDir) || join(doctorEffectiveDir, '.sillyspec');
|
|
859
|
+
// 坑 doctor-align-bypass-review-gate(2026-08-20 实证):align 直写 completed 绕过
|
|
860
|
+
// completeStep,此前 execute 的 Stage/Task Review Gate 被整体跳过——worktree 清理后的
|
|
861
|
+
// 恢复场景恰是最需要审计的时刻。confirm 落盘前跑同源只读校验(dry-run 不拦,保持只读语义)。
|
|
862
|
+
if (doctorConfirm) {
|
|
863
|
+
const { enforceAlignExecuteReviewGate } = await import('./run/gates.js');
|
|
864
|
+
const gateBlocked = await enforceAlignExecuteReviewGate({
|
|
865
|
+
cwd: doctorEffectiveDir, changeName: alignChange, specBase,
|
|
866
|
+
});
|
|
867
|
+
if (gateBlocked) {
|
|
868
|
+
console.error(' 对齐未执行(review 校验未过)。补齐 review 后重跑本命令。');
|
|
869
|
+
process.exitCode = 1;
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
759
873
|
let r;
|
|
760
874
|
try {
|
|
761
875
|
r = pm.alignExecuteToPlan(doctorEffectiveDir, alignChange, specBase, { confirm: doctorConfirm });
|
|
@@ -797,6 +911,26 @@ async function main() {
|
|
|
797
911
|
process.exitCode = r.errors.length > 0 ? 1 : 0;
|
|
798
912
|
break;
|
|
799
913
|
}
|
|
914
|
+
if (cleanupGhosts) {
|
|
915
|
+
// SS-2(2026-08-20):归档幽灵行(db active 无目录)。默认 dry-run,--confirm 才写。
|
|
916
|
+
// SS-2b:扩空壳目录(active + 目录 0 文件 + last_active 超 7 天),归档行并移除空目录。
|
|
917
|
+
const { cleanupGhostChanges } = await import('./doctor-diagnostics.js');
|
|
918
|
+
const r = await cleanupGhostChanges({ cwd: doctorEffectiveDir, specDir, confirm: doctorConfirm });
|
|
919
|
+
if (json) {
|
|
920
|
+
console.log(JSON.stringify(r, null, 2));
|
|
921
|
+
} else {
|
|
922
|
+
console.log(`👻 幽灵 active 记录(db active 无目录 / 空壳目录超 7 天)${doctorConfirm ? '已归档' : '待清理(dry-run)'}:${r.count} 个 [db: ${r.db_path || '(未找到)'}]`);
|
|
923
|
+
for (const n of r.ghosts) console.log(` ${doctorConfirm ? '✅' : '-'} ${n}`);
|
|
924
|
+
for (const n of (r.empty_shells || [])) console.log(` ${doctorConfirm ? '✅' : '🫙'} ${n}(空壳目录${doctorConfirm ? ',空目录已移除' : ''})`);
|
|
925
|
+
for (const n of (r.skipped_nonempty || [])) console.log(` ⏭️ ${n}(复查时目录已非空,已放过——可能是活跃变更)`);
|
|
926
|
+
for (const e of r.errors) console.log(` ❌ ${e.name || ''}: ${e.error}`);
|
|
927
|
+
if (!doctorConfirm && r.count > 0) console.log(`
|
|
928
|
+
加 --confirm 执行归档(仅改 status=archived,可逆;空壳同时移除 0 文件空目录,不删任何有内容的目录)。`);
|
|
929
|
+
if (r.reason) console.log(`ℹ️ ${r.reason}`);
|
|
930
|
+
}
|
|
931
|
+
process.exitCode = r.errors.length > 0 ? 1 : 0;
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
800
934
|
if (dumpDbFlag) {
|
|
801
935
|
if (!dbPath) { console.error('❌ --dump-db 需要 --path <db 路径>'); process.exitCode = 2; break; }
|
|
802
936
|
const { dumpDb } = await import('./doctor-diagnostics.js');
|
|
@@ -881,6 +1015,10 @@ async function main() {
|
|
|
881
1015
|
for (let i = 1; i < args.length; i++) {
|
|
882
1016
|
if (args[i] === '--port' && args[i + 1]) {
|
|
883
1017
|
port = parseInt(args[i + 1], 10);
|
|
1018
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
1019
|
+
console.error(`❌ 无效端口: ${args[i + 1]}(应为 1-65535 整数)`);
|
|
1020
|
+
process.exit(2);
|
|
1021
|
+
}
|
|
884
1022
|
i++;
|
|
885
1023
|
} else if (args[i] === '--no-open') {
|
|
886
1024
|
openBrowser = false;
|
|
@@ -927,7 +1065,7 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
927
1065
|
|
|
928
1066
|
用法:
|
|
929
1067
|
sillyspec worktree create <change-name> [--base <branch>] 创建隔离 worktree
|
|
930
|
-
sillyspec worktree apply <change-name> [--check-only]
|
|
1068
|
+
sillyspec worktree apply <change-name> [--check-only] [--base merge-base|baseline] [--merge] 校验并应用变更到主工作区
|
|
931
1069
|
sillyspec worktree assess <change-name> 风险审计 + 自动 apply
|
|
932
1070
|
sillyspec worktree diff <change-name> [--base <commit>] 查看 worktree 相对 base 的变更
|
|
933
1071
|
sillyspec worktree list 列出所有活跃 worktree
|
|
@@ -974,11 +1112,24 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
974
1112
|
}
|
|
975
1113
|
case 'apply': {
|
|
976
1114
|
if (!wtName) {
|
|
977
|
-
console.error('❌ 用法: sillyspec worktree apply <change-name> [--check-only] [--merge]');
|
|
1115
|
+
console.error('❌ 用法: sillyspec worktree apply <change-name> [--check-only] [--base merge-base|baseline] [--merge]');
|
|
978
1116
|
process.exit(1);
|
|
979
1117
|
}
|
|
980
1118
|
const checkOnly = args.includes('--check-only');
|
|
981
1119
|
const merge = args.includes('--merge');
|
|
1120
|
+
|
|
1121
|
+
// 解析 --base 参数(默认 merge-base)
|
|
1122
|
+
let base = 'merge-base';
|
|
1123
|
+
const baseIdx = args.indexOf('--base');
|
|
1124
|
+
if (baseIdx !== -1) {
|
|
1125
|
+
const baseVal = args[baseIdx + 1];
|
|
1126
|
+
if (!baseVal || (!['merge-base', 'baseline'].includes(baseVal))) {
|
|
1127
|
+
console.error('❌ --base 参数值非法,必须是 merge-base 或 baseline');
|
|
1128
|
+
process.exit(1);
|
|
1129
|
+
}
|
|
1130
|
+
base = baseVal;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
982
1133
|
const { applyWorktree } = await import('./worktree-apply.js');
|
|
983
1134
|
// W3 task-09:apply 链路构造 ctx(D-013),让 task-05 applyWorktree 按 ctx 区分主仓 A5 / 跨仓 no-op。
|
|
984
1135
|
// 跨仓 apply=no-op(G1 D-009):校验 review.head 是跨仓真实 commit + 跳过 wm.cleanup。
|
|
@@ -991,7 +1142,7 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
991
1142
|
console.error(`❌ apply 失败:跨仓 MultiRepoContext 构造失败(${e.message})`);
|
|
992
1143
|
process.exit(1);
|
|
993
1144
|
}
|
|
994
|
-
const result = applyWorktree(wtName, { cwd: dir, checkOnly, merge, ctx: _applyCtx });
|
|
1145
|
+
const result = applyWorktree(wtName, { cwd: dir, checkOnly, merge, base, ctx: _applyCtx });
|
|
995
1146
|
|
|
996
1147
|
if (result.errors.length > 0) {
|
|
997
1148
|
console.error(`❌ 校验失败:`);
|
|
@@ -1216,7 +1367,7 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
1216
1367
|
const staleHours = staleIdx !== -1 && args[staleIdx + 1] ? parseInt(args[staleIdx + 1], 10) : 24;
|
|
1217
1368
|
const changeIdx = args.indexOf('--change');
|
|
1218
1369
|
const changeName = changeIdx !== -1 && args[changeIdx + 1] ? args[changeIdx + 1] : null;
|
|
1219
|
-
const diag = wm.doctor({ fix: fixFlag, staleHours, changeName });
|
|
1370
|
+
const diag = await wm.doctor({ fix: fixFlag, staleHours, changeName });
|
|
1220
1371
|
if (diag.issues.length === 0) {
|
|
1221
1372
|
console.log('✅ worktree 健康检查通过,无异常');
|
|
1222
1373
|
} else {
|
|
@@ -1355,6 +1506,15 @@ SillySpec platform — SillyHub 平台同步
|
|
|
1355
1506
|
process.exit(1);
|
|
1356
1507
|
}
|
|
1357
1508
|
|
|
1509
|
+
// 体检 SEC-05:platform 子命令族统一校验 --change 值——此前只有 run 命令族走
|
|
1510
|
+
// assertSafeChangeName,platform sync/pull/resolve 直接透传,冲突文件名
|
|
1511
|
+
// join(runtimeDir, `sync-conflict-${changeName}.json`) 可被 ../ 穿越出 .runtime
|
|
1512
|
+
{
|
|
1513
|
+
const pChangeIdx = args.indexOf('--change');
|
|
1514
|
+
const pChangeVal = pChangeIdx >= 0 && args[pChangeIdx + 1] ? args[pChangeIdx + 1] : null;
|
|
1515
|
+
if (pChangeVal) assertSafeChangeName(pChangeVal, 'platform --change 变更名');
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1358
1518
|
switch (platformSub) {
|
|
1359
1519
|
case 'pointer': {
|
|
1360
1520
|
// 指针状态检查(不依赖 sync 模块)
|
|
@@ -1421,7 +1581,11 @@ SillySpec platform — SillyHub 平台同步
|
|
|
1421
1581
|
const tokenIdx = args.indexOf('--token');
|
|
1422
1582
|
const token = tokenIdx >= 0 && args[tokenIdx + 1] ? args[tokenIdx + 1] : undefined;
|
|
1423
1583
|
if (!token) {
|
|
1424
|
-
|
|
1584
|
+
// 缺 token 直接终止(体检 HUB-02):交互式输入尚未实现(task-11),此前
|
|
1585
|
+
// 继续执行会把 undefined 序列化成字符串 "undefined" 写入 local.yaml,
|
|
1586
|
+
// 报"连接成功"后所有同步持续 401(Bearer undefined)
|
|
1587
|
+
console.error('❌ 缺少 --token <token>(交互式输入暂未实现);请在 SillyHub 平台获取 token 后重试');
|
|
1588
|
+
process.exit(1);
|
|
1425
1589
|
}
|
|
1426
1590
|
await syncModule.connect(url, token, dir);
|
|
1427
1591
|
break;
|
|
@@ -1544,6 +1708,8 @@ SillySpec platform — SillyHub 平台同步
|
|
|
1544
1708
|
console.error('❌ 必须恰好指定 --keep-local / --take-platform / --abort 之一');
|
|
1545
1709
|
process.exit(1);
|
|
1546
1710
|
}
|
|
1711
|
+
// 位置参数变更名与 --change 同规格校验(SEC-05:resolve 会按该名重推/删冲突文件)
|
|
1712
|
+
assertSafeChangeName(resolveName, 'platform resolve 变更名');
|
|
1547
1713
|
const modeMap = { '--keep-local': 'keep-local', '--take-platform': 'take-platform', '--abort': 'abort' };
|
|
1548
1714
|
const resolveMode = modeMap[resolveFlags[0]];
|
|
1549
1715
|
const r = await syncModule.resolve(resolveName, resolveMode, dir);
|
|
@@ -1591,6 +1757,71 @@ SillySpec platform — SillyHub 平台同步
|
|
|
1591
1757
|
}
|
|
1592
1758
|
break;
|
|
1593
1759
|
}
|
|
1760
|
+
case 'taskcard': {
|
|
1761
|
+
const tcName = filteredArgs.slice(1).find(a => !a.startsWith('-'));
|
|
1762
|
+
if (!tcName || tcName === 'help' || tcName === '--help' || tcName === '-h') {
|
|
1763
|
+
console.log(`
|
|
1764
|
+
SillySpec taskcard — 生成 Windows 安全的 TaskCard 骨架
|
|
1765
|
+
|
|
1766
|
+
用法:
|
|
1767
|
+
sillyspec taskcard <change-name> --task task-01[,task-02...] [--title <t>] [--title-zh <t>] [--force]
|
|
1768
|
+
sillyspec taskcard <change-name> --all [--force]
|
|
1769
|
+
|
|
1770
|
+
骨架由 CLI 直写(LF 行尾 + frontmatter 闭合 + 硬校验 9 字段齐全),标题自动取自 plan.md
|
|
1771
|
+
checkbox 行;之后用 Edit 填充占位符即可,勿手写整卡(CRLF/漏闭合 ---/漏字段的源头治理)。
|
|
1772
|
+
`);
|
|
1773
|
+
break;
|
|
1774
|
+
}
|
|
1775
|
+
try { assertSafeChangeName(tcName, '变更名'); }
|
|
1776
|
+
catch (e) { console.error(`❌ ${e.message}`); process.exit(2); }
|
|
1777
|
+
|
|
1778
|
+
const force = filteredArgs.includes('--force');
|
|
1779
|
+
const all = filteredArgs.includes('--all');
|
|
1780
|
+
const getVal = (flag) => {
|
|
1781
|
+
const idx = filteredArgs.indexOf(flag);
|
|
1782
|
+
return idx !== -1 && filteredArgs[idx + 1] && !filteredArgs[idx + 1].startsWith('-')
|
|
1783
|
+
? filteredArgs[idx + 1] : null;
|
|
1784
|
+
};
|
|
1785
|
+
const taskVal = getVal('--task');
|
|
1786
|
+
const titleVal = getVal('--title');
|
|
1787
|
+
const titleZhVal = getVal('--title-zh');
|
|
1788
|
+
|
|
1789
|
+
if (all && taskVal) {
|
|
1790
|
+
console.error('❌ --all 与 --task 互斥(--all 取 plan.md 全部任务,--task 显式指定)');
|
|
1791
|
+
process.exit(1);
|
|
1792
|
+
}
|
|
1793
|
+
if (!all && !taskVal) {
|
|
1794
|
+
console.error('❌ 用法: sillyspec taskcard <change-name> --task task-01[,task-02...] | --all [--force]');
|
|
1795
|
+
process.exit(1);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
let taskIds;
|
|
1799
|
+
if (all) {
|
|
1800
|
+
taskIds = 'all';
|
|
1801
|
+
} else {
|
|
1802
|
+
const { normalizeTaskId } = await import('./taskcard.js');
|
|
1803
|
+
try {
|
|
1804
|
+
taskIds = taskVal.split(',').map(s => s.trim()).filter(Boolean).map(normalizeTaskId);
|
|
1805
|
+
} catch (e) {
|
|
1806
|
+
console.error(`❌ ${e.message}`);
|
|
1807
|
+
process.exit(1);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
const { cmdTaskcard } = await import('./taskcard.js');
|
|
1812
|
+
try {
|
|
1813
|
+
const result = cmdTaskcard(tcName, { cwd: dir, specDir, taskIds, title: titleVal, titleZh: titleZhVal, force });
|
|
1814
|
+
for (const f of result.created) console.log(`✅ 已生成: ${f}`);
|
|
1815
|
+
for (const f of result.skipped) console.log(`⏭️ 已存在,跳过(--force 覆盖): ${f}`);
|
|
1816
|
+
if (result.created.length > 0) {
|
|
1817
|
+
console.log(` 下一步:用 Edit tool 填充占位符(allowed_paths/goal/implementation/acceptance/verify/constraints),勿用 Write 整文件重写。`);
|
|
1818
|
+
}
|
|
1819
|
+
} catch (e) {
|
|
1820
|
+
console.error(`❌ ${e.message}`);
|
|
1821
|
+
process.exit(1);
|
|
1822
|
+
}
|
|
1823
|
+
break;
|
|
1824
|
+
}
|
|
1594
1825
|
case 'change-rename': {
|
|
1595
1826
|
const oldName = filteredArgs[1];
|
|
1596
1827
|
const newName = filteredArgs[2];
|