@zhushanwen/pi-subagent-workflow 7.3.4 → 8.0.0

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.
Files changed (166) hide show
  1. package/README.md +39 -12
  2. package/agents/analyst.md +61 -0
  3. package/agents/coder.md +70 -0
  4. package/agents/debugger.md +67 -0
  5. package/agents/doc-reviewer.md +3 -3
  6. package/agents/explorer.md +50 -18
  7. package/agents/general-purpose.md +19 -8
  8. package/agents/orchestrator.md +37 -32
  9. package/agents/planner.md +45 -11
  10. package/agents/researcher.md +53 -11
  11. package/agents/reviewer.md +74 -0
  12. package/package.json +1 -1
  13. package/skills/workflow-script-format/SKILL.md +1 -1
  14. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
  15. package/src/execution/__tests__/agent-registry.test.ts +13 -11
  16. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
  17. package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
  18. package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
  19. package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
  20. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
  21. package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
  22. package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
  23. package/src/execution/__tests__/crash-recovery.test.ts +8 -2
  24. package/src/execution/__tests__/delivery-methods.test.ts +385 -0
  25. package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
  26. package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
  27. package/src/execution/__tests__/execute-nesting.test.ts +20 -72
  28. package/src/execution/__tests__/execution-record.test.ts +199 -0
  29. package/src/execution/__tests__/finalize-record.test.ts +170 -14
  30. package/src/execution/__tests__/format.test.ts +131 -7
  31. package/src/execution/__tests__/gc-timer.test.ts +184 -0
  32. package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
  33. package/src/execution/__tests__/helpers/spawn-mock.ts +25 -7
  34. package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
  35. package/src/execution/__tests__/index-session-start.test.ts +257 -5
  36. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
  37. package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
  38. package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
  39. package/src/execution/__tests__/list-component.test.ts +59 -5
  40. package/src/execution/__tests__/list-fields.test.ts +109 -0
  41. package/src/execution/__tests__/model-resolver.test.ts +38 -1
  42. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
  43. package/src/execution/__tests__/nested-visibility.test.ts +325 -0
  44. package/src/execution/__tests__/notifier-flush.test.ts +209 -7
  45. package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
  46. package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
  47. package/src/execution/__tests__/record-store.test.ts +158 -52
  48. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
  49. package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
  50. package/src/execution/__tests__/resource-policy.test.ts +109 -0
  51. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
  52. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
  53. package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
  54. package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
  55. package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
  56. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
  57. package/src/execution/__tests__/session-pending.test.ts +61 -2
  58. package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
  59. package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
  60. package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
  61. package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
  62. package/src/execution/__tests__/spawn-args.test.ts +127 -49
  63. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
  64. package/src/execution/__tests__/spawned-children.test.ts +92 -0
  65. package/src/execution/__tests__/status-refactor.test.ts +345 -0
  66. package/src/execution/__tests__/stdin-writer.test.ts +97 -0
  67. package/src/execution/__tests__/subagent-service-message-close.test.ts +598 -0
  68. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
  69. package/src/execution/__tests__/subagent-service.test.ts +49 -11
  70. package/src/execution/__tests__/timeout-integration.test.ts +27 -13
  71. package/src/execution/__tests__/tool-action.test.ts +12 -10
  72. package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
  73. package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
  74. package/src/execution/__tests__/worktree-manager.test.ts +292 -89
  75. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +13 -12
  76. package/src/execution/argv-mirror.ts +21 -2
  77. package/src/execution/execution-record.ts +126 -9
  78. package/src/execution/finalize-record.ts +90 -13
  79. package/src/execution/host-mode.ts +1 -1
  80. package/src/execution/lifecycle-manager.ts +484 -0
  81. package/src/execution/lifecycle-predicates.ts +65 -0
  82. package/src/execution/manifest-store.ts +61 -16
  83. package/src/execution/model-resolver.ts +26 -5
  84. package/src/execution/notifier.ts +69 -12
  85. package/src/execution/pi-invocation.ts +21 -1
  86. package/src/execution/record-store.ts +554 -107
  87. package/src/execution/session-pending.ts +116 -45
  88. package/src/execution/session-reconstructor.ts +224 -7
  89. package/src/execution/session-runner.ts +290 -75
  90. package/src/execution/sessions-index.ts +304 -0
  91. package/src/execution/stdin-writer.ts +93 -7
  92. package/src/execution/stream-sink.ts +20 -3
  93. package/src/execution/subagent-service.ts +867 -138
  94. package/src/execution/turn-limiter.ts +14 -0
  95. package/src/execution/types.ts +204 -22
  96. package/src/execution/worktree-manager.ts +128 -49
  97. package/src/execution/worktree-registry.ts +13 -2
  98. package/src/index.ts +277 -19
  99. package/src/injectors/subagent-list-injector.ts +26 -8
  100. package/src/injectors/workflow-list-injector.ts +25 -8
  101. package/src/interface/__tests__/subagent-tool-prompt.test.ts +18 -5
  102. package/src/interface/__tests__/tool-render.test.ts +15 -13
  103. package/src/interface/bg-notify-render.ts +32 -8
  104. package/src/interface/command-actions.ts +26 -7
  105. package/src/interface/commands.ts +21 -22
  106. package/src/interface/format.ts +44 -17
  107. package/src/interface/gui-mappers.ts +6 -8
  108. package/src/interface/helpers.ts +170 -10
  109. package/src/interface/list-component.ts +53 -14
  110. package/src/interface/subagent-actions.ts +235 -17
  111. package/src/interface/subagent-tool.ts +82 -17
  112. package/src/interface/subagents.ts +2 -1
  113. package/src/interface/tool-render.ts +11 -24
  114. package/src/interface/tool-workflow.ts +20 -35
  115. package/src/interface/views/WorkflowsView.ts +89 -32
  116. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
  117. package/src/interface/views/__tests__/detail-content-session-file.test.ts +1 -1
  118. package/src/interface/views/format.ts +3 -3
  119. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +325 -0
  120. package/src/orchestration/__tests__/args-validator.test.ts +1 -1
  121. package/src/orchestration/__tests__/config-loader.test.ts +38 -0
  122. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
  123. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
  124. package/src/orchestration/__tests__/execute-agent-call.test.ts +95 -0
  125. package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +657 -18
  126. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
  127. package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
  128. package/src/orchestration/__tests__/skill-discovery.test.ts +157 -0
  129. package/src/orchestration/__tests__/test-mocks.ts +191 -0
  130. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +98 -0
  131. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
  132. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
  133. package/src/orchestration/__tests__/workflows-e2e.test.ts +1 -1
  134. package/src/orchestration/agent-opts-resolver.ts +4 -1
  135. package/src/orchestration/args-validator.ts +2 -2
  136. package/src/orchestration/config-loader.ts +30 -1
  137. package/src/orchestration/error-recovery.ts +133 -29
  138. package/src/orchestration/execute-agent-call.ts +31 -7
  139. package/src/orchestration/jsonl-run-store.ts +287 -40
  140. package/src/orchestration/launcher.ts +7 -1
  141. package/src/orchestration/lifecycle.ts +135 -132
  142. package/src/orchestration/models/__tests__/trace.test.ts +408 -0
  143. package/src/orchestration/models/budget.ts +1 -1
  144. package/src/orchestration/models/run-runtime.ts +15 -17
  145. package/src/orchestration/models/run-spec.ts +2 -2
  146. package/src/orchestration/models/run-state.ts +3 -3
  147. package/src/orchestration/models/trace.ts +95 -15
  148. package/src/orchestration/models/types.ts +8 -9
  149. package/src/orchestration/models/workflow-run.ts +50 -71
  150. package/src/orchestration/models/workflow-script.ts +32 -1
  151. package/src/orchestration/skill-discovery.ts +30 -0
  152. package/src/orchestration/worker-handle.ts +1 -1
  153. package/src/orchestration/worker-host.ts +1 -1
  154. package/src/orchestration/worker-script-builder.ts +29 -10
  155. package/src/shared/__tests__/agent-ref.test.ts +34 -0
  156. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
  157. package/src/shared/__tests__/resource-discovery.test.ts +55 -0
  158. package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
  159. package/src/shared/agent-ref.ts +16 -0
  160. package/src/shared/resource-discovery.ts +147 -58
  161. package/src/shared/schema-jsonify.ts +53 -0
  162. package/workflows/README.md +4 -4
  163. package/agents/code-reviewer.md +0 -47
  164. package/agents/context-builder.md +0 -21
  165. package/agents/oracle.md +0 -34
  166. package/agents/worker.md +0 -20
