sillyspec 3.26.2 → 3.26.4

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.
@@ -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
  ```
@@ -33,6 +33,18 @@ sillyspec run quick --reset # 重置阶段(从头开始)
33
33
  sillyspec run quick --reopen --from-step N # 重新打开已完成阶段修订(N=序号或名称)
34
34
  ```
35
35
 
36
+ ### 已有进行中会话的恢复(sessionId)
37
+
38
+ `sillyspec run quick` / `run quick --done` / `--status` **不带 `--change` 时,是启动一个新 quick 会话**(CLI 分配新 `quick-<hash>` + 新 ql-ID,QUICKLOG 追加「进行中」条目),不是恢复正在进行的旧会话。多会话并发或中断恢复时,必须带上启动时 CLI 打印的 sessionId:
39
+
40
+ ```bash
41
+ sillyspec run quick --change quick-<hash> # 恢复查看该会话当前 step prompt
42
+ sillyspec run quick --done --change quick-<hash> --output "…" # 完成该会话步骤
43
+ ```
44
+
45
+ - 这里的 `--change quick-<hash>` 是「恢复到该 CLI 生成的 session」,与下方 `--linked-changes` 的关联业务变更语义不同:想关联真实业务变更才用 `--linked-changes`。
46
+ - 误启动的空壳会话:`sillyspec run quick --reset --change quick-<hash>` 重置其进度;QUICKLOG 里残留的「(quick 任务)」骨架条目需手动删除,不留占位条目。
47
+
36
48
  ## 通用参数(所有阶段适用)
37
49
 
38
50
  | 参数 | 说明 |
@@ -91,7 +103,7 @@ sillyspec run quick --files src/phone.ts,src/phone.test.ts
91
103
  - QUICKLOG 记录的**骨架由 CLI 接管**:启动时 CLI 自动分配 ql-ID 并在 `.sillyspec/quicklog/QUICKLOG-<user>.md` 写「进行中」条目(含关联变更 tasks.md),`--done` 时 CLI 自动翻「已完成」+ 勾选 task + 回填文件路径。ql-ID 分配/状态/task 你无需手写,只需用注入的 `<quicklog-id>` 在模块文档变更索引引用
92
104
  - **QUICKLOG 落盘已结构化(`--done` 后按需核对,多数无需手改)**:CLI 已落盘结构化条目——标题从 `--output` 的「需求:」自动提取、正文四字段自动分行、文件行用 `--file-notes` 时为多行带括注。`--done` 后只需核对:标题弱才改(禁留 `(quick 任务)` 占位);没用 `--file-notes` 时文件行是单行、可事后补括注(参照同文件早期丰富条目);正文 `需求:`/`根因:`/`方案:`/`结果:` 四段按需充实(禁只留一段「结果:」)。一条 quick = 一条独立 ql,不追加到旧条目
93
105
  - **收尾顺序(模块文档在 `--done` 前,QUICKLOG 在 `--done` 后,别记混)**:① 命中模块→改模块文档→`git add`;② `sillyspec run quick --done --change <id> --output "四字段" [--file-notes "..."]`(CLI 自动翻完成 + 勾 task + 落盘 QUICKLOG 标题/文件/正文);③ 核对 QUICKLOG 标题(CLI 已从「需求:」提取,仅弱标题才改)→若改了再 `git add`
94
- - **最后一步 `--done --output` 必须按结构化结果模板给全四字段**(逐项一句话):`需求:… 根因:… 方案:… 结果:…`。这是 QUICKLOG「结果:」归档的唯一来源;CLI 校验缺字段会拒绝 `--done`(exit 1),补全后重跑即可。前两个 step 的 `--output` 是中间摘要,不用此模板
106
+ - **最后一步 `--done --output` 必须按结构化结果模板给全四字段**(逐项一句话):`需求:… 根因:… 方案:… 结果:…`。这是 QUICKLOG「结果:」归档的唯一来源;CLI 校验缺字段会拒绝 `--done`(exit 1),补全后重跑即可。正文内避免嵌套全角冒号(如「方案:(说明 xxx):」这样标签后紧接嵌套冒号会被拆分判定缺字段),直接让标签接正文。前两个 step 的 `--output` 是中间摘要,不用此模板
95
107
  - **禁止**在没有运行 CLI 的情况下自行决定流程
96
108
 
