sillyspec 3.28.8 → 3.28.10
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/full.json +1 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -224
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +118 -117
- package/AGENTS.md +3 -2
- package/README.md +35 -11
- package/SKILL.md +24 -10
- package/SillySpec-/350/203/275/345/212/233/344/272/256/347/202/271/345/205/250/346/231/257-2026-09-14.pptx +0 -0
- package/package.json +1 -1
- package/src/change-list.js +73 -20
- package/src/change-risk-profile.js +20 -0
- package/src/config-schema.js +62 -0
- package/src/contract-matrix.js +84 -5
- package/src/cross-repo-reconcile.js +134 -0
- package/src/db.js +11 -3
- package/src/decision-distill.js +15 -3
- package/src/design-facts.js +89 -20
- package/src/docs-gate.js +105 -6
- package/src/doctor-diagnostics.js +224 -2
- package/src/friction-ledger.js +171 -0
- package/src/hooks/worktree-guard.js +13 -1
- package/src/index.js +206 -18
- package/src/knowledge-classify.js +382 -0
- package/src/knowledge-hits.js +74 -0
- package/src/knowledge-stats.js +180 -0
- package/src/probe7-anchor-check.js +69 -0
- package/src/progress/change-registry.js +183 -8
- package/src/progress/shared.js +3 -2
- package/src/progress.js +100 -8
- package/src/quick-gate-profile.js +251 -0
- package/src/quicklog.js +164 -18
- package/src/review-dispatch.js +2 -1
- package/src/run/command.js +131 -13
- package/src/run/complete-handlers.js +511 -21
- package/src/run/complete.js +123 -23
- package/src/run/concurrent-detect.js +98 -0
- package/src/run/gate-snapshot.js +110 -2
- package/src/run/gates.js +141 -16
- package/src/run/prompt.js +221 -11
- package/src/run/quick-audit.js +111 -13
- package/src/run/shared.js +147 -11
- package/src/run/stage.js +6 -0
- package/src/scan-diff.js +61 -30
- package/src/scan-postcheck.js +74 -0
- package/src/scan-refresh.js +477 -0
- package/src/scan-staleness.js +25 -13
- package/src/scope-audit.js +456 -56
- package/src/stage-contract.js +185 -2
- package/src/stage-review.js +23 -7
- package/src/stages/brainstorm.js +6 -0
- package/src/stages/cmd-existence.js +9 -3
- package/src/stages/execute.js +85 -3
- package/src/stages/knowledge.js +76 -6
- package/src/stages/plan-postcheck.js +32 -5
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +1 -0
- package/src/sync-noise.js +170 -6
- package/src/sync.js +141 -9
- package/src/task-review.js +366 -46
- package/src/verify-postcheck.js +212 -55
- package/src/verify-probes.js +727 -16
- package/src/worktree-apply.js +685 -48
- package/src/worktree-deps.js +124 -3
- package/src/worktree.js +391 -15
- package/templates/agents-instruction.md +2 -2
- package/templates/prompts/testcase-design.md +2 -1
- package/templates/prompts/verify-probes.md +4 -1
- package/.patch16-tmp.mjs +0 -169
- package/sa-rename-probe/src/new.js +0 -4
package/src/index.js
CHANGED
|
@@ -60,6 +60,9 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
60
60
|
--force-rescan 覆盖已有 scan 文档保护
|
|
61
61
|
--diff [--base <commit>] [--full] [--report] scan 文档 vs 源码漂移清单(纯只读)
|
|
62
62
|
(子命令: sillyspec scan diff 等价 --diff)
|
|
63
|
+
refresh [--project <名>] [--force] [--json] 增量刷新①拍:门控+受影响文档手术工单
|
|
64
|
+
refresh --done [--docs a.md,b.md] [--force] 增量刷新②拍:内容比对+per-doc 基线推进+postcheck
|
|
65
|
+
(检出极限:refresh 只覆盖带 file:line 引用的检出项,不声称文档与源码一致)
|
|
63
66
|
archive: --confirm 归档确认(必须)
|
|
64
67
|
auto: --mode <模式> 显式指定流程模式
|
|
65
68
|
平台: --runtime-root <path> / --workspace-id <id> / --scan-run-id <id>
|
|
@@ -70,6 +73,9 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
70
73
|
check | repair [--apply] | validate | reset [--stage X] |
|
|
71
74
|
batch --total N --completed M [--failed F] [--skipped K] | batch --status
|
|
72
75
|
|
|
76
|
+
sillyspec wt-commit [--change <名>] -m <信息> [--pathspec-from-file <f>] -- <path...>
|
|
77
|
+
worktree 内 per-task 串行化提交(文件锁排队 + 强制显式 pathspec,
|
|
78
|
+
防 add -A 卷入并行兄弟 WIP;worktree 内可省 --change,从 cwd 推断)
|
|
73
79
|
sillyspec worktree <cmd> git worktree 隔离管理(execute 阶段相关)
|
|
74
80
|
create <change> [--base <branch>] 创建隔离 worktree
|
|
75
81
|
apply <change> [--check-only] 校验并应用变更到主工作区
|
|
@@ -1109,6 +1115,18 @@ task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标
|
|
|
1109
1115
|
console.log(`📄 已补齐缺失槽段(不触碰既有正文): ${vpAdded.added.join('、')}`)
|
|
1110
1116
|
}
|
|
1111
1117
|
}
|
|
1118
|
+
// 2026-09-14-acceptance-test-matrix:探针 7 验收×测试覆盖矩阵骨架幂等补段——
|
|
1119
|
+
// verify-result.md 已存在但缺「#### 探针 7」段且 applicable(有 TaskCard)时补矩阵
|
|
1120
|
+
// 骨架(学 backfillMissingEvidenceSlots 形态,不触既有正文;判定/证据槽 fail-closed
|
|
1121
|
+
// 由 verify 门禁负责)。本变更自举:verify-result.md 先于特性存在的存量走此补段。
|
|
1122
|
+
try {
|
|
1123
|
+
const { ensureAcceptanceMatrixSection } = await import('./verify-probes.js')
|
|
1124
|
+
const vpMatrix = ensureAcceptanceMatrixSection(vpReportPath, vpResult.probe7)
|
|
1125
|
+
if (vpMatrix.added) {
|
|
1126
|
+
try { await mirrorInitArtifact(vpReportPath, 'verify-result.md', readFileSync(vpReportPath, 'utf8')) } catch { /* 镜像失败不阻断 */ }
|
|
1127
|
+
console.log(`🧩 探针 7 验收×测试覆盖矩阵骨架段已补齐(判定=四选一/证据两槽待填,命中≠判定;不触碰既有正文): ${vpReportPath}${vpPlatformNote}`)
|
|
1128
|
+
}
|
|
1129
|
+
} catch { /* 矩阵补段失败不阻断 --init */ }
|
|
1112
1130
|
// P0-1(noai-ir-roadmap §3)两件机械预填,均幂等 fail-soft:
|
|
1113
1131
|
// ①决策追踪矩阵机械半边——D→FR→task 链自 decisions.md × tasks/*.md frontmatter
|
|
1114
1132
|
// 结构化字段构建,Evidence/状态两列留 agent 逐格复核;
|
|
@@ -1309,10 +1327,14 @@ task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标
|
|
|
1309
1327
|
// 路由形态对齐 verify-probes(:897)/ module-impact(:1041)先例——--change 必填缺参
|
|
1310
1328
|
// 用法错 exit 2、assertSafeChangeName 消毒、动态 import、--json 结构化输出、运行错
|
|
1311
1329
|
// fail-soft 出错误摘要 exit 1 不抛栈。
|
|
1330
|
+
// 画像双出口(2026-09-14-quick-exit-tiered-gates task-03,FR-04 / D-008):quick 会话的
|
|
1331
|
+
// gateProfile 由 computeChangeScopeAudit 结果对象携带——--json 出口经 ...saResult 展开
|
|
1332
|
+
// 自动透出(gateProfile-json)、表格出口经 renderScopeAuditTable 按存在性追加 [gate]
|
|
1333
|
+
// 画像段(gate-table-section),本层零分支零新解析;--file 子命令行为不变。
|
|
1312
1334
|
const saChangeIdx = args.indexOf('--change');
|
|
1313
1335
|
const saChange = saChangeIdx >= 0 && args[saChangeIdx + 1] && !String(args[saChangeIdx + 1]).startsWith("--") ? args[saChangeIdx + 1] : null;
|
|
1314
1336
|
if (!saChange) {
|
|
1315
|
-
console.error('用法: sillyspec scope-audit --change <name> [--file <path>] [--json] [--spec-dir <path>]\n 变更范围对账:计划×实际三态全表 + 行数(quick 会话传 quick-<8hex>
|
|
1337
|
+
console.error('用法: sillyspec scope-audit --change <name> [--file <path>] [--json] [--spec-dir <path>]\n 变更范围对账:计划×实际三态全表 + 行数(quick 会话传 quick-<8hex> 出归属表,附 [gate] 分级门禁画像——表格画像段与 --json gateProfile 字段;已归档变更可查——快照冻结记录态);--file <path> 看单文件变化内容(对账同源锚点的 git diff);advisory 只读展示,不构成门禁');
|
|
1316
1338
|
process.exit(2);
|
|
1317
1339
|
}
|
|
1318
1340
|
assertSafeChangeName(saChange, '--change 变更名');
|
|
@@ -1340,10 +1362,12 @@ task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标
|
|
|
1340
1362
|
}
|
|
1341
1363
|
// full-flow 变更目录存在性预检(endpoints baseline 先例同款):post-apply 形态下实际侧三源
|
|
1342
1364
|
// 对「变更不存在」不敏感(status 成功即 ok=true,会把主仓脏区误当实际侧),靠预检给明确
|
|
1343
|
-
// 错误摘要 exit 1;quick 会话 id 无 changes/ 目录(guard
|
|
1365
|
+
// 错误摘要 exit 1;quick 会话 id 无 changes/ 目录(guard 祖先链定位),不预检;ql-xxx
|
|
1366
|
+
// 条目号同 quick 会话 id(ql-xxx 反查走 quicklog/patches,亦无 changes/ 目录)不预检。
|
|
1344
1367
|
// 归档形态兼容(quick-8aa52289):changes/<名> 不在 → 接受 changes/archive/<名>(scope-audit
|
|
1345
1368
|
// 内部走快照记录态/HEAD 未提交窗口兜底)。
|
|
1346
1369
|
if (!/^quick-[0-9a-f]{8}$/.test(saChange)
|
|
1370
|
+
&& !/^ql-\d{8}-\d{3}-[0-9a-f]{4}$/.test(saChange)
|
|
1347
1371
|
&& !existsSync(join(saSpecBase, 'changes', saChange))
|
|
1348
1372
|
&& !existsSync(join(saSpecBase, 'changes', 'archive', saChange))) {
|
|
1349
1373
|
console.error(`❌ 变更目录不存在(活跃与归档区均未找到): ${join(saSpecBase, 'changes', saChange)}(确认 --change 名,或 --spec-dir 指向主仓 .sillyspec)`);
|
|
@@ -2244,6 +2268,8 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
|
|
|
2244
2268
|
// usage 输出且 exit 0——gate 形同虚设(docs-signals 范围内文件+hook 已就位,仅此分支缺)。
|
|
2245
2269
|
// B9(2026-08-16):flag 白名单化(对齐 docs check 分支)——未知 --xxx 显式 exit 2
|
|
2246
2270
|
// (interface-contract §1.3b 宣称),并接线 --paths 透传 runDocsGate.checkOpts(原被忽略)。
|
|
2271
|
+
// 坑 docs-gate-stale-baseline(ql-20260915-004):超基线分支实测 origin/main 兜底——
|
|
2272
|
+
// 未劣于远端放行 + 提示重锚;--json 输出含 originCount 字段(null=未实测/快路径)。
|
|
2247
2273
|
const GATE_BARE_FLAGS = ['--init-baseline'];
|
|
2248
2274
|
const GATE_PAIRED_FLAGS = ['--paths', '--against'];
|
|
2249
2275
|
const rawGateArgs = filteredArgs.slice(2);
|
|
@@ -2546,6 +2572,40 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
|
|
|
2546
2572
|
})
|
|
2547
2573
|
break
|
|
2548
2574
|
}
|
|
2575
|
+
// scan refresh 子命令(2026-09-14-scan-incremental-refresh D-002@v1:增量刷新两拍交互)。
|
|
2576
|
+
// ①拍纯读出工单(含 guard 握手写);②拍 --done 盖章收尾。与 diff 同为旁路:跳过
|
|
2577
|
+
// triggerPullActiveChange、不走 scan 主流程。--project 支持子项目(缺省仓库根 basename,
|
|
2578
|
+
// 与 scan facts 同口径)。
|
|
2579
|
+
if (filteredArgs[1] === 'refresh') {
|
|
2580
|
+
const { runRefresh, finalizeRefresh } = await import('./scan-refresh.js')
|
|
2581
|
+
const refreshEffectiveDir = specDir ? dir : resolveEffectiveDir(dir)
|
|
2582
|
+
const refreshSpecBase = resolvePlatformSpecDir(dir, specDir) || join(refreshEffectiveDir, '.sillyspec')
|
|
2583
|
+
const projIdxR = filteredArgs.indexOf('--project')
|
|
2584
|
+
const refreshProjectName = projIdxR >= 0 && filteredArgs[projIdxR + 1] && !String(filteredArgs[projIdxR + 1]).startsWith("--") ? filteredArgs[projIdxR + 1] : basename(refreshEffectiveDir)
|
|
2585
|
+
if (filteredArgs.includes('--done')) {
|
|
2586
|
+
const docsIdx = filteredArgs.indexOf('--docs')
|
|
2587
|
+
const docsList = docsIdx >= 0 && filteredArgs[docsIdx + 1] && !String(filteredArgs[docsIdx + 1]).startsWith("--")
|
|
2588
|
+
? filteredArgs[docsIdx + 1].split(',').map(s => s.trim()).filter(Boolean) : null
|
|
2589
|
+
const r = await finalizeRefresh({
|
|
2590
|
+
projectRoot: refreshEffectiveDir,
|
|
2591
|
+
specBase: refreshSpecBase,
|
|
2592
|
+
projectName: refreshProjectName,
|
|
2593
|
+
docs: docsList,
|
|
2594
|
+
force: filteredArgs.includes('--force'),
|
|
2595
|
+
platformOpts: platformWorkspaceId || platformRuntimeRoot ? { workspaceId: platformWorkspaceId, runtimeRoot: platformRuntimeRoot, specRoot: resolvePlatformSpecDir(dir, specDir) } : null,
|
|
2596
|
+
})
|
|
2597
|
+
process.exitCode = r.code
|
|
2598
|
+
} else {
|
|
2599
|
+
process.exitCode = await runRefresh({
|
|
2600
|
+
projectRoot: refreshEffectiveDir,
|
|
2601
|
+
specBase: refreshSpecBase,
|
|
2602
|
+
projectName: refreshProjectName,
|
|
2603
|
+
force: filteredArgs.includes('--force'),
|
|
2604
|
+
json: filteredArgs.includes('--json'),
|
|
2605
|
+
})
|
|
2606
|
+
}
|
|
2607
|
+
break
|
|
2608
|
+
}
|
|
2549
2609
|
}
|
|
2550
2610
|
case 'status':
|
|
2551
2611
|
case 'quick':
|
|
@@ -2597,14 +2657,82 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
|
|
|
2597
2657
|
console.log('按 Ctrl+C 停止服务器');
|
|
2598
2658
|
break;
|
|
2599
2659
|
}
|
|
2660
|
+
case 'wt-commit': {
|
|
2661
|
+
// 坑 wt-commit-ghost-dispatch:runWtCommit 模块自 bd1cb91 存在但 dispatch 从未接线——
|
|
2662
|
+
// execute Wave prompt 指示的 `sillyspec wt-commit` 一直报「未知命令」(2026-09-14
|
|
2663
|
+
// knowledge-loop-close execute W1 实证,ql-20260914-016-8786 根治)。参数面见模块头:
|
|
2664
|
+
// wt-commit [--change <名>] -m <信息> [--pathspec-from-file <f>] -- <path...>;
|
|
2665
|
+
// worktree 内可省 --change(从 cwd 的 worktrees/<名> 段推断)。
|
|
2666
|
+
const wtCommitRest = filteredArgs.slice(1);
|
|
2667
|
+
const wtCommitDd = wtCommitRest.indexOf('--');
|
|
2668
|
+
const wtCommitFlags = wtCommitDd === -1 ? wtCommitRest : wtCommitRest.slice(0, wtCommitDd);
|
|
2669
|
+
const wtCommitPaths = wtCommitDd === -1 ? [] : wtCommitRest.slice(wtCommitDd + 1);
|
|
2670
|
+
let wtCommitChange = null;
|
|
2671
|
+
let wtCommitMessage = null;
|
|
2672
|
+
let wtCommitPathspecFile = null;
|
|
2673
|
+
for (let i = 0; i < wtCommitFlags.length; i++) {
|
|
2674
|
+
if (wtCommitFlags[i] === '--change' && wtCommitFlags[i + 1]) wtCommitChange = wtCommitFlags[++i];
|
|
2675
|
+
else if ((wtCommitFlags[i] === '-m' || wtCommitFlags[i] === '--message') && wtCommitFlags[i + 1]) wtCommitMessage = wtCommitFlags[++i];
|
|
2676
|
+
else if (wtCommitFlags[i] === '--pathspec-from-file' && wtCommitFlags[i + 1]) wtCommitPathspecFile = wtCommitFlags[++i];
|
|
2677
|
+
}
|
|
2678
|
+
if (!wtCommitChange) {
|
|
2679
|
+
const inferred = String(dir).match(/[\\/]worktrees[\\/]([^\\/]+)/);
|
|
2680
|
+
wtCommitChange = inferred ? inferred[1] : null;
|
|
2681
|
+
}
|
|
2682
|
+
try {
|
|
2683
|
+
const { runWtCommit } = await import('./wt-commit.js');
|
|
2684
|
+
const r = await runWtCommit({
|
|
2685
|
+
changeName: wtCommitChange,
|
|
2686
|
+
message: wtCommitMessage,
|
|
2687
|
+
pathspecs: wtCommitPaths,
|
|
2688
|
+
pathspecFile: wtCommitPathspecFile,
|
|
2689
|
+
cwd: dir,
|
|
2690
|
+
});
|
|
2691
|
+
if (json) console.log(JSON.stringify({ command: 'wt-commit', ...r }, null, 2));
|
|
2692
|
+
else if (r.skipped) console.log(`⏭️ wt-commit 无变更跳过(HEAD 不动):${r.shortHead}`);
|
|
2693
|
+
else console.log(`✅ wt-commit 完成:${r.shortHead}(${r.files.length} 文件,worktree=${r.worktreePath})`);
|
|
2694
|
+
} catch (e) {
|
|
2695
|
+
console.error(`❌ wt-commit 失败:${e.message}`);
|
|
2696
|
+
process.exit(1);
|
|
2697
|
+
}
|
|
2698
|
+
break;
|
|
2699
|
+
}
|
|
2600
2700
|
case 'worktree': {
|
|
2601
2701
|
const { WorktreeManager } = await import('./worktree.js');
|
|
2602
|
-
const { ProgressManager } = await import('./progress.js');
|
|
2702
|
+
const { ProgressManager, resolveSessionIdentity } = await import('./progress.js');
|
|
2603
2703
|
const wtSubCmd = filteredArgs[1];
|
|
2604
2704
|
const wtName = filteredArgs.slice(2).find(a => !a.startsWith('-'));
|
|
2605
2705
|
const wm = new WorktreeManager({ cwd: dir });
|
|
2606
2706
|
const pm = new ProgressManager({ specDir: resolvePlatformSpecDir(dir, specDir) });
|
|
2607
2707
|
|
|
2708
|
+
// ── 所有权护栏公共件(2026-09-14-change-ownership-guards task-02 / FR-01 / D-001@v1)──
|
|
2709
|
+
// 接管类操作(apply / cleanup / assess 自动 apply)改主仓前校验 change 归属:
|
|
2710
|
+
// 他人且活跃(活跃窗内,heartbeat_minutes 缺省 15min)→ 拒绝;无主 / 窗口外 / 本会话
|
|
2711
|
+
// 自有 / --takeover → 放行,接管分支重写 owner=本会话(setChangeOwner 含 last_active
|
|
2712
|
+
// 心跳起算)。调用点必须在 withMainRepoLock 锁内(判定与执行无 TOCTOU);只读路径
|
|
2713
|
+
// (diff/list/meta/doctor/--check-only)不接。会话标识三级解析:--session flag >
|
|
2714
|
+
// env SILLYSPEC_SESSION_ID > anon@host 机器级降级(同机并行不设防是 R-01 明示局限)。
|
|
2715
|
+
const sessionFlagIdx = args.indexOf('--session');
|
|
2716
|
+
const sessionFlagValue = (sessionFlagIdx !== -1 && args[sessionFlagIdx + 1]
|
|
2717
|
+
&& !String(args[sessionFlagIdx + 1]).startsWith('--')) ? String(args[sessionFlagIdx + 1]).trim() : null;
|
|
2718
|
+
// soft=true:assess 自动路径拒绝语义(跳过自动落盘 + warning 不抛错,同 overlapSkipped 软跳);
|
|
2719
|
+
// soft=false:显式 apply/cleanup 拒绝(结构化错误,调用方 exit 非零)。
|
|
2720
|
+
function _guardChangeOwnership(changeName, { forced = false, soft = false } = {}) {
|
|
2721
|
+
const { session } = resolveSessionIdentity({ flagSession: sessionFlagValue, cwd: dir });
|
|
2722
|
+
const check = pm.assertChangeOwnership(dir, changeName, { selfSession: session, nowMs: Date.now(), forced });
|
|
2723
|
+
if (check.allowed) {
|
|
2724
|
+
if (check.action !== 'self') pm.setChangeOwner(dir, changeName, session);
|
|
2725
|
+
return { allowed: true, action: check.action };
|
|
2726
|
+
}
|
|
2727
|
+
const lines = [
|
|
2728
|
+
`变更 ${changeName} 正被其他会话持有(owner: ${check.owner},最后活跃: ${check.lastActive || '未知'}),活跃窗口 ${Math.round(check.heartbeatMs / 60000)} 分钟内拒绝接管类操作`,
|
|
2729
|
+
`确认对方已放弃且本会话应接管:加 --takeover 显式强制接管(重写 owner 并留痕);或等对方会话收尾后重试`,
|
|
2730
|
+
];
|
|
2731
|
+
if (soft) { for (const l of lines) console.warn(`⚠️ ${l}`); }
|
|
2732
|
+
else { for (const l of lines) console.error(`🚫 ${l}`); }
|
|
2733
|
+
return { allowed: false, action: check.action };
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2608
2736
|
// isolation 写入 DB 的辅助函数
|
|
2609
2737
|
function _writeIsolationToDB(cwd, changeName, info) {
|
|
2610
2738
|
if (info.blocked) {
|
|
@@ -2629,17 +2757,19 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
2629
2757
|
|
|
2630
2758
|
用法:
|
|
2631
2759
|
sillyspec worktree create <change-name> [--base <branch>] [--adopt-branch] 创建隔离 worktree(--adopt-branch:收编既有同名分支为工作分支,分支现状作 baseline)
|
|
2632
|
-
sillyspec worktree apply <change-name> [--check-only] [--base merge-base|baseline] [--merge] [--skip-overlap] [--stash-dirty] 校验并应用变更到主工作区(--stash-dirty:主仓在途改动自动 stash→apply→恢复,SHA
|
|
2633
|
-
sillyspec worktree assess <change-name> 风险审计 + 自动 apply
|
|
2760
|
+
sillyspec worktree apply <change-name> [--check-only] [--base merge-base|baseline] [--merge] [--skip-overlap] [--stash-dirty] [--force] [--takeover] 校验并应用变更到主工作区(--stash-dirty:主仓在途改动自动 stash→apply→恢复,SHA 兜底可审计;--force:越过活跃 quick 会话 guard 相交预检,overlapForced 留痕;--takeover:越过所有权护栏显式强制接管,owner 重写留痕)
|
|
2761
|
+
sillyspec worktree assess <change-name> 风险审计 + 自动 apply(自动路径受所有权护栏约束:他人活跃 change 跳过自动落盘并 warning)
|
|
2634
2762
|
sillyspec worktree diff <change-name> [--base <commit>] 查看 worktree 相对 base 的变更
|
|
2635
2763
|
sillyspec worktree list 列出所有活跃 worktree
|
|
2636
2764
|
sillyspec worktree meta <change-name> 读取 worktree meta.json
|
|
2637
|
-
sillyspec worktree cleanup <change-name> [--force]
|
|
2765
|
+
sillyspec worktree cleanup <change-name> [--force] [--takeover] 强制清理 worktree(受所有权护栏约束:他人活跃 change 拒绝,--takeover 显式接管)
|
|
2638
2766
|
sillyspec worktree doctor [--fix] [--stale-hours N] [--change <name>] 健康检查 + 修复(--change 仅扫指定 change)
|
|
2639
2767
|
|
|
2640
2768
|
选项:
|
|
2641
2769
|
--base <branch> create: 指定基础分支(默认当前 HEAD)
|
|
2642
2770
|
--check-only apply: 只输出检查结果,不实际 apply
|
|
2771
|
+
--takeover apply/cleanup: 所有权护栏拒绝时的显式强制接管口(owner 他人且活跃时解锁,重写 owner 留痕)
|
|
2772
|
+
--session <id> 显式会话标识(所有权三级解析最高优先级;缺省 env SILLYSPEC_SESSION_ID,再缺省 anon@<host> 机器级降级——同机并行不设防)
|
|
2643
2773
|
`);
|
|
2644
2774
|
break;
|
|
2645
2775
|
}
|
|
@@ -2677,13 +2807,15 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
2677
2807
|
}
|
|
2678
2808
|
case 'apply': {
|
|
2679
2809
|
if (!wtName) {
|
|
2680
|
-
console.error('❌ 用法: sillyspec worktree apply <change-name> [--check-only] [--base merge-base|baseline] [--merge] [--skip-overlap] [--stash-dirty]');
|
|
2810
|
+
console.error('❌ 用法: sillyspec worktree apply <change-name> [--check-only] [--base merge-base|baseline] [--merge] [--skip-overlap] [--stash-dirty] [--force] [--takeover]');
|
|
2681
2811
|
process.exit(1);
|
|
2682
2812
|
}
|
|
2683
2813
|
const checkOnly = args.includes('--check-only');
|
|
2684
2814
|
const merge = args.includes('--merge');
|
|
2685
2815
|
const skipOverlap = args.includes('--skip-overlap');
|
|
2686
2816
|
const stashDirty = args.includes('--stash-dirty');
|
|
2817
|
+
const force = args.includes('--force'); // guard 相交预检显式解锁(task-02 / FR-03;人工 flag,自动路径永不带)
|
|
2818
|
+
const takeover = args.includes('--takeover'); // 所有权护栏显式强制接管(2026-09-14-change-ownership-guards task-02 / FR-01;人工 flag,自动路径永不带)
|
|
2687
2819
|
|
|
2688
2820
|
// 解析 --base 参数(默认 merge-base)
|
|
2689
2821
|
let base = 'merge-base';
|
|
@@ -2711,15 +2843,22 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
2711
2843
|
}
|
|
2712
2844
|
// 主仓 apply 互斥锁(坑 main-apply-no-mutex):真 apply(非 checkOnly)在锁内跑——
|
|
2713
2845
|
// apply 链(rollback/merge/cleanup)直接改主仓工作区,两会话并发互踩会互相清文件。
|
|
2846
|
+
// 所有权护栏(2026-09-14-change-ownership-guards task-02 / FR-01):锁内、实际 apply 前
|
|
2847
|
+
// 判归属——他人且活跃(活跃窗内)拒绝;--check-only 是只读路径不校验(铁律:只读命令不接)。
|
|
2848
|
+
// 拒绝时结构化错误已在护栏内打印,经哨兵返回值带出锁外统一 exit 1(不进结果展示分支)。
|
|
2714
2849
|
let result;
|
|
2715
2850
|
try {
|
|
2716
2851
|
result = checkOnly
|
|
2717
|
-
? applyWorktree(wtName, { cwd: dir, checkOnly, merge, base, ctx: _applyCtx, skipOverlap, stashDirty })
|
|
2718
|
-
: await withMainRepoLock(dir, wtName, 'apply', () =>
|
|
2852
|
+
? applyWorktree(wtName, { cwd: dir, checkOnly, merge, base, ctx: _applyCtx, skipOverlap, stashDirty, force })
|
|
2853
|
+
: await withMainRepoLock(dir, wtName, 'apply', () => {
|
|
2854
|
+
if (!_guardChangeOwnership(wtName, { forced: takeover }).allowed) return { __ownershipDenied: true };
|
|
2855
|
+
return applyWorktree(wtName, { cwd: dir, checkOnly, merge, base, ctx: _applyCtx, skipOverlap, stashDirty, force });
|
|
2856
|
+
});
|
|
2719
2857
|
} catch (lockErr) {
|
|
2720
2858
|
console.error(`❌ ${lockErr.message}`);
|
|
2721
2859
|
process.exit(1);
|
|
2722
2860
|
}
|
|
2861
|
+
if (result && result.__ownershipDenied) process.exit(1);
|
|
2723
2862
|
|
|
2724
2863
|
if (result.errors.length > 0) {
|
|
2725
2864
|
console.error(`❌ 校验失败:`);
|
|
@@ -2855,15 +2994,29 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
2855
2994
|
if (assessment.decision === 'SAFE' || assessment.decision === 'WARNING') {
|
|
2856
2995
|
console.log('Action: auto-applying...');
|
|
2857
2996
|
const { applyWorktree, withMainRepoLock } = await import('./worktree-apply.js');
|
|
2858
|
-
// 主仓互斥锁(与手动 apply 同款):自动 apply
|
|
2997
|
+
// 主仓互斥锁(与手动 apply 同款):自动 apply 同样改主仓工作区,必须互斥。
|
|
2998
|
+
// 所有权护栏(2026-09-14-change-ownership-guards task-02 / FR-01):assess 自动 apply
|
|
2999
|
+
// 是护栏①最可能的旁路(SAFE/WARNING 即自动改主仓+链内 cleanup)——锁内、实际动作前
|
|
3000
|
+
// 判归属。自动路径永不带 --takeover;他人活跃 change 拒绝 = 跳过自动落盘 + warning
|
|
3001
|
+
// 不抛错(同 overlapSkipped 软跳语义:无人值守不越权也不阻断审计流)。
|
|
2859
3002
|
let applyResult;
|
|
2860
3003
|
try {
|
|
2861
|
-
applyResult = await withMainRepoLock(dir, wtName, 'assess-auto-apply', () =>
|
|
3004
|
+
applyResult = await withMainRepoLock(dir, wtName, 'assess-auto-apply', () => {
|
|
3005
|
+
if (!_guardChangeOwnership(wtName, { soft: true }).allowed) return { ownershipSkipped: true };
|
|
3006
|
+
return applyWorktree(wtName, { cwd: dir, ctx: _assessCtx, autoApply: true });
|
|
3007
|
+
});
|
|
2862
3008
|
} catch (lockErr) {
|
|
2863
3009
|
console.error(`❌ 自动 apply 未执行:${lockErr.message}`);
|
|
2864
3010
|
break;
|
|
2865
3011
|
}
|
|
2866
|
-
if (applyResult.
|
|
3012
|
+
if (applyResult.ownershipSkipped) {
|
|
3013
|
+
// 所有权拒绝(结构化 warning 已在护栏内打印):跳过自动落盘,指引人工显式接管
|
|
3014
|
+
console.log(' → 确认对方会话已放弃:人工评估后显式接管 sillyspec worktree apply ' + wtName + ' --takeover(或等对方收尾后重跑 assess)');
|
|
3015
|
+
} else if (applyResult.overlapSkipped) {
|
|
3016
|
+
// guard 相交软跳过(task-02 / FR-03):无人值守不越权也不阻断审计流——不落盘,亮 warning 指引人工评估
|
|
3017
|
+
for (const w of applyResult.warnings || []) console.log(`⚠️ ${w}`);
|
|
3018
|
+
console.log(' → 人工评估后显式 apply: sillyspec worktree apply ' + wtName + '(确认与活跃 quick 会话无冲突;或等对方 --done 后重跑 assess)');
|
|
3019
|
+
} else if (applyResult.errors.length > 0) {
|
|
2867
3020
|
console.error('❌ apply 失败:', applyResult.errors.join('; '));
|
|
2868
3021
|
} else {
|
|
2869
3022
|
console.log(`✅ 已自动应用 ${applyResult.changedFiles.length} 个文件变更:`);
|
|
@@ -2956,15 +3109,23 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
2956
3109
|
}
|
|
2957
3110
|
case 'cleanup': {
|
|
2958
3111
|
if (!wtName) {
|
|
2959
|
-
console.error('❌ 用法: sillyspec worktree cleanup <change-name>');
|
|
3112
|
+
console.error('❌ 用法: sillyspec worktree cleanup <change-name> [--force] [--takeover]');
|
|
2960
3113
|
process.exit(1);
|
|
2961
3114
|
}
|
|
2962
3115
|
const forceFlag = args.includes('--force');
|
|
3116
|
+
const takeover = args.includes('--takeover'); // 所有权护栏显式强制接管(task-02 / FR-01)
|
|
2963
3117
|
try {
|
|
2964
3118
|
// 主仓互斥锁(坑 main-repo-no-mutex 二批):cleanup 删 worktree 注册表/分支/目录,
|
|
2965
|
-
// 与并行会话的 apply/cleanup 互踩会互相清——与其他写主仓操作共用一把 main-repo.lock
|
|
3119
|
+
// 与并行会话的 apply/cleanup 互踩会互相清——与其他写主仓操作共用一把 main-repo.lock。
|
|
3120
|
+
// 所有权护栏(2026-09-14-change-ownership-guards task-02 / FR-01):锁内、实际清理前
|
|
3121
|
+
// 判归属——他人且活跃拒绝(结构化错误在护栏内打印,哨兵带出统一 exit 1,跨仓清理
|
|
3122
|
+
// 同步跳过);--takeover 显式强制接管放行。
|
|
2966
3123
|
const { withMainRepoLock } = await import('./worktree-apply.js');
|
|
2967
|
-
const result = await withMainRepoLock(dir, wtName, 'worktree-cleanup', () =>
|
|
3124
|
+
const result = await withMainRepoLock(dir, wtName, 'worktree-cleanup', () => {
|
|
3125
|
+
if (!_guardChangeOwnership(wtName, { forced: takeover }).allowed) return { __ownershipDenied: true };
|
|
3126
|
+
return wm.cleanup(wtName, { force: forceFlag });
|
|
3127
|
+
});
|
|
3128
|
+
if (result && result.__ownershipDenied) process.exit(1);
|
|
2968
3129
|
// 跨仓 worktree(坑 cross-repo-no-worktree-isolation):与主仓同锁清理——跨仓 worktree
|
|
2969
3130
|
// 注册在各跨仓仓 .git 内,但目录/meta 在主仓 .sillyspec 运行时区,同样怕并发互踩
|
|
2970
3131
|
const { cleanupCrossWorktrees } = await import('./worktree-cross.js');
|
|
@@ -2986,7 +3147,16 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
2986
3147
|
}
|
|
2987
3148
|
} else if (result.result === 'skipped') {
|
|
2988
3149
|
console.log(`⏭️ worktree 跳过清理: ${wtName} (mode: ${result.mode})`);
|
|
2989
|
-
|
|
3150
|
+
// ql-20260915-003 修复②(坑 cleanup-meta-fallback,2026-09-15 实证「mode: null
|
|
3151
|
+
// 跳过清理」但 worktree 实际创建过):早退跳过(meta/目录/git 注册三路探针全未
|
|
3152
|
+
// 命中)时,误导性的「in-place 模式」文案升级为诊断式——列三路探针路径 + meta
|
|
3153
|
+
// 丢失可能原因 + doctor 指引。probePaths 由 cleanup 兜底探测带回。
|
|
3154
|
+
if (Array.isArray(result.probePaths) && result.probePaths.length > 0) {
|
|
3155
|
+
for (const p of result.probePaths) console.log(` 探针: ${p}`);
|
|
3156
|
+
console.log(` meta 丢失可能原因:apply/cleanup 后随目录删除、并行会话已清理、cwd/specDir 漂移。可用 sillyspec worktree doctor 检查 git 注册与分支残留。`);
|
|
3157
|
+
} else {
|
|
3158
|
+
console.log(` 原因: in-place 模式没有隔离目录需要清理`);
|
|
3159
|
+
}
|
|
2990
3160
|
} else if (result.result === 'blocked') {
|
|
2991
3161
|
console.error(`🚫 拒绝清理:有未落主仓交付变更,请先 sillyspec worktree apply ${wtName} 或 --force`);
|
|
2992
3162
|
} else if (result.result === 'partial') {
|
|
@@ -3250,7 +3420,8 @@ SillySpec platform — SillyHub 平台同步
|
|
|
3250
3420
|
case 'sync': {
|
|
3251
3421
|
const syncChangeIdx = args.indexOf('--change');
|
|
3252
3422
|
const syncChangeName = syncChangeIdx >= 0 && args[syncChangeIdx + 1] && !String(args[syncChangeIdx + 1]).startsWith("--") ? args[syncChangeIdx + 1] : null;
|
|
3253
|
-
|
|
3423
|
+
// manual:用户显式同步,change_deleted 回执噪音闸旁路(显式动作的回执必须可见)
|
|
3424
|
+
await syncModule.sync(syncChangeName, dir, { manual: true });
|
|
3254
3425
|
break;
|
|
3255
3426
|
}
|
|
3256
3427
|
case 'sync-docs': {
|
|
@@ -3918,7 +4089,14 @@ SillySpec modules — 模块文档管理
|
|
|
3918
4089
|
process.exit(1);
|
|
3919
4090
|
}
|
|
3920
4091
|
const { resolveLocalYamlWriteTarget } = await import('./config-cat.js');
|
|
3921
|
-
|
|
4092
|
+
// 坑 register-repo-specbase-split-brain(2026-09-15 wp 平台模式会话实证):specBase
|
|
4093
|
+
// 优先的写候选链在平台模式下命中平台 spec 根 local.yaml,而 repos: 段的全部读侧
|
|
4094
|
+
// (execute MultiRepoContext / worktree-cross / run shared)恒读 <cwd>/.sillyspec/local.yaml
|
|
4095
|
+
// ——注册写进 spec 根 = 死配置,execute 启动 fail-closed 报未注册,agent 只能试出
|
|
4096
|
+
// --spec-dir 指向项目 .sillyspec 才能绕过。修复:默认(无显式 --spec-dir)写目标走
|
|
4097
|
+
// 读侧对齐链(不喂 specBase,cwd 祖先链命中项目 local.yaml,worktree 自动反推主仓);
|
|
4098
|
+
// 显式 --spec-dir 是用户明示选择,仍尊重。
|
|
4099
|
+
const target = resolveLocalYamlWriteTarget(dir, specDir ? { specBase: rrSpecBase } : {});
|
|
3922
4100
|
const { registerRepoInLocalYaml } = await import('./local-register.js');
|
|
3923
4101
|
let rrResult;
|
|
3924
4102
|
try {
|
|
@@ -3930,6 +4108,16 @@ SillySpec modules — 模块文档管理
|
|
|
3930
4108
|
const verb = rrResult.replaced ? '已更新(覆盖旧路径)' : '已注册';
|
|
3931
4109
|
console.log(`✅ repos.${rrKey} ${verb}: ${absRepo.replace(/\\/g, '/')}`);
|
|
3932
4110
|
console.log(` 写入: ${target.path}${rrResult.fileCreated ? '(新建文件)' : ''}${rrResult.sectionCreated ? '(新建 repos: 段)' : ''}`);
|
|
4111
|
+
// 同坑提醒:平台 spec 根也躺着一份 local.yaml 时,其 repos: 段不生效(读侧只读项目侧)
|
|
4112
|
+
// ——不Dual写(两份各自演化=再分裂),只提示用户那份是死配置可清理
|
|
4113
|
+
if (!specDir && rrSpecBase) {
|
|
4114
|
+
const altYaml = join(rrSpecBase, 'local.yaml');
|
|
4115
|
+
try {
|
|
4116
|
+
if (existsSync(altYaml) && resolve(altYaml) !== resolve(target.path)) {
|
|
4117
|
+
console.warn(`⚠️ 平台 spec 根另有一份 local.yaml(${altYaml})——repos: 注册只在项目 local.yaml 生效(execute 等读侧只读项目侧),spec 根那份的 repos: 段是死配置`);
|
|
4118
|
+
}
|
|
4119
|
+
} catch { /* 提醒失败不影响注册结果 */ }
|
|
4120
|
+
}
|
|
3933
4121
|
console.log(' 查看: sillyspec config cat(真实配置); main 隐式=主仓不用注册');
|
|
3934
4122
|
break;
|
|
3935
4123
|
}
|