@@ -3,7 +3,9 @@
3
3
  // git worktree 生命周期管理:创建、清理、patch 回传、孤儿 reaper。
4
4
  //
5
5
  // 设计约束:
6
- // - gitRun 是唯一 git 命令出口,统一超时/错误包装
6
+ // - gitRunAsync 是唯一 git 命令出口,统一超时/错误包装(旧同步 gitRun 已在 phase 2 删除)
7
+ // - gitRunAsync 输出保真(不 trim):diff stdout 直接落盘为 patch,裁掉尾换行会让
8
+ // `git apply` 报 corrupt patch;需要干净文本的消费点(baseCommit/status 拼接)自行 trim
7
9
  // - recordId 白名单 `^[\w-]+$` 防止路径注入
8
10
  // - clean tree 前置校验防止创建脏 worktree
9
11
  // - checkout 放 os.tmpdir()(脱离 .git/),兼容普通 repo 与 bare+worktree 结构
@@ -15,7 +17,7 @@
15
17
  // WorktreeRegistry(<agentDir>/subagents/worktrees.json)。判据从终态 marker
16
18
  // 状态机降为 pid 死活一条——进程崩溃无人写终态时也能正确回收。
17
19
 
18
- import { execFileSync } from "node:child_process";
20
+ import { execFile } from "node:child_process";
19
21
  import * as fs from "node:fs";