97
109
  ## 用户指令
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.26.2",
3
+ "version": "3.26.4",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
package/src/db.js CHANGED
@@ -116,6 +116,17 @@ export class DB {
116
116
  if (readValid(this.dbPath) === 'ok') {
117
117
  const primaryDb = tryOpen(this.dbPath);
118
118
  if (primaryDb) return primaryDb;
119
+ // 并发首开竞争(2026-08-12 db-concurrency flaky 根因实证):多进程近乎同时
120
+ // new DB().init() 打开同一新建库时,tryOpen 的 prepare(SELECT count(*)) 可能撞上
121
+ // 他者进程的 CHECKPOINT 改写主库 → 瞬时失败返 null。此时主库存在且有内容,
122
+ // 非真损坏——有限重试消化锁竞争(复用 MAX_BUSY_RETRIES 退避,与 transaction 的
123
+ // SQLITE_BUSY 重试同基建)。真损坏重试也不会过,最终仍走 .bak 回退 / fail-loud,
124
+ // 防吞进度语义零回归。
125
+ for (let attempt = 0; attempt < MAX_BUSY_RETRIES; attempt++) {
126
+ _sleepSync(BUSY_BACKOFF_MS[attempt]);
127
+ const retryDb = tryOpen(this.dbPath);
128
+ if (retryDb) return retryDb;
129
+ }
119
130
  }
120
131
 
121
132
  // 2. .bak 回退(.bak 是恢复源:存在且能打开即用,含 0 字节空库;门禁只挡"不存在",
@@ -137,6 +137,9 @@ ${branchLine}
137
137
  2. 蓝图文件路径(让子代理按需读取详情,不要内联整份蓝图污染上下文)
138
138
  3. 编码铁律:先读后写、TDD、不编造方法、只做蓝图里写的事、遵守边界处理规则、不超出 allowedPaths
139
139
  4. 如存在模块文档(\`.sillyspec/docs/*/modules/\`),按需读取涉及模块的 <module>.md 参考接口约定与数据流
140
+ 5. 任务含测试代码时,把下方「测试用例设计」整段复制进子代理 prompt,要求子代理按此设计测试用例
141
+
142
+ {{include: testcase-design}}
140
143
  ${modeLine}
141
144
  ${allowedLine}
142
145
 
@@ -223,6 +223,9 @@ dispatch_worker 的 \`worker_prompt\` 参数必须传以下覆写文本(路径
223
223
  交 SillySpec 主体 git diff 回收(D-004)
224
224
  3. 不超出 allowedPaths,不碰其他 task 的文件
225
225
  4. 完成后输出简短自报(改了哪些文件 / 跑了哪些测试 / 是否存疑),不自报 review.json(SillySpec 自己写)
226
+ 5. 写测试时遵循下方「测试用例设计」规则:
227
+
228
+ {{include: testcase-design}}
226
229
  \`\`\`
227
230
 
228
231
  ### 终态轮询 + 超时 kill lease(UB-6)
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
- const result = await generateTaskReviewDrafts({ changeName: brChange, cwd: dir, platformOpts: brPlatformOpts });
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
- const result = applyWorktree(wtName, { cwd: dir, checkOnly, merge });
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 {
@@ -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,
@@ -1079,6 +1079,9 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1079
1079
  console.log(`⏳ 变更 ${changeName} 的执行审批待处理中...`)
1080
1080
  console.log(' 提示:使用 --skip-approval 跳过审批检查')
1081
1081
  }
1082
+ if (approval.status === 'unknown') {
1083
+ console.warn(`⚠️ 变更 ${changeName} 的审批状态未知(${approval.reason || '请求失败'}),按本地模式放行(非审批中,无需等待)`)
1084
+ }
1082
1085
  }
1083
1086
  }
1084
1087
  await outputStep(currentStage, pendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
@@ -1109,6 +1112,9 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1109
1112
  console.log(`⏳ 变更 ${changeName} 的执行审批待处理中...`)
1110
1113
  console.log(' 提示:使用 --skip-approval 跳过审批检查')
1111
1114
  }
1115
+ if (approval.status === 'unknown') {
1116
+ console.warn(`⚠️ 变更 ${changeName} 的审批状态未知(${approval.reason || '请求失败'}),按本地模式放行(非审批中,无需等待)`)
1117
+ }
1112
1118
  }
1113
1119
  }
1114
1120
  await outputStep(currentStage, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
@@ -1176,6 +1182,9 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1176
1182
  console.log(`⏳ 变更 ${changeName} 的执行审批待处理中...`)
1177
1183
  console.log(' 提示:使用 --skip-approval 跳过审批检查')
1178
1184
  }
1185
+ if (approval.status === 'unknown') {
1186
+ console.warn(`⚠️ 变更 ${changeName} 的审批状态未知(${approval.reason || '请求失败'}),按本地模式放行(非审批中,无需等待)`)
1187
+ }
1179
1188
  }
1180
1189
  }
1181
1190
  await outputStep(next, firstPending, nextSteps, cwd, changeName, progress.project || null, platformOpts)
@@ -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
- const _drafts = await generateTaskReviewDrafts({ changeName, cwd, platformOpts })
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
- const _stageGatesResult = await completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, outputText })
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
- const _stageGatesResult = await completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps: stageData.steps, currentIdx, outputText: null })
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
- try {
360
- const { WorktreeManager } = await import('../worktree.js')
361
- const wm = new WorktreeManager({ cwd })
362
- const meta = wm.getMeta(changeName)
363
- if (meta?.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
364
- reviewGitDir = meta.worktreePath
365
- }
366
- } catch {}
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 确认。`)