sillyspec 3.26.1 → 3.26.3
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-execute/SKILL.md +38 -0
- package/.claude/skills/sillyspec-plan/SKILL.md +48 -0
- package/package.json +1 -1
- package/src/classify-change.js +41 -5
- package/src/config-schema.js +287 -0
- package/src/db.js +1 -1
- package/src/hooks/worktree-guard.js +3 -2
- package/src/index.js +96 -7
- package/src/init.js +13 -0
- package/src/machine-interface.js +11 -4
- package/src/run/command.js +4 -2
- package/src/run/complete.js +27 -4
- package/src/run/gates.js +26 -14
- package/src/run/multi-repo-context.js +206 -0
- package/src/run/prompt.js +15 -3
- package/src/run/quick-audit.js +12 -3
- package/src/run/shared.js +179 -6
- package/src/stage-review.js +1 -0
- package/src/stages/execute.js +185 -7
- package/src/stages/plan-postcheck.js +266 -11
- package/src/stages/plan.js +5 -0
- package/src/sync.js +125 -40
- package/src/task-review.js +135 -25
- package/src/verify-postcheck.js +242 -11
- package/src/worktree-apply.js +137 -9
- package/.sillyspec-platform-cleaned +0 -1
- package/.sillyspec-platform.json +0 -7
package/src/index.js
CHANGED
|
@@ -75,6 +75,7 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
75
75
|
doctor [--fix] [--stale-hours N] 健康检查 + 修复
|
|
76
76
|
|
|
77
77
|
sillyspec local detect [--dir <path>] 生成本地配置 local.yaml(纯 fs 嗅探,零 token、不跑 scan)
|
|
78
|
+
sillyspec config [schema] [--json] 打印 local.yaml 全部已知键 + 生效状态 + 读取点(堵外部 agent 配置发现缺口)
|
|
78
79
|
sillyspec runtime list [--json] 枚举 .sillyspec/.runtime/ 运行时产物(只读,看手上有哪些证据/状态文件)
|
|
79
80
|
sillyspec dispatch <probe | hint> SillyHub 派发能力探测 + 策略生成(agent 调用桥,仅渲染不执行 tool)
|
|
80
81
|
|
|
@@ -368,6 +369,16 @@ async function main() {
|
|
|
368
369
|
const wt = detectWorktreeSpecDrift(resolveSpecDir(dir));
|
|
369
370
|
if (wt) gateOpts.specDriftAnchor = wt.mainSpecBase;
|
|
370
371
|
}
|
|
372
|
+
// W3 task-09:gate 是只读查询命令,best-effort 构造 ctx(D-013)。失败降级 null(不阻断只读命令)——
|
|
373
|
+
// ctx 让 validateTaskReviews/runVerifyTestCheck 正确切跨仓 gitDir;配置错时单仓降级会让两函数
|
|
374
|
+
// 自然报「跨仓 commit 找不到」等错误进入 check.errors,gate 结论仍如实反映问题(D-002@v1 只读契约不破)。
|
|
375
|
+
try {
|
|
376
|
+
const { getOrCreateMultiRepoContext } = await import('./run/shared.js');
|
|
377
|
+
const _ctx = await getOrCreateMultiRepoContext({ cwd: dir, changeName: gateChange, platformOpts: specDir ? { specRoot: specDir } : {} });
|
|
378
|
+
if (_ctx) gateOpts.ctx = _ctx;
|
|
379
|
+
} catch (e) {
|
|
380
|
+
console.warn(`⚠️ gate ctx 构造失败,降级单仓核验(${e.message})`);
|
|
381
|
+
}
|
|
371
382
|
const { envelope, exitCode } = await withJsonOutput(json, () => runGate(gateStage, gateChange, gateOpts));
|
|
372
383
|
if (json) {
|
|
373
384
|
process.stdout.write(JSON.stringify(envelope));
|
|
@@ -409,6 +420,14 @@ async function main() {
|
|
|
409
420
|
const wt = detectWorktreeSpecDrift(resolveSpecDir(dir));
|
|
410
421
|
if (wt) deriveOpts.specDriftAnchor = wt.mainSpecBase;
|
|
411
422
|
}
|
|
423
|
+
// W3 task-09:derive 同 gate,best-effort 构造 ctx 透传给 verify-test/task-reviews facet(D-013)。
|
|
424
|
+
try {
|
|
425
|
+
const { getOrCreateMultiRepoContext } = await import('./run/shared.js');
|
|
426
|
+
const _ctx = await getOrCreateMultiRepoContext({ cwd: dir, changeName: deriveChange, platformOpts: specDir ? { specRoot: specDir } : {} });
|
|
427
|
+
if (_ctx) deriveOpts.ctx = _ctx;
|
|
428
|
+
} catch (e) {
|
|
429
|
+
console.warn(`⚠️ derive ctx 构造失败,降级单仓核验(${e.message})`);
|
|
430
|
+
}
|
|
412
431
|
const { envelope, exitCode } = await withJsonOutput(json, () => runDerive(facet, deriveChange, deriveOpts));
|
|
413
432
|
if (json) {
|
|
414
433
|
process.stdout.write(JSON.stringify(envelope));
|
|
@@ -439,7 +458,16 @@ async function main() {
|
|
|
439
458
|
// --spec-dir 透传 platformOpts.specRoot(与 gate/derive 对称);不传走默认 join(cwd,'.sillyspec')。
|
|
440
459
|
const brPlatformOpts = {};
|
|
441
460
|
if (specDir) brPlatformOpts.specRoot = specDir;
|
|
442
|
-
|
|
461
|
+
// W3 task-09:best-effort 构造 ctx 透传(D-013),让 task-04 generateTaskReviewDrafts 跨仓 task
|
|
462
|
+
// 用跨仓 gitDir 取 base..head diff。失败降级 null(与 gate/derive 同语义,辅助修复命令不阻断)。
|
|
463
|
+
let _brCtx = null;
|
|
464
|
+
try {
|
|
465
|
+
const { getOrCreateMultiRepoContext } = await import('./run/shared.js');
|
|
466
|
+
_brCtx = await getOrCreateMultiRepoContext({ cwd: dir, changeName: brChange, platformOpts: brPlatformOpts });
|
|
467
|
+
} catch (e) {
|
|
468
|
+
console.warn(`⚠️ backfill-reviews ctx 构造失败,降级单仓草稿(${e.message})`);
|
|
469
|
+
}
|
|
470
|
+
const result = await generateTaskReviewDrafts({ changeName: brChange, cwd: dir, platformOpts: brPlatformOpts, ctx: _brCtx });
|
|
443
471
|
if (json) {
|
|
444
472
|
process.stdout.write(JSON.stringify({ ok: true, command: 'backfill-reviews', change: brChange, ...result }));
|
|
445
473
|
} else {
|
|
@@ -768,7 +796,18 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
768
796
|
const checkOnly = args.includes('--check-only');
|
|
769
797
|
const merge = args.includes('--merge');
|
|
770
798
|
const { applyWorktree } = await import('./worktree-apply.js');
|
|
771
|
-
|
|
799
|
+
// W3 task-09:apply 链路构造 ctx(D-013),让 task-05 applyWorktree 按 ctx 区分主仓 A5 / 跨仓 no-op。
|
|
800
|
+
// 跨仓 apply=no-op(G1 D-009):校验 review.head 是跨仓真实 commit + 跳过 wm.cleanup。
|
|
801
|
+
// ctx 构造 fail-closed(约束②)——跨仓配置错时 apply 必须阻断(走错仓=数据所有权事故),不降级。
|
|
802
|
+
let _applyCtx = null;
|
|
803
|
+
try {
|
|
804
|
+
const { getOrCreateMultiRepoContext } = await import('./run/shared.js');
|
|
805
|
+
_applyCtx = await getOrCreateMultiRepoContext({ cwd: dir, changeName: wtName, platformOpts: specDir ? { specRoot: specDir } : {} });
|
|
806
|
+
} catch (e) {
|
|
807
|
+
console.error(`❌ apply 失败:跨仓 MultiRepoContext 构造失败(${e.message})`);
|
|
808
|
+
process.exit(1);
|
|
809
|
+
}
|
|
810
|
+
const result = applyWorktree(wtName, { cwd: dir, checkOnly, merge, ctx: _applyCtx });
|
|
772
811
|
|
|
773
812
|
if (result.errors.length > 0) {
|
|
774
813
|
console.error(`❌ 校验失败:`);
|
|
@@ -821,6 +860,20 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
821
860
|
const { assessApplyRisk } = await import('./worktree-apply.js');
|
|
822
861
|
const assessment = assessApplyRisk(wtName, { cwd: dir });
|
|
823
862
|
|
|
863
|
+
// W3 task-09:assess 自动 apply 路径构造 ctx(D-013),SAFE/WARNING 时透传给 applyWorktree。
|
|
864
|
+
// 构造 fail-closed(跨仓配置错阻断,与显式 apply case 同语义)。
|
|
865
|
+
let _assessCtx = null;
|
|
866
|
+
try {
|
|
867
|
+
const { getOrCreateMultiRepoContext } = await import('./run/shared.js');
|
|
868
|
+
_assessCtx = await getOrCreateMultiRepoContext({ cwd: dir, changeName: wtName, platformOpts: specDir ? { specRoot: specDir } : {} });
|
|
869
|
+
} catch (e) {
|
|
870
|
+
if (assessment.decision === 'SAFE' || assessment.decision === 'WARNING') {
|
|
871
|
+
console.error(`❌ assess 自动 apply 失败:跨仓 MultiRepoContext 构造失败(${e.message})`);
|
|
872
|
+
process.exit(1);
|
|
873
|
+
}
|
|
874
|
+
// blocked 决策不触发 apply,ctx 失败仅记 warn(不阻断只读 assess 展示)
|
|
875
|
+
console.warn(`⚠️ ctx 构造失败(${e.message})`);
|
|
876
|
+
}
|
|
824
877
|
const SEPARATOR = '─'.repeat(32);
|
|
825
878
|
console.log('Worktree Apply Decision');
|
|
826
879
|
console.log(SEPARATOR);
|
|
@@ -852,7 +905,7 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
852
905
|
if (assessment.decision === 'SAFE' || assessment.decision === 'WARNING') {
|
|
853
906
|
console.log('Action: auto-applying...');
|
|
854
907
|
const { applyWorktree } = await import('./worktree-apply.js');
|
|
855
|
-
const applyResult = applyWorktree(wtName, { cwd: dir });
|
|
908
|
+
const applyResult = applyWorktree(wtName, { cwd: dir, ctx: _assessCtx });
|
|
856
909
|
if (applyResult.errors.length > 0) {
|
|
857
910
|
console.error('❌ apply 失败:', applyResult.errors.join('; '));
|
|
858
911
|
} else {
|
|
@@ -1543,10 +1596,10 @@ SillySpec modules — 模块文档管理
|
|
|
1543
1596
|
lines.push('# 测试策略:full=全量测试, module=只测变更模块, skip=跳过测试');
|
|
1544
1597
|
lines.push('test_strategy: module');
|
|
1545
1598
|
lines.push('');
|
|
1546
|
-
lines.push('#
|
|
1547
|
-
lines.push('#
|
|
1548
|
-
lines.push('# user-service: "user/"');
|
|
1549
|
-
lines.push('# order-service: "order/"');
|
|
1599
|
+
lines.push('# monorepo 子模块映射(test_strategy: module 时按 git diff 命中模块收窄测试;inline flow 形态)');
|
|
1600
|
+
lines.push('# modules:');
|
|
1601
|
+
lines.push('# user-service: { path: "user/", test: "cd user && npm test" }');
|
|
1602
|
+
lines.push('# order-service: { path: "order/", test: "cd order && npm test" }');
|
|
1550
1603
|
lines.push('# 已知预存失败(可选):verify 实测时这些失败被豁免、不阻断归档;fail-safe 仅豁免能检测到的失败行');
|
|
1551
1604
|
lines.push('# known_failures:');
|
|
1552
1605
|
lines.push('# - "tests/test_legacy.py::test_old"');
|
|
@@ -1560,6 +1613,42 @@ SillySpec modules — 模块文档管理
|
|
|
1560
1613
|
console.log(` 路径: ${localYamlPath}`);
|
|
1561
1614
|
break;
|
|
1562
1615
|
}
|
|
1616
|
+
case 'config': {
|
|
1617
|
+
// local.yaml 键发现缺口(2026-08-11):键散落 ~10 个 reader,外部项目 agent 无从得知。
|
|
1618
|
+
// config schema 把全部已知键 + 生效状态 + 读取点打印出来,堵住发现缺口。
|
|
1619
|
+
// 数据源唯一 = src/config-schema.js(与 init 落盘的 local.yaml.example 同源)。
|
|
1620
|
+
const configSub = filteredArgs[1];
|
|
1621
|
+
const wantHelp = configSub === 'help' || configSub === '--help' || configSub === '-h';
|
|
1622
|
+
const isSchema = !configSub || configSub === 'schema';
|
|
1623
|
+
if (wantHelp) {
|
|
1624
|
+
console.log(`
|
|
1625
|
+
SillySpec config — local.yaml 配置键速查
|
|
1626
|
+
|
|
1627
|
+
用法:
|
|
1628
|
+
sillyspec config 打印 local.yaml 全部已知键(人类可读树,默认)
|
|
1629
|
+
sillyspec config schema 同上(显式子命令)
|
|
1630
|
+
sillyspec config --json 机读 JSON(程序化消费)
|
|
1631
|
+
sillyspec config schema --json 同上
|
|
1632
|
+
|
|
1633
|
+
说明:
|
|
1634
|
+
- 数据源:src/config-schema.js(唯一真相;reader 见各键「读取点」)
|
|
1635
|
+
- 键分两类:【生效】配了即生效;【声明但未接线】代码/JSDoc 提及但无 reader,配了不生效
|
|
1636
|
+
- 脱敏示例文件:sillyspec init 生成 local.yaml.example(可提交;真实 local.yaml 是 gitignored)
|
|
1637
|
+
`);
|
|
1638
|
+
break;
|
|
1639
|
+
}
|
|
1640
|
+
if (!isSchema) {
|
|
1641
|
+
const sug = didYouMean(configSub, ['schema']);
|
|
1642
|
+
console.error(`❌ 未知子命令: config ${configSub}`);
|
|
1643
|
+
if (sug) console.error(` 你是想输入「config ${sug}」吗?`);
|
|
1644
|
+
console.error('用法: sillyspec config [schema] [--json] 打印 local.yaml 配置键');
|
|
1645
|
+
process.exit(2);
|
|
1646
|
+
}
|
|
1647
|
+
const { renderSchemaHuman, renderSchemaJson } = await import('./config-schema.js');
|
|
1648
|
+
if (json) console.log(renderSchemaJson());
|
|
1649
|
+
else console.log(renderSchemaHuman());
|
|
1650
|
+
break;
|
|
1651
|
+
}
|
|
1563
1652
|
case 'runtime': {
|
|
1564
1653
|
// D2:.runtime/ 产物索引。CLI 往 .runtime/ 写各种证据/状态文件(db、doctor-diagnosis、
|
|
1565
1654
|
// workflow-runs、user-inputs…),却没有命令让 agent 一眼看到「我手上有哪些产物」。
|
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 { renderExample } from './config-schema.js';
|
|
8
9
|
// 向后兼容:getVersion 已抽到轻量 version.js(避免 index.js 为 --version 静态加载 init.js 的 inquirer 税),
|
|
9
10
|
// 此处 re-export 保持 init.js 既有导出 API 不破坏(如 test/init-claude-injection.test.mjs)。
|
|
10
11
|
export { getVersion };
|
|
@@ -313,6 +314,18 @@ function doInstall(projectDir, tools, subprojects = [], specDir = null) {
|
|
|
313
314
|
const pm = new ProgressManager({ specDir: spec });
|
|
314
315
|
pm.init(projectDir);
|
|
315
316
|
|
|
317
|
+
// 落盘脱敏 local.yaml.example(config-schema.js 单一数据源;不存在才写,仿 local detect skip-if-exists)
|
|
318
|
+
// 真实 local.yaml 是 gitignored 含凭据;example 可提交,是给人/外部 agent 看的配置发现物。
|
|
319
|
+
const examplePath = join(spec, 'local.yaml.example');
|
|
320
|
+
if (!existsSync(examplePath)) {
|
|
321
|
+
try {
|
|
322
|
+
writeFileSync(examplePath, renderExample());
|
|
323
|
+
console.log(chalk.green(' ✓ local.yaml.example 已生成(脱敏配置示例,可提交;真实 local.yaml 由 sillyspec local detect / platform connect 写入)'));
|
|
324
|
+
} catch (e) {
|
|
325
|
+
console.warn(chalk.yellow(` ⚠ 生成 local.yaml.example 失败: ${e.message}`));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
316
329
|
// .gitignore 只在 specDir 在项目内时才修改
|
|
317
330
|
const isExternalSpec = specDir && resolve(spec) !== resolve(projectDir, '.sillyspec');
|
|
318
331
|
if (!isExternalSpec) {
|
package/src/machine-interface.js
CHANGED
|
@@ -106,9 +106,12 @@ export function buildEnvelope({
|
|
|
106
106
|
* @param {string} [opts.specDriftAnchor] - drift 场景主仓 specBase 锚点(坑 execute-runs-isolation)。
|
|
107
107
|
* drift 守卫命中时由调用方传入,task-reviews 的 execute-run-id marker 落点经
|
|
108
108
|
* resolveRuntimeRoot 锚主仓 .runtime,不读 worktree 副本(与 contract-matrix 调用方同语义)。
|
|
109
|
+
* @param {object} [opts.ctx] - MultiRepoContext 单例(W3 task-09,D-013)。缺省=null 退化单仓。
|
|
110
|
+
* 透传给 validateTaskReviews(execute task-reviews check)+ runVerifyTestCheck(verify-test check),
|
|
111
|
+
* 让两函数按 ctx 切跨仓 gitDir / per-repo cwd。null 时两函数按既有单仓行为零回归。
|
|
109
112
|
* @returns {Promise<{ envelope: object, exitCode: number }>}
|
|
110
113
|
*/
|
|
111
|
-
export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, specDriftAnchor } = {}) {
|
|
114
|
+
export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, specDriftAnchor, ctx = null } = {}) {
|
|
112
115
|
const specRoot = specBase || resolveSpecDir(cwd);
|
|
113
116
|
const pm = new ProgressManager();
|
|
114
117
|
|
|
@@ -232,6 +235,7 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
|
|
|
232
235
|
executeRunId,
|
|
233
236
|
changeDir: planDir,
|
|
234
237
|
gitDir,
|
|
238
|
+
ctx,
|
|
235
239
|
});
|
|
236
240
|
checks.push({
|
|
237
241
|
id: 'task-reviews',
|
|
@@ -243,7 +247,7 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
|
|
|
243
247
|
|
|
244
248
|
// ── d. verify 阶段追加 verify-test(CLI 实测 local.yaml commands.test)──
|
|
245
249
|
if (stage === 'verify') {
|
|
246
|
-
const vt = runVerifyTestCheck({ cwd, specBase: specRoot, changeName });
|
|
250
|
+
const vt = runVerifyTestCheck({ cwd, specBase: specRoot, changeName, ctx });
|
|
247
251
|
const vtWarnings = [];
|
|
248
252
|
if (vt.status === 'skipped') {
|
|
249
253
|
vtWarnings.push(`⚠️ verify-test SKIPPED — gate 未核验测试(${vt.reason || 'local.yaml 未配置 commands.test 或显式无测试'})。本次 gate 结论不含测试客观核验,driver 不应据 exit 0 判定测试通过;integration-critical 变更应在 verify 阶段降级 FAIL`);
|
|
@@ -334,9 +338,11 @@ export const FACETS = ['execute-evidence', 'verify-test', 'task-reviews', 'artif
|
|
|
334
338
|
* @param {string} [opts.runtimeRoot] - .runtime 目录;默认 resolveRuntimeRoot 三级优先级解析
|
|
335
339
|
* @param {string} [opts.specDriftAnchor] - drift 场景主仓 specBase 锚点(坑 execute-runs-isolation),
|
|
336
340
|
* task-reviews facet 的 execute-run-id marker 落点经 resolveRuntimeRoot 锚主仓 .runtime,不读副本。
|
|
341
|
+
* @param {object} [opts.ctx] - MultiRepoContext 单例(W3 task-09,D-013)。缺省=null 退化单仓。
|
|
342
|
+
* 透传给 verify-test / task-reviews facet(runVerifyTestCheck + validateTaskReviews)。
|
|
337
343
|
* @returns {Promise<{ envelope: object, exitCode: number }>}
|
|
338
344
|
*/
|
|
339
|
-
export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot, specDriftAnchor } = {}) {
|
|
345
|
+
export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot, specDriftAnchor, ctx = null } = {}) {
|
|
340
346
|
// ── 非法 facet:用法错 → exit 2(D-004@v1)──
|
|
341
347
|
if (!FACETS.includes(facet)) {
|
|
342
348
|
const envelope = buildEnvelope({
|
|
@@ -402,7 +408,7 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot,
|
|
|
402
408
|
|
|
403
409
|
// ── b. verify-test:真实执行测试命令并取证 ──
|
|
404
410
|
case 'verify-test': {
|
|
405
|
-
const vt = runVerifyTestCheck({ cwd, specBase: specRoot, changeName });
|
|
411
|
+
const vt = runVerifyTestCheck({ cwd, specBase: specRoot, changeName, ctx });
|
|
406
412
|
data = {
|
|
407
413
|
status: vt.status,
|
|
408
414
|
exitCode: vt.exitCode,
|
|
@@ -464,6 +470,7 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot,
|
|
|
464
470
|
executeRunId,
|
|
465
471
|
changeDir,
|
|
466
472
|
gitDir,
|
|
473
|
+
ctx,
|
|
467
474
|
});
|
|
468
475
|
data = {
|
|
469
476
|
ok: tr.ok,
|
package/src/run/command.js
CHANGED
|
@@ -1004,8 +1004,10 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1004
1004
|
|
|
1005
1005
|
// ── Classify change on first entry ──
|
|
1006
1006
|
if (!progress.stages?.brainstorm?.status && !progress.stages?.plan?.status) {
|
|
1007
|
-
const { classifyChange } = await import('../classify-change.js')
|
|
1008
|
-
|
|
1007
|
+
const { classifyChange, readAutoModeFromLocalYaml } = await import('../classify-change.js')
|
|
1008
|
+
// auto_mode 接线(2026-08-11):从 local.yaml 读 auto_mode 段传 localConfig,让 force_*_patterns 真生效。
|
|
1009
|
+
const localConfig = readAutoModeFromLocalYaml(cwd)
|
|
1010
|
+
const classification = classifyChange({ description: inputText || '', explicitMode, localConfig })
|
|
1009
1011
|
if (classification.mode === 'quick') {
|
|
1010
1012
|
console.log(`📊 auto 模式分类:${classification.mode}(${classification.reason})`)
|
|
1011
1013
|
console.log(` 此变更建议使用 quick 模式,运行:sillyspec run quick "${inputText || '需求'}"`)
|
package/src/run/complete.js
CHANGED
|
@@ -18,7 +18,7 @@ import { join } from 'node:path'
|
|
|
18
18
|
import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs'
|
|
19
19
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
20
20
|
import { withFileLock } from '../quicklog.js'
|
|
21
|
-
import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot } from './shared.js'
|
|
21
|
+
import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot, getOrCreateMultiRepoContext } from './shared.js'
|
|
22
22
|
import { outputStep } from './prompt.js'
|
|
23
23
|
import { enforceDepsGate, enforceReviewJsonGate, completeStageGates, readDesignScale } from './gates.js'
|
|
24
24
|
import { handleArchiveConfirmStep, handlePlanGeneratePlanStep, handleScanProjectListStep, handleWorkflowPostCheck, handleQuickStageCompletion, handleExecuteWaveArtifact } from './complete-handlers.js'
|
|
@@ -213,7 +213,15 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
213
213
|
// 已存在跳过),据 git diff base..head 按 allowed_paths 归属生成 cannot_verify 草稿,进 gate 前就位。
|
|
214
214
|
try {
|
|
215
215
|
const { generateTaskReviewDrafts } = await import('../task-review.js')
|
|
216
|
-
|
|
216
|
+
// W3 task-09:best-effort 构造 ctx 透传(D-013),跨仓 task 草稿用跨仓 gitDir 取 diff。
|
|
217
|
+
// 失败降级 null(不阻断 execute 完成——草稿兜底本就是 best-effort,gate 会复校验)。
|
|
218
|
+
let _draftCtx = null
|
|
219
|
+
try {
|
|
220
|
+
_draftCtx = await getOrCreateMultiRepoContext({ cwd, changeName, platformOpts })
|
|
221
|
+
} catch (e) {
|
|
222
|
+
console.warn(' ⚠️ 草稿 ctx 构造失败,降级单仓草稿(' + (e && e.message ? e.message : e) + ')')
|
|
223
|
+
}
|
|
224
|
+
const _drafts = await generateTaskReviewDrafts({ changeName, cwd, platformOpts, ctx: _draftCtx })
|
|
217
225
|
if (_drafts.generated > 0) {
|
|
218
226
|
console.log(' 📄 自动补写 ' + _drafts.generated + ' 个 per-task review.json 草稿(cannot_verify,主 agent 实现模式兜底,需 agent 复核后升级为 pass/fail)')
|
|
219
227
|
if (_drafts.unattributed && _drafts.unattributed.length > 0) {
|
|
@@ -280,7 +288,15 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
280
288
|
// 阶段完成收尾共享管线(handleScan manifest + validateMetadata/FileLocations + auxiliary 重置 +
|
|
281
289
|
// runStageCompletionGates + execute worktree cleanup),消除 S1/S2/S3 三处收尾不对称(task-01 抽出)。
|
|
282
290
|
// gate 失败已 rollback,early-return 跳过下方"阶段已完成/下一步"提示(合理收紧:回滚不该打完成提示)。
|
|
283
|
-
|
|
291
|
+
// W3 task-09:best-effort 构造 ctx 透传给 completeStageGates(D-013),让 task-07 gate 跨仓 task
|
|
292
|
+
// 切 gitDir + per-repo verify。execute 启动期已 fail-closed 过一次,此处命中缓存或单仓退化。
|
|
293
|
+
let _completeCtx = null
|
|
294
|
+
try {
|
|
295
|
+
_completeCtx = await getOrCreateMultiRepoContext({ cwd, changeName, platformOpts })
|
|
296
|
+
} catch (e) {
|
|
297
|
+
console.warn('⚠️ 阶段完成 ctx 构造失败,降级单仓 gate(' + (e && e.message ? e.message : e) + ')')
|
|
298
|
+
}
|
|
299
|
+
const _stageGatesResult = await completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, outputText, ctx: _completeCtx })
|
|
284
300
|
if (_stageGatesResult) return _stageGatesResult
|
|
285
301
|
|
|
286
302
|
// gate 全过:persist completed(task-01 移后)。此处到 _write 之间若崩,DB 仍 in-progress(内存已 completed 但未落盘),下次进 CLI 读 DB 即 in-progress,不产生"假 completed"。
|
|
@@ -739,7 +755,14 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
739
755
|
// persist _write 移到 completeStageGates 成功之后(task-03 / review-2026-08-09 #2):gate 异常/失败 → rollback 回 in-progress 落盘,此处未到 _write,DB 不留假 completed。
|
|
740
756
|
// 阶段完成收尾共享管线(含 execute worktree cleanup),消除 continueStep 完成分支绕过 gate 的 S2(task-01 抽出)。
|
|
741
757
|
// gate 失败已 rollback,early-return 跳过下方"阶段已完成/下一步"提示(与 completeStep 同语义)。
|
|
742
|
-
|
|
758
|
+
// W3 task-09:best-effort 构造 ctx 透传(D-013),与 completeStep 完成分支同语义。
|
|
759
|
+
let _contCtx = null
|
|
760
|
+
try {
|
|
761
|
+
_contCtx = await getOrCreateMultiRepoContext({ cwd, changeName, platformOpts })
|
|
762
|
+
} catch (e) {
|
|
763
|
+
console.warn('⚠️ 阶段完成 ctx 构造失败,降级单仓 gate(' + (e && e.message ? e.message : e) + ')')
|
|
764
|
+
}
|
|
765
|
+
const _stageGatesResult = await completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps: stageData.steps, currentIdx, outputText: null, ctx: _contCtx })
|
|
743
766
|
if (_stageGatesResult) return _stageGatesResult
|
|
744
767
|
// gate 全过:persist completed(task-03 移后;此处无 triggerSync)。
|
|
745
768
|
pm._write(cwd, progress, changeName)
|
package/src/run/gates.js
CHANGED
|
@@ -187,7 +187,7 @@ function rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx,
|
|
|
187
187
|
*
|
|
188
188
|
* @returns {{stageCompleted:false,currentIdx,nextPendingIdx:number}|null}
|
|
189
189
|
*/
|
|
190
|
-
export async function runStageCompletionGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx }) {
|
|
190
|
+
export async function runStageCompletionGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, ctx = null }) {
|
|
191
191
|
const projectName = progress.project || basename(cwd)
|
|
192
192
|
const contractResult = runValidators(stageName, cwd, changeName, { projectName, specRoot: platformOpts?.specRoot })
|
|
193
193
|
if (contractResult.errors.length > 0) {
|
|
@@ -220,7 +220,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
220
220
|
const { runVerifyTestCheck, printVerifyTestCheck } = await import('../verify-postcheck.js')
|
|
221
221
|
// 测试实测是同步 execSync,长套件可跑 2~10min 且中途无输出——先预告避免 agent 误判卡死
|
|
222
222
|
console.log(`\n⏳ Verify 测试对账:CLI 亲自执行 local.yaml 的 commands.test(同步,耗时可能较长,请等待…)`)
|
|
223
|
-
const testCheck = runVerifyTestCheck({ cwd, specBase, changeName })
|
|
223
|
+
const testCheck = runVerifyTestCheck({ cwd, specBase, changeName, ctx })
|
|
224
224
|
printVerifyTestCheck(testCheck)
|
|
225
225
|
if (testCheck.status === 'failed') {
|
|
226
226
|
console.error('\n❌ verify 阶段被阻断:verify-result.md 自报告通过,但 CLI 实测测试失败。')
|
|
@@ -354,18 +354,30 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
354
354
|
}
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
-
// git 真实性校验目录:worktree 存在则用 worktree(base/head commit
|
|
357
|
+
// git 真实性校验目录:worktree 存在则用 worktree(base/head commit 在其中),否则主仓库。
|
|
358
|
+
// 跨仓支持(task-07 / design §6 gates 行 + D-013):
|
|
359
|
+
// - 有 ctx(execute 启动入口 task-09 构造的 MultiRepoContext 实例)→ 用
|
|
360
|
+
// ctx.resolve('main').gitDir 作为主仓 task 的校验 cwd。ctx 的 main entry 已在
|
|
361
|
+
// _buildMainEntry 内统一编码 worktreePath/cwd + in-place-fallback 兜底(与 task-review.js:724
|
|
362
|
+
// generateTaskReviewDrafts 的 in-place 逻辑同源),避免两处漂移。Task Review Gate 内部循环
|
|
363
|
+
// 再按 review.repo ?? 'main' 从 ctx.resolve(repo).gitDir 切跨仓 task 的 gitDir(task-04 实现)。
|
|
364
|
+
// - 无 ctx(ctx=null 缺省,单仓 / 旧调用链)→ 走原逻辑(meta.worktreePath 或 cwd),零回归。
|
|
358
365
|
let reviewGitDir = cwd
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
366
|
+
if (ctx) {
|
|
367
|
+
const mainEntry = ctx.resolve('main')
|
|
368
|
+
if (mainEntry?.gitDir) reviewGitDir = mainEntry.gitDir
|
|
369
|
+
} else {
|
|
370
|
+
try {
|
|
371
|
+
const { WorktreeManager } = await import('../worktree.js')
|
|
372
|
+
const wm = new WorktreeManager({ cwd })
|
|
373
|
+
const meta = wm.getMeta(changeName)
|
|
374
|
+
if (meta?.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
|
|
375
|
+
reviewGitDir = meta.worktreePath
|
|
376
|
+
}
|
|
377
|
+
} catch {}
|
|
378
|
+
}
|
|
367
379
|
|
|
368
|
-
const reviewResult = validateTaskReviews({ planContent, runtimeRoot, executeRunId, changeDir: planFile, gitDir: reviewGitDir })
|
|
380
|
+
const reviewResult = validateTaskReviews({ planContent, runtimeRoot, executeRunId, changeDir: planFile, gitDir: reviewGitDir, ctx })
|
|
369
381
|
printReviewResult(reviewResult, { runtimeRoot, executeRunId })
|
|
370
382
|
|
|
371
383
|
if (!reviewResult.ok) {
|
|
@@ -557,7 +569,7 @@ function readDesignOwnFiles(specBase, changeName) {
|
|
|
557
569
|
* null = 全部通过,调用方继续自管收尾(如下一步提示);
|
|
558
570
|
* 非 null = gate/handler 失败已 rollback,调用方直接 return。
|
|
559
571
|
*/
|
|
560
|
-
export async function completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, outputText }) {
|
|
572
|
+
export async function completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, outputText, ctx = null }) {
|
|
561
573
|
// 整体 try/catch(task-04 / review-2026-08-09 #2):收尾段(execute 预检 + scan handler + validate* +
|
|
562
574
|
// auxiliary 重置 + runStageCompletionGates)任一段抛非结构化异常 → rollbackCompletionAndReturn
|
|
563
575
|
// (回滚 in-progress + 落盘 + 返回未完成对象),不冒顶 exit 1。正常的 _scanResult/_gateEarlyReturn
|
|
@@ -630,7 +642,7 @@ export async function completeStageGates({ stageName, cwd, changeName, platformO
|
|
|
630
642
|
|
|
631
643
|
// 阶段完成校验 gate 级联(runValidators → verify-test → Plan→Execute → Stage Review → Task Review)
|
|
632
644
|
if (settledCount === total && total > 0) {
|
|
633
|
-
const _gateEarlyReturn = await runStageCompletionGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx })
|
|
645
|
+
const _gateEarlyReturn = await runStageCompletionGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, ctx })
|
|
634
646
|
if (_gateEarlyReturn) return _gateEarlyReturn
|
|
635
647
|
} else if (settledCount < total) {
|
|
636
648
|
console.log(`\n⚠️ 阶段校验跳过:${total} 步中仅 ${settledCount} 步标记为已结案(completed‖skipped),可能存在状态不同步。如确认阶段已完成,请运行 --status 确认。`)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MultiRepoContext — 跨仓 task 运行时多仓执行上下文(W1 task-01)。
|
|
3
|
+
*
|
|
4
|
+
* 设计依据:design §7.1(接口权威定义)+ 决策 D-005/D-006/D-007/D-013。
|
|
5
|
+
*
|
|
6
|
+
* 收口 execute 链路 7 个单仓假设点(applyWorktree / validateTaskReviews /
|
|
7
|
+
* runVerifyTestCheck / generateTaskReviewDrafts 等)——把"主仓=唯一仓库"假设
|
|
8
|
+
* 抽成 Map<repoKey, RepoEntry>,单仓 change 退化为 {main:{...}} 单值 map 零回归
|
|
9
|
+
* (GOAL-2),跨仓 change 实时反映各仓 HEAD(约束①)+ 配置错误 fail-closed(约束②)。
|
|
10
|
+
*
|
|
11
|
+
* 关键约束:
|
|
12
|
+
* - 约束①(D-006):跨仓仓无 meta.json,base/head 不读 meta。resolveHead 实时
|
|
13
|
+
* `git -C <crossRepo> rev-parse HEAD`(不缓存,task 推进后过期);resolveBase
|
|
14
|
+
* 用 taskBaseCommit(task 卡 base_commit 锡点,跨仓必传)。
|
|
15
|
+
* - 约束②(D-007):declaredRepos 有 key 不在 repoRegistry → throw;跨仓仓
|
|
16
|
+
* `git -C <crossRepo> rev-parse HEAD` 失败 → throw。配置错误不降级,不沿用
|
|
17
|
+
* 主仓 verifyReviewGitEvidence 的 unavailable 降级(那是环境缺失容错;跨仓
|
|
18
|
+
* 不可用是用户 local.yaml 配错,性质不同,必须阻断)。
|
|
19
|
+
* - D-013:execute 启动时构造一次,进程级缓存贯穿 execute/apply/verify(本模块
|
|
20
|
+
* 不持有进程级缓存——ctx 实例本身就是进程级单例,由 execute 入口持有)。
|
|
21
|
+
*
|
|
22
|
+
* 不涉及生命周期契约(design §7.5):纯内存对象,不跨进程、不持久化、不入库。
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { git, gitQuiet } from '../git-helper.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} RepoEntry
|
|
29
|
+
* @property {string} repoKey - repo 标识('main' 或 local.yaml repos: 的键名)
|
|
30
|
+
* @property {string} gitDir - git 工作目录(rev-parse/diff/apply 的 cwd)
|
|
31
|
+
* @property {string} worktreePath - 子代理工作目录(主仓=worktreePath 或 in-place-fallback 时=cwd;跨仓=跨仓仓根)
|
|
32
|
+
* @property {string} projectRoot - apply 目标根(主仓=主仓根;跨仓=跨仓仓根,但跨仓 apply=no-op 不用)
|
|
33
|
+
* @property {boolean} isMain - 是否主仓
|
|
34
|
+
* @property {function} resolveHead - () => string,实时 git rev-parse HEAD(跨仓不缓存)
|
|
35
|
+
* @property {function} resolveBase - (taskBaseCommit?) => string,base 锚点(主仓→meta.baseHash;跨仓→taskBaseCommit 必传)
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
export class MultiRepoContext {
|
|
39
|
+
/**
|
|
40
|
+
* @param {Object} opts
|
|
41
|
+
* @param {string} opts.cwd - 主仓 cwd
|
|
42
|
+
* @param {string} opts.changeName - 当前 change 名(读 worktreeManager.getMeta)
|
|
43
|
+
* @param {Object} [opts.platformOpts] - 平台选项(specRoot 等,预留,本模块不消费)
|
|
44
|
+
* @param {string[]} opts.declaredRepos - 所有 task 卡片声明的 repo: 值(含 'main')
|
|
45
|
+
* @param {Map<string,string>} opts.repoRegistry - local.yaml repos: 段解析结果 Map<key, path>
|
|
46
|
+
* @param {Object} opts.worktreeManager - WorktreeManager 实例(主仓 meta 读取;in-place-fallback 兜底 cwd)
|
|
47
|
+
* @throws {Error} 约束②: declaredRepos 中有 repoKey 不在 repoRegistry → 抛错列已注册 repo,阻断 execute
|
|
48
|
+
* 约束②: 跨仓仓 git rev-parse 失败(路径不存在/非 git 仓)→ 抛错阻断 execute
|
|
49
|
+
*/
|
|
50
|
+
constructor({ cwd, changeName, platformOpts, declaredRepos, repoRegistry, worktreeManager }) {
|
|
51
|
+
if (!cwd) throw new Error('MultiRepoContext: cwd 必传');
|
|
52
|
+
if (!Array.isArray(declaredRepos)) throw new Error('MultiRepoContext: declaredRepos 必传为数组');
|
|
53
|
+
if (!(repoRegistry instanceof Map)) throw new Error('MultiRepoContext: repoRegistry 必传为 Map<key,path>');
|
|
54
|
+
if (!worktreeManager || typeof worktreeManager.getMeta !== 'function') {
|
|
55
|
+
throw new Error('MultiRepoContext: worktreeManager 必传(需 getMeta 方法)');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.cwd = cwd;
|
|
59
|
+
this.changeName = changeName;
|
|
60
|
+
this.platformOpts = platformOpts || {};
|
|
61
|
+
this.repoRegistry = repoRegistry;
|
|
62
|
+
this.worktreeManager = worktreeManager;
|
|
63
|
+
|
|
64
|
+
// dedupe + 保留声明顺序(同一 repo 多 task 声明只建一项)
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
const orderedKeys = [];
|
|
67
|
+
for (const key of declaredRepos) {
|
|
68
|
+
if (!seen.has(key)) {
|
|
69
|
+
seen.add(key);
|
|
70
|
+
orderedKeys.push(key);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 约束② fail-closed 预检:declaredRepos ⊆ repoRegistry ∪ {'main'}
|
|
75
|
+
// main 隐式 = cwd,不要求注册;其余 key 必须在 repoRegistry 命中。
|
|
76
|
+
const unregistered = orderedKeys.filter(k => k !== 'main' && !repoRegistry.has(k));
|
|
77
|
+
if (unregistered.length > 0) {
|
|
78
|
+
const registered = ['main (隐式)', ...repoRegistry.keys()];
|
|
79
|
+
throw new Error(
|
|
80
|
+
`MultiRepoContext: 以下 repo 未在 local.yaml repos: 段注册:[${unregistered.join(', ')}]。` +
|
|
81
|
+
`当前已注册 repo:[${registered.join(', ')}]。` +
|
|
82
|
+
`请在 local.yaml repos: 段补注册后再启动 execute(约束② fail-closed,跨仓 apply 走错仓=数据所有权事故)。`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const map = new Map();
|
|
87
|
+
for (const repoKey of orderedKeys) {
|
|
88
|
+
map.set(repoKey, repoKey === 'main'
|
|
89
|
+
? this._buildMainEntry()
|
|
90
|
+
: this._buildCrossRepoEntry(repoKey, repoRegistry.get(repoKey)));
|
|
91
|
+
}
|
|
92
|
+
this.map = map;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 主仓 RepoEntry:isMain=true,读 worktreeManager.getMeta 的 baseHash + worktreePath。
|
|
97
|
+
* in-place-fallback 模式(meta.mode==='in-place-fallback' 或 meta 缺失)worktreePath 兜底为 cwd。
|
|
98
|
+
* @returns {RepoEntry}
|
|
99
|
+
* @private
|
|
100
|
+
*/
|
|
101
|
+
_buildMainEntry() {
|
|
102
|
+
const meta = this.changeName ? this.worktreeManager.getMeta(this.changeName) : null;
|
|
103
|
+
const isInPlace = !meta || meta.mode === 'in-place-fallback';
|
|
104
|
+
const worktreePath = isInPlace ? this.cwd : (meta.worktreePath || this.cwd);
|
|
105
|
+
const baseHash = meta?.baseHash || null;
|
|
106
|
+
const gitDir = worktreePath; // 主仓 git 操作 cwd = worktree(或 in-place 的 cwd)
|
|
107
|
+
const projectRoot = this.cwd;
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
repoKey: 'main',
|
|
111
|
+
gitDir,
|
|
112
|
+
worktreePath,
|
|
113
|
+
projectRoot,
|
|
114
|
+
isMain: true,
|
|
115
|
+
// 主仓 HEAD:从 worktree/cwd 实时取(不缓存)。主仓单仓不变式下也可用 meta.baseHash 作 base,
|
|
116
|
+
// 但 HEAD 反映 worktree 当前态(子代理可能 commit 推进),故实时取与跨仓一致语义。
|
|
117
|
+
resolveHead: () => git(gitDir, ['rev-parse', 'HEAD']),
|
|
118
|
+
// 主仓 base:meta.baseHash(单仓不变式,CLI 创建 worktree 时锡点)。
|
|
119
|
+
// 跨仓 base 不走这里(resolveBase 必传 taskBaseCommit,由 _buildCrossRepoEntry 闭包消费)。
|
|
120
|
+
// 主仓忽略 taskBaseCommit 参数——主仓 base 永远锚 meta.baseHash,与单仓行为一致(零回归)。
|
|
121
|
+
resolveBase: (_taskBaseCommit) => {
|
|
122
|
+
if (!baseHash) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
'MultiRepoContext: 主仓 meta.baseHash 缺失,无法解析 base。' +
|
|
125
|
+
'可能 worktree 未创建或 meta.json 损坏——请先 sillyspec worktree doctor 检查。'
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return baseHash;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 跨仓 RepoEntry:isMain=false,gitDir=projectRoot=worktreePath=跨仓仓根。
|
|
135
|
+
* 构造时 `git -C <crossRepo> rev-parse HEAD` 必须 succeed(约束② fail-closed),
|
|
136
|
+
* 路径不存在/非 git 仓 → throw 阻断 execute。
|
|
137
|
+
* @param {string} repoKey
|
|
138
|
+
* @param {string} crossRepoPath - repoRegistry 解析出的跨仓仓绝对路径
|
|
139
|
+
* @returns {RepoEntry}
|
|
140
|
+
* @private
|
|
141
|
+
*/
|
|
142
|
+
_buildCrossRepoEntry(repoKey, crossRepoPath) {
|
|
143
|
+
if (!crossRepoPath) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`MultiRepoContext: repo "${repoKey}" 在 local.yaml repos: 段注册但路径为空。` +
|
|
146
|
+
`请补全路径(约束② fail-closed)。`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
// 约束②:跨仓 git 必须可达。失败 = 路径不存在 / 非 git 仓 / git 异常,一律阻断。
|
|
150
|
+
// 不沿用主仓 verifyReviewGitEvidence unavailable 降级(design §9 兼容策略表)。
|
|
151
|
+
let headVerify;
|
|
152
|
+
try {
|
|
153
|
+
headVerify = git(crossRepoPath, ['rev-parse', 'HEAD']);
|
|
154
|
+
} catch (e) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`MultiRepoContext: 跨仓 repo "${repoKey}" git 不可达(路径:${crossRepoPath})。` +
|
|
157
|
+
`rev-parse HEAD 失败:${e.message}。` +
|
|
158
|
+
`请检查 local.yaml repos: 段路径是否正确、目录是否存在且为 git 仓(约束② fail-closed,配置错误不降级)。`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (!headVerify) {
|
|
162
|
+
// git() 返回空串属异常态(无 commit 的空仓 rev-parse HEAD 会非零退出,已被 catch 兜底),
|
|
163
|
+
// 此处仅作防御性双保险,触发同样走 fail-closed。
|
|
164
|
+
throw new Error(
|
|
165
|
+
`MultiRepoContext: 跨仓 repo "${repoKey}"(${crossRepoPath})rev-parse HEAD 返回空——` +
|
|
166
|
+
`疑似空 git 仓(无 commit)。跨仓 task 需仓库已有至少一个 commit(约束② fail-closed)。`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
repoKey,
|
|
172
|
+
gitDir: crossRepoPath,
|
|
173
|
+
worktreePath: crossRepoPath,
|
|
174
|
+
projectRoot: crossRepoPath,
|
|
175
|
+
isMain: false,
|
|
176
|
+
// 约束①:跨仓 HEAD 实时取,不缓存(task 推进后过期,每次 resolveHead 反映最新 HEAD)。
|
|
177
|
+
resolveHead: () => git(crossRepoPath, ['rev-parse', 'HEAD']),
|
|
178
|
+
// 约束①:跨仓 base = task 卡 base_commit 锡点(必传)。跨仓仓无 meta.json,
|
|
179
|
+
// 不读 meta;taskBaseCommit 由 task-02 DeclaredRepos 之外的 task 卡 frontmatter 提供。
|
|
180
|
+
resolveBase: (taskBaseCommit) => {
|
|
181
|
+
if (!taskBaseCommit) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`MultiRepoContext: 跨仓 repo "${repoKey}" resolveBase 必传 taskBaseCommit(task 卡 base_commit 锡点)。` +
|
|
184
|
+
`跨仓仓无 meta.json,base 只能由 task 卡片锡点提供(约束①)。`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return taskBaseCommit;
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** @returns {RepoEntry|null} */
|
|
193
|
+
resolve(repoKey) {
|
|
194
|
+
return this.map.get(repoKey) || null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** @returns {Map<string, RepoEntry>} */
|
|
198
|
+
get repos() {
|
|
199
|
+
return this.map;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** @returns {boolean} 是否含跨仓 task(map.size>1,execute prompt 分叉用) */
|
|
203
|
+
hasCrossRepo() {
|
|
204
|
+
return this.map.size > 1;
|
|
205
|
+
}
|
|
206
|
+
}
|