20
22
  import * as os from "node:os";
21
23
  import * as path from "node:path";
@@ -36,9 +38,47 @@ const SAFE_ID_RE = /^[\w-]+$/;
36
38
  // 默认 git 命令超时(ms)
37
39
  const GIT_TIMEOUT_MS = 30_000;
38
40
 
41
+ /**
42
+ * gitRunAsync 的包装错误:message 格式与旧同步 gitRun 逐字一致(下游
43
+ * DirtyWorktreeError 判定与测试 toThrow 匹配零改动);exitCode/stderr/timedOut
44
+ * 为新增诊断属性(探针 P-errshape 实测 Node 24:execFile 的 err.stderr 为
45
+ * undefined——stderr 在 callback 第三参;退出码在 err.code(数字))。
46
+ */
47
+ export class GitRunError extends Error {
48
+ readonly exitCode?: number;
49
+ readonly stderr?: string;
50
+ readonly timedOut?: boolean;
51
+
52
+ constructor(
53
+ message: string,
54
+ props: { exitCode?: number; stderr?: string; timedOut?: boolean },
55
+ ) {
56
+ super(message);
57
+ this.name = "GitRunError";
58
+ this.exitCode = props.exitCode;
59
+ this.stderr = props.stderr;
60
+ this.timedOut = props.timedOut;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * 写类 git 命令判定(per-repo mutex 串行对象)。读类(status/rev-parse/diff)
66
+ * 无副作用不加锁——并发读 git 自身安全,P-lock 实测冲突仅是写窗口假设。
67
+ */
68
+ function isWriteCommand(args: string[]): boolean {
69
+ if (args[0] === "worktree") return args[1] === "add" || args[1] === "remove";
70
+ if (args[0] === "branch") return args[1] === "-D";
71
+ return args[0] === "add";
72
+ }
73
+
39
74
  export class WorktreeManager {
40
75
  // 全局注册表:跨 repo 记录所有活 worktree,reaper 遍历此表判孤儿。
41
76
  private readonly registry: WorktreeRegistry;
77
+ // per-repo 写命令串行队列:value = 队尾(已吞 rejection 的)Promise。
78
+ // 入队形态 prev.catch(()=>{}).then(run)——后继只关心「自己已排队」,
79
+ // 不继承前驱错误(否则 1 个 worktree add 失败会传染同 repo 后续全部写命令,
80
+ // 替代旧同步版单线程天然全局串行的「各命令独立失败」语义)。
81
+ private readonly writeQueues = new Map<string, Promise<void>>();
42
82
 
43
83
  constructor(agentDir: string) {
44
84
  this.registry = new WorktreeRegistry(agentDir);
@@ -51,16 +91,33 @@ export class WorktreeManager {
51
91
  * @param recordId 执行记录 ID(必须匹配 `^[\w-]+$`)
52
92
  * @returns 冻结的 WorktreeHandle
53
93
  */
54
- create(mainCwd: string, recordId: string): WorktreeHandle {
94
+ async create(mainCwd: string, recordId: string): Promise<WorktreeHandle> {
55
95
  if (!SAFE_ID_RE.test(recordId)) {
56
96
  throw new DirtyWorktreeError(
57
97
  `recordId contains unsafe characters: "${recordId}" (must match ^[\\w-]+$)`,
58
98
  );
59
99
  }
60
100
 
61
- this.assertCleanTree(mainCwd);
101
+ // 脏树校验与 base commit 并行(读类无锁可并发)。allSettled 而非 all:
102
+ // status 先 reject 时 all 会短路,rev-parse 的后续 rejection 无人处理 →
103
+ // unhandledRejection。判定顺序固定:status 错误 → 脏树 → rev-parse 错误
104
+ // (脏树语义不变:仍先于 worktree add,rev-parse 结果在脏树时被丢弃)。
105
+ const [statusR, revR] = await Promise.allSettled([
106
+ this.gitRunAsync(["status", "--porcelain"], { cwd: mainCwd }),
107
+ this.gitRunAsync(["rev-parse", "HEAD"], { cwd: mainCwd }),
108
+ ]);
109
+ if (statusR.status === "rejected") throw statusR.reason;
110
+ // 消费点自行 trim:gitRunAsync 返回原始 stdout(保真),status 输出以 \n 结尾
111
+ const statusText = statusR.value.trim();
112
+ if (statusText.length > 0) {
113
+ throw new DirtyWorktreeError(
114
+ `Working tree is dirty in ${mainCwd}:\n${statusText}`,
115
+ );
116
+ }
117
+ if (revR.status === "rejected") throw revR.reason;
118
+ // 消费点自行 trim:rev-parse 输出 "hash\n",不 trim 会把换行带进后续 git args
119
+ const baseCommit = revR.value.trim();
62
120
 
63
- const baseCommit = this.gitRun(["rev-parse", "HEAD"], { cwd: mainCwd });
64
121
  const branch = `pi-sub-${recordId}`;
65
122
  // checkout 放 tmpdir,脱离 .git/ 目录结构。
66
123
  // 这样 git 自行把元数据注册到 <commonDir>/worktrees/<branch>/,
@@ -79,7 +136,7 @@ export class WorktreeManager {
79
136
  }
80
137
  }
81
138
 
82
- this.gitRun(["worktree", "add", "-b", branch, worktreePath, "HEAD"], {
139
+ await this.gitRunAsync(["worktree", "add", "-b", branch, worktreePath, "HEAD"], {
83
140
  cwd: mainCwd,
84
141
  });
85
142
 
@@ -112,12 +169,12 @@ export class WorktreeManager {
112
169
  } catch (err) {
113
170
  // 回滚已创建的 worktree+分支+注册表条目,best-effort 吞清理异常(原始 err 仍外抛)
114
171
  try {
115
- this.gitRun(["worktree", "remove", "--force", worktreePath], { cwd: mainCwd });
172
+ await this.gitRunAsync(["worktree", "remove", "--force", worktreePath], { cwd: mainCwd });
116
173
  } catch (cleanErr) {
117
174
  bestEffort(cleanErr, "worktree remove (create rollback MF#3)");
118
175
  }
119
176
  try {
120
- this.gitRun(["branch", "-D", branch], { cwd: mainCwd });
177
+ await this.gitRunAsync(["branch", "-D", branch], { cwd: mainCwd });
121
178
  } catch (cleanErr) {
122
179
  bestEffort(cleanErr, "branch delete (create rollback MF#3)");
123
180
  }
@@ -130,9 +187,10 @@ export class WorktreeManager {
130
187
  * 注册子进程 pid(runSpawn spawn() 返回后同步调)。
131
188
  * create 时 pid 未知写 0 占位,子进程 spawn 返回后(child.pid 同步可得)由此补全。
132
189
  * reaper 据 pid 死活判孤儿,pid=0 条目用 SPAWN_GRACE 宽限。
190
+ * sessionFile 可选补全:传入时填入 registry entry(reaper 据 pid 死活判孤儿,不读本字段;保留供诊断)。
133
191
  */
134
- registerPid(branch: string, pid: number): void {
135
- this.registry.updatePid(branch, pid);
192
+ registerPid(branch: string, pid: number, sessionFile?: string): void {
193
+ this.registry.updatePid(branch, pid, sessionFile);
136
194
  }
137
195
 
138
196
  /**
@@ -142,9 +200,9 @@ export class WorktreeManager {
142
200
  *
143
201
  * @param handle 要清理的 worktree handle(含 mainCwd,不靠路径反推)
144
202
  */
145
- cleanup(handle: WorktreeHandle): void {
203
+ async cleanup(handle: WorktreeHandle): Promise<void> {
146
204
  try {
147
- this.gitRun(["worktree", "remove", "--force", handle.path], {
205
+ await this.gitRunAsync(["worktree", "remove", "--force", handle.path], {
148
206
  cwd: handle.mainCwd,
149
207
  });
150
208
  } catch (err) {
@@ -152,7 +210,7 @@ export class WorktreeManager {
152
210
  }
153
211
 
154
212
  try {
155
- this.gitRun(["branch", "-D", handle.branch], {
213
+ await this.gitRunAsync(["branch", "-D", handle.branch], {
156
214
  cwd: handle.mainCwd,
157
215
  });
158
216
  } catch (err) {
@@ -176,15 +234,15 @@ export class WorktreeManager {
176
234
  * written=true 仅当 diff 非空且写盘成功;空 diff 或写失败均 written=false,
177
235
  * 调用方据此回填 record.patchFile,避免悬空路径(`git apply` 不存在的文件)。
178
236
  */
179
- collectPatch(handle: WorktreeHandle, patchFile: string): PatchResult {
237
+ async collectPatch(handle: WorktreeHandle, patchFile: string): Promise<PatchResult> {
180
238
  // git add -A:暂存全部改动(含未跟踪新文件),使后续 --cached diff 能捕获新建文件
181
239
  try {
182
- this.gitRun(["add", "-A"], { cwd: handle.path });
240
+ await this.gitRunAsync(["add", "-A"], { cwd: handle.path });
183
241
  } catch (err) {
184
242
  // add 失败不致命:继续尝试 diff,最差得到部分 diff(仅已跟踪文件的改动)
185
243
  bestEffort(err, "git add -A (collectPatch)");
186
244
  }
187
- const diff = this.gitRun(
245
+ const diff = await this.gitRunAsync(
188
246
  ["diff", "--cached", handle.baseCommit],
189
247
  { cwd: handle.path },
190
248
  );
@@ -214,19 +272,22 @@ export class WorktreeManager {
214
272
  * pid == 0 且超 SPAWN_GRACE_MS → 孤儿(create 后崩溃,pid 永未补全)
215
273
  * pid == 0 且未超宽限 → 跳过(可能正在 spawn)
216
274
  */
217
- scan(): void {
275
+ async scan(): Promise<void> {
218
276
  const entries = this.registry.load();
219
277
  const now = Date.now();
220
278
 
279
+ // 逐孤儿串行 await(保持 for 循环串行语义,防止一次 reaper 打出 N 个并发 git)
221
280
  for (const entry of entries) {
222
281
  if (!this.isOrphan(entry, now)) {
223
282
  continue;
224
283
  }
225
- this.cleanupOrphan(entry);
284
+ await this.cleanupOrphan(entry);
226
285
  }
227
286
  }
228
287
 
229
- /** pid 死活判孤儿。pid=0 走 SPAWN_GRACE 宽限。 */
288
+ /**
289
+ * 判孤儿:pid 死活为主判据。pid=0 走 SPAWN_GRACE 宽限(create→spawn 窗口)。
290
+ */
230
291
  private isOrphan(entry: WorktreeEntry, now: number): boolean {
231
292
  if (entry.pid === 0) {
232
293
  // create→spawn 窗口:超过宽限期仍未补 pid = create 后崩溃
@@ -246,14 +307,14 @@ export class WorktreeManager {
246
307
  }
247
308
 
248
309
  /** 清理单个孤儿条目:worktree remove + branch -D + 注册表移除,三步各自 best-effort。 */
249
- private cleanupOrphan(entry: WorktreeEntry): void {
310
+ private async cleanupOrphan(entry: WorktreeEntry): Promise<void> {
250
311
  try {
251
- this.gitRun(["worktree", "remove", "--force", entry.checkout], { cwd: entry.repo });
312
+ await this.gitRunAsync(["worktree", "remove", "--force", entry.checkout], { cwd: entry.repo });
252
313
  } catch (err) {
253
314
  bestEffort(err, "worktree remove (orphan reaper)");
254
315
  }
255
316
  try {
256
- this.gitRun(["branch", "-D", entry.branch], { cwd: entry.repo });
317
+ await this.gitRunAsync(["branch", "-D", entry.branch], { cwd: entry.repo });
257
318
  } catch (err) {
258
319
  bestEffort(err, "branch delete (orphan reaper)");
259
320
  }
@@ -265,34 +326,52 @@ export class WorktreeManager {
265
326
  // ============================================================
266
327
 
267
328
  /**
268
- * git 命令执行器。统一超时 + 错误包装。
329
+ * git 命令异步执行器。与 gitRun 同一超时/错误包装约定(message 格式逐字一致),
330
+ * 差异仅在错误属性形态(GitRunError 挂 exitCode/stderr/timedOut)。
331
+ * 写类命令经 per-repo mutex 串行(不依赖 git 锁实现细节 + 并发限流 + 行为确定性)。
332
+ *
333
+ * stdout 保真返回(不 trim):collectPatch 把 diff 输出原样落盘为 patch 文件,
334
+ * 裁掉尾换行会产出 `git apply` 拒绝的 corrupt patch(2026-08-16 门 4 实测)。
335
+ * 需要干净文本的消费点(baseCommit / 脏树 status 拼接)自行 trim。
269
336
  */
270
- private gitRun(args: string[], opts: { cwd: string; timeout?: number }): string {
271
- try {
272
- return execFileSync("git", args, {
273
- cwd: opts.cwd,
274
- timeout: opts.timeout ?? GIT_TIMEOUT_MS,
275
- encoding: "utf-8",
276
- stdio: ["pipe", "pipe", "pipe"],
277
- }).trim();
278
- } catch (err: unknown) {
279
- if (err instanceof Error) {
280
- throw new Error(`git ${args[0]} failed: ${err.message}`);
281
- }
282
- throw new Error(`git ${args[0]} failed: unknown error`);
283
- }
284
- }
337
+ private async gitRunAsync(args: string[], opts: { cwd: string; timeout?: number }): Promise<string> {
338
+ const run = (): Promise<string> =>
339
+ new Promise((resolve, reject) => {
340
+ execFile(
341
+ "git",
342
+ args,
343
+ { cwd: opts.cwd, timeout: opts.timeout ?? GIT_TIMEOUT_MS, encoding: "utf-8" },
344
+ (err, stdout, stderr) => {
345
+ if (err) {
346
+ const execErr = err as Error & { code?: unknown; killed?: boolean; signal?: string };
347
+ reject(
348
+ new GitRunError(`git ${args[0]} failed: ${execErr.message}`, {
349
+ // P-errshape 实测:execFile 退出码在 err.code(数字时);超时 killed+SIGTERM
350
+ exitCode: typeof execErr.code === "number" ? execErr.code : undefined,
351
+ stderr: typeof stderr === "string" ? stderr : undefined,
352
+ timedOut: execErr.killed === true && execErr.signal === "SIGTERM",
353
+ }),
354
+ );
355
+ return;
356
+ }
357
+ resolve(stdout);
358
+ },
359
+ );
360
+ });
285
361
 
286
- /**
287
- * 校验工作目录是 clean tree。
288
- */
289
- private assertCleanTree(cwd: string): void {
290
- const status = this.gitRun(["status", "--porcelain"], { cwd });
291
- if (status.length > 0) {
292
- throw new DirtyWorktreeError(
293
- `Working tree is dirty in ${cwd}:\n${status}`,
294
- );
295
- }
362
+ if (!isWriteCommand(args)) return run();
363
+ // per-repo 队列:吞前驱 rejection 后接续本命令;队尾比对自清理防 Map 泄漏
364
+ const repo = opts.cwd;
365
+ const prev = this.writeQueues.get(repo) ?? Promise.resolve();
366
+ const next = prev.catch(() => {}).then(run);
367
+ const tail: Promise<void> = next.then(
368
+ () => undefined,
369
+ () => undefined,
370
+ );
371
+ this.writeQueues.set(repo, tail);
372
+ void tail.finally(() => {
373
+ if (this.writeQueues.get(repo) === tail) this.writeQueues.delete(repo);
374
+ });
375
+ return next;
296
376
  }
297
-
298
377
  }
@@ -56,6 +56,12 @@ export interface WorktreeEntry {
56
56
  readonly pid: number;
57
57
  /** 创建时间戳(ms,SPAWN_GRACE 判据 + 调试用)。 */
58
58
  readonly createdAt: number;
59
+ /**
60
+ * 对应 subagent session jsonl 文件全路径(诊断/兼容字段,reaper 据 pid 死活判孤儿)。
61
+ * session-runner first header 拿到 pid 时补全(create 时 record.sessionFile 尚未确定)。
62
+ * 可选:旧 worktrees.json / 非 worktree 模式无此字段,向后兼容(undefined 时 reaper 走原 pid 判据)。
63
+ */
64
+ readonly sessionFile?: string;
59
65
  }
60
66
 
61
67
  /**
@@ -89,12 +95,17 @@ export class WorktreeRegistry {
89
95
  /**
90
96
  * 更新 pid(runSpawn spawn() 返回后同步调)。
91
97
  * branch 不存在则忽略(create 后崩溃 + reaper 已清的竞态)。
98
+ * sessionFile 可选补全:传入时填入 entry(reaper 据 pid 死活判孤儿,不读本字段)。
92
99
  */
93
- updatePid(branch: string, pid: number): void {
100
+ updatePid(branch: string, pid: number, sessionFile?: string): void {
94
101
  const entries = this.load();
95
102
  const idx = entries.findIndex((e) => e.branch === branch);
96
103
  if (idx >= 0) {
97
- entries[idx] = { ...entries[idx], pid };
104
+ entries[idx] = {
105
+ ...entries[idx],
106
+ pid,
107
+ ...(sessionFile !== undefined ? { sessionFile } : {}),
108
+ };
98
109
  this.save(entries, { branch, pid });
99
110
  }
100
111
  }