sillyspec 3.26.2 → 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/index.js +55 -3
- package/src/machine-interface.js +11 -4
- 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 +6 -0
- package/src/run/shared.js +170 -2
- package/src/stage-review.js +1 -0
- package/src/stages/execute.js +185 -7
- package/src/stages/plan-postcheck.js +266 -11
- package/src/sync.js +24 -1
- 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
|
@@ -118,6 +118,44 @@ execute 还有**第二道**独立的 stage 级审查:除逐 task review.json
|
|
|
118
118
|
|
|
119
119
|
可选:`sillyspec dispatch probe` 查看 SillyHub 是否可用。
|
|
120
120
|
|
|
121
|
+
## 跨仓 task(一个 change 改多个仓库)
|
|
122
|
+
|
|
123
|
+
单个 change 的 task 可以分散到主仓 + 多个跨仓仓实现(典型场景:dogfood 自指、monorepo 多包仓、共享库 + 调用方联合改造)。**单仓 change 不需要任何跨仓配置**(所有 task 不写 `repo:` 即走原流程,零回归)。
|
|
124
|
+
|
|
125
|
+
### 跨仓 task 配置(plan 阶段产出,execute 阶段消费)
|
|
126
|
+
|
|
127
|
+
- **task 卡片 `repo:` 字段**:跨仓 task 在 `tasks/task-NN.md` frontmatter 写 `repo: <key>`(缺省='main'=主仓 task,不写即主仓)。
|
|
128
|
+
- **local.yaml `repos:` 段**:在 `.sillyspec/local.yaml` 注册跨仓仓路径(`main` 不用注册,隐式=当前项目):
|
|
129
|
+
```yaml
|
|
130
|
+
repos:
|
|
131
|
+
sillyspec: C:/Users/qinyi/IdeaProjects/sillyspec
|
|
132
|
+
shared-lib: ../shared-lib
|
|
133
|
+
```
|
|
134
|
+
task 卡 `repo:` 引用的 key 必须在此注册,否则 execute 启动 fail-closed 阻断(跨仓 apply 走错仓=数据所有权事故,配置错误不降级)。
|
|
135
|
+
- **跨仓 task 的 `allowed_paths`**:指**相对跨仓仓根**的路径(非主仓根)。
|
|
136
|
+
|
|
137
|
+
### workdir 切换(execute 派发子代理时)
|
|
138
|
+
|
|
139
|
+
- **主仓 task**:子代理 workdir = 主仓 worktree 路径(CLI 自动创建的隔离 worktree)。
|
|
140
|
+
- **跨仓 task**:子代理 workdir = 跨仓仓根目录(**直接在跨仓仓主干工作区改+commit,不经主仓 worktree、不建分支**)。commit 到跨仓仓主干即落盘。
|
|
141
|
+
- 同一个 Wave 内允许混合主仓 + 跨仓 task(每个 task 独立子代理,各传各的 workdir)。Wave prompt 会注入 per-task workdir 表,按表选。
|
|
142
|
+
|
|
143
|
+
### 跨仓 task 的双锡点(CLI 写入,子代理不改)
|
|
144
|
+
|
|
145
|
+
- `base_commit`:CLI 派发跨仓 task 前实时 `git -C <跨仓仓根> rev-parse HEAD` 落盘到 task 卡 frontmatter(锁 base,防同 Wave 多 task 改同跨仓仓时 HEAD 推进致 diff 漂移)。
|
|
146
|
+
- `head_commit`:跨仓 task 子代理 commit 完成后、写 review.json / 勾选 checkbox **之前**,由你(主 agent)运行 `git -C <跨仓仓根> rev-parse HEAD` 把结果写入该 task 卡 frontmatter `head_commit:` 字段。
|
|
147
|
+
- review.json 的 `base`/`head` 取这两个锡点(非瞬时 HEAD)。
|
|
148
|
+
|
|
149
|
+
### 跨仓 task 的 review.json
|
|
150
|
+
|
|
151
|
+
- 路径仍写主仓 `.sillyspec/.runtime/execute-runs/<run-id>/tasks/task-XX/review.json`(review 统一存主仓)。
|
|
152
|
+
- 加 `repo: <key>` 字段标该 task 所属仓(缺省='main')。
|
|
153
|
+
- `base`/`head` 是**跨仓仓的 commit**(取 task 卡锡点),CLI 据此在跨仓仓根跑 git 校验。
|
|
154
|
+
|
|
155
|
+
### 跨仓 task apply = no-op
|
|
156
|
+
|
|
157
|
+
跨仓 task 的代码由子代理直接 commit 到跨仓仓主干(commit 即落地),主仓 `worktree apply` 对跨仓 task **不打 patch、不 cleanup**——只校验 `review.head` 是跨仓仓真实 commit。主仓 task 走原 apply 路径不变。
|
|
158
|
+
|
|
121
159
|
## worktree 子命令(execute 相关)
|
|
122
160
|
|
|
123
161
|
```bash
|
|
@@ -81,6 +81,54 @@ plan 的步骤是动态的:`generate_plan`(生成分级计划)→ `review_
|
|
|
81
81
|
|
|
82
82
|
plan 完成校验会检查:design 提到入口文件(cli.ts/main.ts/server.ts 等)但 task 的 allowed_paths 不含该文件 → 报 error。若确实不需要改入口,在 design.md 明示理由。
|
|
83
83
|
|
|
84
|
+
## 跨仓 task 卡片协议(一个 change 改多个仓库)
|
|
85
|
+
|
|
86
|
+
单个 change 的 task 可以分散到主仓 + 多个跨仓仓实现(典型场景:dogfood 自指、monorepo 多包仓、共享库 + 调用方联合改造)。**单仓 change 不需要任何跨仓配置**(所有 task 不写 `repo:` 即走原流程,零回归)。
|
|
87
|
+
|
|
88
|
+
### 跨仓 task 卡片字段
|
|
89
|
+
|
|
90
|
+
在 `tasks/task-NN.md` frontmatter 加:
|
|
91
|
+
|
|
92
|
+
- `repo: <key>`(可选,缺省='main'=主仓 task):task 所属仓的 key。key 必须在 `.sillyspec/local.yaml` `repos:` 段注册(`main` 隐式=主仓不用注册)。跨仓 change 缺注册 → execute 启动 fail-closed 阻断。
|
|
93
|
+
- `allowed_paths`:跨仓 task 时指**相对跨仓仓根**的路径(非主仓根)。例如跨仓 task 改 `sillyspec` 仓的 `src/foo.js`,allowed_paths 写 `src/foo.js`(相对 sillyspec 仓根),不是 `../sillyspec/src/foo.js`。
|
|
94
|
+
- `base_commit` / `head_commit`:**不要手写**——CLI 在 execute 派发/回收时自动落盘双锡点(锁 base/head 防 diff 漂移),plan 阶段不写。
|
|
95
|
+
|
|
96
|
+
### local.yaml `repos:` 段(跨仓注册表)
|
|
97
|
+
|
|
98
|
+
跨仓 change 必须在 `.sillyspec/local.yaml` 注册所有跨仓仓路径:
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
repos:
|
|
102
|
+
sillyspec: C:/Users/qinyi/IdeaProjects/sillyspec
|
|
103
|
+
shared-lib: ../shared-lib
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
key = task 卡 `repo:` 引用名,value = 跨仓仓绝对路径(或相对主仓根的路径)。`main` 不用注册。
|
|
107
|
+
|
|
108
|
+
### design.md 文件变更清单按仓分段
|
|
109
|
+
|
|
110
|
+
跨仓 change 的 `## 文件变更清单` 段须按仓分段,段头格式 `## <repo> 仓变更`(主仓段头可省略或写 `## main 仓变更`):
|
|
111
|
+
|
|
112
|
+
```markdown
|
|
113
|
+
## 文件变更清单
|
|
114
|
+
|
|
115
|
+
### sillyspec 仓变更
|
|
116
|
+
| 操作 | 文件路径 | 说明 |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| 修改 | src/task-review.js | 跨仓 task 改的文件 |
|
|
119
|
+
|
|
120
|
+
### main 仓变更
|
|
121
|
+
| 操作 | 文件路径 | 说明 |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| 修改 | src/index.js | 主仓 task 改的文件 |
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
plan 完成校验会按仓分段对账:跨仓仓路径相对跨仓仓根校验 task allowed_paths 覆盖,主仓路径相对主仓根校验。跨仓 task 与主仓 task 同名物理路径分属不同 repo 不判冲突。
|
|
127
|
+
|
|
128
|
+
### Wave 划分
|
|
129
|
+
|
|
130
|
+
同 Wave 内允许混合主仓 + 跨仓 task(execute 按 per-task workdir 派发,不强制同 Wave 同 repo)。共享文件的 task 仍须分到不同 Wave(同 Wave 共享文件会被强制并行,子代理互相覆盖)。
|
|
131
|
+
|
|
84
132
|
## 阶段流转
|
|
85
133
|
|
|
86
134
|
```
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -369,6 +369,16 @@ async function main() {
|
|
|
369
369
|
const wt = detectWorktreeSpecDrift(resolveSpecDir(dir));
|
|
370
370
|
if (wt) gateOpts.specDriftAnchor = wt.mainSpecBase;
|
|
371
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
|
+
}
|
|
372
382
|
const { envelope, exitCode } = await withJsonOutput(json, () => runGate(gateStage, gateChange, gateOpts));
|
|
373
383
|
if (json) {
|
|
374
384
|
process.stdout.write(JSON.stringify(envelope));
|
|
@@ -410,6 +420,14 @@ async function main() {
|
|
|
410
420
|
const wt = detectWorktreeSpecDrift(resolveSpecDir(dir));
|
|
411
421
|
if (wt) deriveOpts.specDriftAnchor = wt.mainSpecBase;
|
|
412
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
|
+
}
|
|
413
431
|
const { envelope, exitCode } = await withJsonOutput(json, () => runDerive(facet, deriveChange, deriveOpts));
|
|
414
432
|
if (json) {
|
|
415
433
|
process.stdout.write(JSON.stringify(envelope));
|
|
@@ -440,7 +458,16 @@ async function main() {
|
|
|
440
458
|
// --spec-dir 透传 platformOpts.specRoot(与 gate/derive 对称);不传走默认 join(cwd,'.sillyspec')。
|
|
441
459
|
const brPlatformOpts = {};
|
|
442
460
|
if (specDir) brPlatformOpts.specRoot = specDir;
|
|
443
|
-
|
|
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 });
|
|
444
471
|
if (json) {
|
|
445
472
|
process.stdout.write(JSON.stringify({ ok: true, command: 'backfill-reviews', change: brChange, ...result }));
|
|
446
473
|
} else {
|
|
@@ -769,7 +796,18 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
769
796
|
const checkOnly = args.includes('--check-only');
|
|
770
797
|
const merge = args.includes('--merge');
|
|
771
798
|
const { applyWorktree } = await import('./worktree-apply.js');
|
|
772
|
-
|
|
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 });
|
|
773
811
|
|
|
774
812
|
if (result.errors.length > 0) {
|
|
775
813
|
console.error(`❌ 校验失败:`);
|
|
@@ -822,6 +860,20 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
822
860
|
const { assessApplyRisk } = await import('./worktree-apply.js');
|
|
823
861
|
const assessment = assessApplyRisk(wtName, { cwd: dir });
|
|
824
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
|
+
}
|
|
825
877
|
const SEPARATOR = '─'.repeat(32);
|
|
826
878
|
console.log('Worktree Apply Decision');
|
|
827
879
|
console.log(SEPARATOR);
|
|
@@ -853,7 +905,7 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
853
905
|
if (assessment.decision === 'SAFE' || assessment.decision === 'WARNING') {
|
|
854
906
|
console.log('Action: auto-applying...');
|
|
855
907
|
const { applyWorktree } = await import('./worktree-apply.js');
|
|
856
|
-
const applyResult = applyWorktree(wtName, { cwd: dir });
|
|
908
|
+
const applyResult = applyWorktree(wtName, { cwd: dir, ctx: _assessCtx });
|
|
857
909
|
if (applyResult.errors.length > 0) {
|
|
858
910
|
console.error('❌ apply 失败:', applyResult.errors.join('; '));
|
|
859
911
|
} else {
|
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/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
|
+
}
|
package/src/run/prompt.js
CHANGED
|
@@ -23,6 +23,7 @@ import { stageRegistry } from '../stages/index.js'
|
|
|
23
23
|
import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, WAIT_MARKER_RE } from './shared.js'
|
|
24
24
|
import { renderStageContract } from '../stage-contract-spec.js'
|
|
25
25
|
import { parseModuleMapSimple } from '../modules.js'
|
|
26
|
+
import { REVIEW_SCHEMA_VERSION } from '../task-review.js'
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* 从 _module-map.yaml 读取模块上下文索引
|
|
@@ -218,6 +219,11 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
218
219
|
promptText = promptText.replace(/<now-datetime>/g, nowDatetime)
|
|
219
220
|
promptText = promptText.replace(/<now-timestamp>/g, nowTimestamp)
|
|
220
221
|
promptText = promptText.replace(/<now-date>/g, nowDate)
|
|
222
|
+
// 替换 {REVIEW_SCHEMA_VERSION} 占位符(task review 示例模板用,值=CLI 当前 REVIEW_SCHEMA_VERSION 常量,
|
|
223
|
+
// 避免 agent 照抄 design 目标版本与 CLI 写侧常量漂移;与 stage review 契约 renderReviewJsonContract 动态注入同源)
|
|
224
|
+
if (promptText.includes('{REVIEW_SCHEMA_VERSION}')) {
|
|
225
|
+
promptText = promptText.replace(/\{REVIEW_SCHEMA_VERSION\}/g, String(REVIEW_SCHEMA_VERSION))
|
|
226
|
+
}
|
|
221
227
|
// 替换 <change-name> 占位符
|
|
222
228
|
if (changeName && promptText.includes('<change-name>')) {
|
|
223
229
|
promptText = promptText.replace(/<change-name>/g, changeName)
|