@zhushanwen/subagent-core 0.2.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 (330) hide show
  1. package/README.md +114 -0
  2. package/dist/chunk-3VOERJPJ.js +22 -0
  3. package/dist/chunk-A4IVZWVX.js +31 -0
  4. package/dist/execution/engine/engines/zcode/constants.cjs +54 -0
  5. package/dist/execution/engine/engines/zcode/constants.d.cts +31 -0
  6. package/dist/execution/engine/engines/zcode/constants.d.ts +31 -0
  7. package/dist/execution/engine/engines/zcode/constants.js +22 -0
  8. package/dist/execution/engine/engines/zcode/reader.cjs +276 -0
  9. package/dist/execution/engine/engines/zcode/reader.d.cts +20 -0
  10. package/dist/execution/engine/engines/zcode/reader.d.ts +20 -0
  11. package/dist/execution/engine/engines/zcode/reader.js +239 -0
  12. package/dist/execution/engine/paths.cjs +55 -0
  13. package/dist/execution/engine/paths.d.cts +8 -0
  14. package/dist/execution/engine/paths.d.ts +8 -0
  15. package/dist/execution/engine/paths.js +26 -0
  16. package/dist/execution/relay-env.cjs +62 -0
  17. package/dist/execution/relay-env.d.cts +33 -0
  18. package/dist/execution/relay-env.d.ts +33 -0
  19. package/dist/execution/relay-env.js +20 -0
  20. package/dist/index.cjs +1852 -0
  21. package/dist/index.d.cts +1184 -0
  22. package/dist/index.d.ts +1184 -0
  23. package/dist/index.js +1810 -0
  24. package/dist/types-BxyAidGf.d.cts +875 -0
  25. package/dist/types-BxyAidGf.d.ts +875 -0
  26. package/package.json +99 -0
  27. package/src/__tests__/agent-opts-resolver-schema-prompt.test.ts +142 -0
  28. package/src/__tests__/append-system-prompt-assembly.test.ts +242 -0
  29. package/src/__tests__/builtin-workflows-structure.test.ts +180 -0
  30. package/src/__tests__/derive-closed-display-parity.test.ts +93 -0
  31. package/src/__tests__/fr4-get-state-handshake.test.ts +125 -0
  32. package/src/__tests__/fr8-orphan-recovery.test.ts +259 -0
  33. package/src/__tests__/m2-append-content-probe.test.ts +73 -0
  34. package/src/__tests__/manifest-store.test.ts +368 -0
  35. package/src/__tests__/record-store-cache.test.ts +335 -0
  36. package/src/__tests__/record-store-index.test.ts +560 -0
  37. package/src/__tests__/record-store-last-line.test.ts +129 -0
  38. package/src/__tests__/review-fix-loop-script.test.ts +288 -0
  39. package/src/__tests__/review-fix-loop-utils.test.ts +2377 -0
  40. package/src/__tests__/robustness-low-batch1.test.ts +286 -0
  41. package/src/__tests__/robustness-medium-batch1.test.ts +69 -0
  42. package/src/__tests__/robustness-medium-batch2.test.ts +65 -0
  43. package/src/__tests__/robustness-medium-batch3.test.ts +30 -0
  44. package/src/__tests__/robustness-medium-batch4.test.ts +358 -0
  45. package/src/__tests__/session-runner.test.ts +69 -0
  46. package/src/__tests__/smoke.test.ts +9 -0
  47. package/src/__tests__/stream-sink.test.ts +203 -0
  48. package/src/core/__tests__/host-services.test.ts +144 -0
  49. package/src/core/__tests__/logger.test.ts +78 -0
  50. package/src/core/__tests__/notify-ports.test.ts +104 -0
  51. package/src/core/host-services.ts +97 -0
  52. package/src/core/logger.ts +47 -0
  53. package/src/core/notify-ports.ts +126 -0
  54. package/src/execution/__tests__/__fixtures__/notifier-golden-snapshots.json +53 -0
  55. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
  56. package/src/execution/__tests__/agent-result-mapper.test.ts +150 -0
  57. package/src/execution/__tests__/alive-store.test.ts +147 -0
  58. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +490 -0
  59. package/src/execution/__tests__/channel-registry-handshake.test.ts +242 -0
  60. package/src/execution/__tests__/chat-engine-routing.test.ts +610 -0
  61. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
  62. package/src/execution/__tests__/concurrency-pool.test.ts +250 -0
  63. package/src/execution/__tests__/config.test.ts +302 -0
  64. package/src/execution/__tests__/delivery-methods.test.ts +424 -0
  65. package/src/execution/__tests__/dialog-queue.test.ts +299 -0
  66. package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
  67. package/src/execution/__tests__/execute-and-await-worktree.test.ts +243 -0
  68. package/src/execution/__tests__/execute-nesting.test.ts +323 -0
  69. package/src/execution/__tests__/execute-options-mapper.test.ts +200 -0
  70. package/src/execution/__tests__/execution-record.test.ts +1394 -0
  71. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +171 -0
  72. package/src/execution/__tests__/finalize-record.test.ts +367 -0
  73. package/src/execution/__tests__/finalized-marker.test.ts +104 -0
  74. package/src/execution/__tests__/format-schema-instruction.test.ts +169 -0
  75. package/src/execution/__tests__/gc-timer.test.ts +184 -0
  76. package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
  77. package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
  78. package/src/execution/__tests__/helpers/spawn-mock.ts +242 -0
  79. package/src/execution/__tests__/host-mode.test.ts +87 -0
  80. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
  81. package/src/execution/__tests__/lifecycle-manager.test.ts +383 -0
  82. package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
  83. package/src/execution/__tests__/manifest-parentid.test.ts +117 -0
  84. package/src/execution/__tests__/model-resolver.test.ts +465 -0
  85. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
  86. package/src/execution/__tests__/nested-visibility.test.ts +325 -0
  87. package/src/execution/__tests__/output-collector.test.ts +358 -0
  88. package/src/execution/__tests__/path-encoding.test.ts +104 -0
  89. package/src/execution/__tests__/pi-invocation.test.ts +134 -0
  90. package/src/execution/__tests__/record-store.test.ts +1057 -0
  91. package/src/execution/__tests__/records-cwd-isolation.test.ts +91 -0
  92. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +339 -0
  93. package/src/execution/__tests__/recursive-visibility-env.test.ts +273 -0
  94. package/src/execution/__tests__/relay-env.test.ts +42 -0
  95. package/src/execution/__tests__/resource-policy.test.ts +109 -0
  96. package/src/execution/__tests__/rpc-mode.test.ts +89 -0
  97. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
  98. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
  99. package/src/execution/__tests__/run-spawn-edges.test.ts +687 -0
  100. package/src/execution/__tests__/run-spawn-integration.test.ts +933 -0
  101. package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
  102. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +196 -0
  103. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +199 -0
  104. package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
  105. package/src/execution/__tests__/session-file-gc.test.ts +293 -0
  106. package/src/execution/__tests__/session-reconstructor.test.ts +379 -0
  107. package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
  108. package/src/execution/__tests__/session-runner-schema-env.test.ts +350 -0
  109. package/src/execution/__tests__/spawn-args.test.ts +445 -0
  110. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +189 -0
  111. package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
  112. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +207 -0
  113. package/src/execution/__tests__/spawned-children.test.ts +92 -0
  114. package/src/execution/__tests__/start-sync-model-guard.test.ts +150 -0
  115. package/src/execution/__tests__/stdin-writer.test.ts +462 -0
  116. package/src/execution/__tests__/stream-sink-retirement.test.ts +261 -0
  117. package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
  118. package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
  119. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
  120. package/src/execution/__tests__/subagent-service.test.ts +788 -0
  121. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +310 -0
  122. package/src/execution/__tests__/subprocess-agent-runner.test.ts +612 -0
  123. package/src/execution/__tests__/temp-prompt.test.ts +53 -0
  124. package/src/execution/__tests__/timeout-integration.test.ts +615 -0
  125. package/src/execution/__tests__/tombstone-store.test.ts +73 -0
  126. package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
  127. package/src/execution/__tests__/turn-limiter.test.ts +65 -0
  128. package/src/execution/__tests__/ui-channels.test.ts +187 -0
  129. package/src/execution/__tests__/ui-interaction-model.test.ts +67 -0
  130. package/src/execution/__tests__/ui-request-handler-factory.test.ts +369 -0
  131. package/src/execution/__tests__/ui-request-handler.test.ts +204 -0
  132. package/src/execution/__tests__/ui-request-observability.test.ts +113 -0
  133. package/src/execution/__tests__/ui-request-queue.test.ts +133 -0
  134. package/src/execution/__tests__/worktree-manager.test.ts +633 -0
  135. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +229 -0
  136. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
  137. package/src/execution/__tests__/worktree-registry.test.ts +199 -0
  138. package/src/execution/agent-registry.ts +199 -0
  139. package/src/execution/agent-result-mapper.ts +90 -0
  140. package/src/execution/alive-store.ts +119 -0
  141. package/src/execution/argv-mirror.ts +113 -0
  142. package/src/execution/best-effort.ts +38 -0
  143. package/src/execution/channel-registry-access.ts +144 -0
  144. package/src/execution/concurrency-pool.ts +116 -0
  145. package/src/execution/config.ts +165 -0
  146. package/src/execution/dialog-queue.ts +330 -0
  147. package/src/execution/engine/__tests__/common/data-dir.test.ts +78 -0
  148. package/src/execution/engine/__tests__/common/errors.test.ts +132 -0
  149. package/src/execution/engine/__tests__/common/event-journal.test.ts +177 -0
  150. package/src/execution/engine/__tests__/common/kill-chain.test.ts +192 -0
  151. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +81 -0
  152. package/src/execution/engine/__tests__/common/persona-router.test.ts +123 -0
  153. package/src/execution/engine/__tests__/common/pool-manager.test.ts +154 -0
  154. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +128 -0
  155. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +28 -0
  156. package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +141 -0
  157. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +109 -0
  158. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +101 -0
  159. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +77 -0
  160. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +104 -0
  161. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +201 -0
  162. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +76 -0
  163. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +79 -0
  164. package/src/execution/engine/__tests__/engine-discovery.test.ts +87 -0
  165. package/src/execution/engine/__tests__/model-prompt.test.ts +197 -0
  166. package/src/execution/engine/__tests__/paths.test.ts +39 -0
  167. package/src/execution/engine/__tests__/registry.test.ts +120 -0
  168. package/src/execution/engine/__tests__/routing.test.ts +231 -0
  169. package/src/execution/engine/common/data-dir.ts +66 -0
  170. package/src/execution/engine/common/errors.ts +183 -0
  171. package/src/execution/engine/common/event-journal.ts +254 -0
  172. package/src/execution/engine/common/journal-replay.ts +62 -0
  173. package/src/execution/engine/common/kill-chain.ts +221 -0
  174. package/src/execution/engine/common/nesting-guard.ts +50 -0
  175. package/src/execution/engine/common/persona-router.ts +108 -0
  176. package/src/execution/engine/common/pool-manager.ts +226 -0
  177. package/src/execution/engine/common/schema-emulation.ts +189 -0
  178. package/src/execution/engine/common/session-view-projection.ts +51 -0
  179. package/src/execution/engine/engine-discovery.ts +65 -0
  180. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +469 -0
  181. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +155 -0
  182. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +164 -0
  183. package/src/execution/engine/engines/pi/pi-engine.ts +415 -0
  184. package/src/execution/engine/engines/pi/reader.ts +48 -0
  185. package/src/execution/engine/engines/pi/registration.ts +35 -0
  186. package/src/execution/engine/engines/pi/task-spec-mapper.ts +100 -0
  187. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +39 -0
  188. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +150 -0
  189. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +246 -0
  190. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +228 -0
  191. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +210 -0
  192. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +71 -0
  193. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +127 -0
  194. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +580 -0
  195. package/src/execution/engine/engines/zcode/constants.ts +43 -0
  196. package/src/execution/engine/engines/zcode/golden-sample.ts +39 -0
  197. package/src/execution/engine/engines/zcode/launcher.ts +161 -0
  198. package/src/execution/engine/engines/zcode/parser.ts +436 -0
  199. package/src/execution/engine/engines/zcode/preparer.ts +363 -0
  200. package/src/execution/engine/engines/zcode/reader.ts +381 -0
  201. package/src/execution/engine/engines/zcode/registration.ts +37 -0
  202. package/src/execution/engine/engines/zcode/zcode-engine.ts +658 -0
  203. package/src/execution/engine/host-task-spec.ts +47 -0
  204. package/src/execution/engine/model-prompt.ts +158 -0
  205. package/src/execution/engine/paths.ts +42 -0
  206. package/src/execution/engine/port.ts +153 -0
  207. package/src/execution/engine/registry.ts +133 -0
  208. package/src/execution/engine/routing.ts +218 -0
  209. package/src/execution/engine/types.ts +309 -0
  210. package/src/execution/execute-options-mapper.ts +115 -0
  211. package/src/execution/execution-record.ts +984 -0
  212. package/src/execution/finalize-record.ts +254 -0
  213. package/src/execution/finalized-marker.ts +70 -0
  214. package/src/execution/get-state-handshake.ts +104 -0
  215. package/src/execution/host-mode.ts +56 -0
  216. package/src/execution/idle-gc.ts +47 -0
  217. package/src/execution/lifecycle-manager.ts +513 -0
  218. package/src/execution/lifecycle-predicates.ts +65 -0
  219. package/src/execution/manifest-store.ts +256 -0
  220. package/src/execution/model-config-service.ts +250 -0
  221. package/src/execution/model-resolver.ts +248 -0
  222. package/src/execution/notifier.ts +372 -0
  223. package/src/execution/notify-ledger.ts +580 -0
  224. package/src/execution/output-collector.ts +228 -0
  225. package/src/execution/path-encoding.ts +60 -0
  226. package/src/execution/pi-invocation.ts +120 -0
  227. package/src/execution/record-entry.ts +132 -0
  228. package/src/execution/record-store.ts +1265 -0
  229. package/src/execution/relay-env.ts +37 -0
  230. package/src/execution/session-context-resolver.ts +64 -0
  231. package/src/execution/session-file-gc.ts +120 -0
  232. package/src/execution/session-pending.ts +170 -0
  233. package/src/execution/session-reconstructor.ts +678 -0
  234. package/src/execution/session-runner.ts +1789 -0
  235. package/src/execution/sessions-index.ts +304 -0
  236. package/src/execution/spawn-event-adapter.ts +363 -0
  237. package/src/execution/stdin-writer.ts +198 -0
  238. package/src/execution/stream-sink.ts +126 -0
  239. package/src/execution/subagent-service.ts +2268 -0
  240. package/src/execution/subprocess-agent-runner.ts +317 -0
  241. package/src/execution/temp-prompt.ts +62 -0
  242. package/src/execution/tombstone-store.ts +72 -0
  243. package/src/execution/turn-limiter.ts +102 -0
  244. package/src/execution/types.ts +1023 -0
  245. package/src/execution/ui-channels.ts +216 -0
  246. package/src/execution/ui-interaction-model.ts +48 -0
  247. package/src/execution/ui-request-handler-factory.ts +243 -0
  248. package/src/execution/ui-request-observability.ts +81 -0
  249. package/src/execution/ui-request-queue.ts +184 -0
  250. package/src/execution/worktree-manager.ts +688 -0
  251. package/src/execution/worktree-registry.ts +279 -0
  252. package/src/index.ts +87 -0
  253. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +349 -0
  254. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +202 -0
  255. package/src/orchestration/__tests__/agent-call-stream.test.ts +152 -0
  256. package/src/orchestration/__tests__/args-validator.test.ts +143 -0
  257. package/src/orchestration/__tests__/config-loader.test.ts +503 -0
  258. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +809 -0
  259. package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +404 -0
  260. package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +56 -0
  261. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
  262. package/src/orchestration/__tests__/execute-agent-call.test.ts +451 -0
  263. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +600 -0
  264. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
  265. package/src/orchestration/__tests__/lifecycle.test.ts +659 -0
  266. package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +95 -0
  267. package/src/orchestration/__tests__/review-fix-loop-scriptpath-failfast.test.ts +183 -0
  268. package/src/orchestration/__tests__/script-lint.test.ts +831 -0
  269. package/src/orchestration/__tests__/skill-discovery.test.ts +210 -0
  270. package/src/orchestration/__tests__/test-mocks.ts +197 -0
  271. package/src/orchestration/__tests__/worker-exit-without-result.test.ts +368 -0
  272. package/src/orchestration/__tests__/worker-host.test.ts +120 -0
  273. package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
  274. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +563 -0
  275. package/src/orchestration/__tests__/worker-script-builder.test.ts +309 -0
  276. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +129 -0
  277. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +317 -0
  278. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
  279. package/src/orchestration/agent-opts-resolver.ts +174 -0
  280. package/src/orchestration/args-validator.ts +127 -0
  281. package/src/orchestration/config-loader.ts +316 -0
  282. package/src/orchestration/error-recovery.ts +1018 -0
  283. package/src/orchestration/execute-agent-call.ts +256 -0
  284. package/src/orchestration/launcher.ts +455 -0
  285. package/src/orchestration/lifecycle.ts +399 -0
  286. package/src/orchestration/models/__tests__/budget.test.ts +307 -0
  287. package/src/orchestration/models/agent-call.ts +83 -0
  288. package/src/orchestration/models/budget.ts +118 -0
  289. package/src/orchestration/models/ports.ts +164 -0
  290. package/src/orchestration/models/run-runtime.ts +104 -0
  291. package/src/orchestration/models/run-spec.ts +83 -0
  292. package/src/orchestration/models/run-state.ts +44 -0
  293. package/src/orchestration/models/trace.ts +183 -0
  294. package/src/orchestration/models/types.ts +301 -0
  295. package/src/orchestration/models/workflow-run.ts +254 -0
  296. package/src/orchestration/models/workflow-script-registry.ts +35 -0
  297. package/src/orchestration/models/workflow-script.ts +117 -0
  298. package/src/orchestration/script-lint.ts +766 -0
  299. package/src/orchestration/skill-discovery.ts +135 -0
  300. package/src/orchestration/worker-handle.ts +115 -0
  301. package/src/orchestration/worker-host.ts +98 -0
  302. package/src/orchestration/worker-script-builder.ts +421 -0
  303. package/src/orchestration/workflow-files.ts +85 -0
  304. package/src/orchestration/workflow-script-registry-impl.ts +133 -0
  305. package/src/shared/__tests__/agent-ref.test.ts +34 -0
  306. package/src/shared/__tests__/meta-parser.test.ts +304 -0
  307. package/src/shared/__tests__/model-ref.test.ts +306 -0
  308. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +288 -0
  309. package/src/shared/__tests__/resource-discovery.test.ts +749 -0
  310. package/src/shared/__tests__/resource-meta.test.ts +51 -0
  311. package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
  312. package/src/shared/__tests__/timer-delay.test.ts +61 -0
  313. package/src/shared/agent-event.ts +13 -0
  314. package/src/shared/agent-ref.ts +57 -0
  315. package/src/shared/meta-parser.ts +261 -0
  316. package/src/shared/model-ref.ts +286 -0
  317. package/src/shared/resource-discovery.ts +835 -0
  318. package/src/shared/resource-meta.ts +65 -0
  319. package/src/shared/schema-env.ts +44 -0
  320. package/src/shared/schema-jsonify.ts +58 -0
  321. package/src/shared/timer-delay.ts +54 -0
  322. package/src/shared/xml-injection.ts +35 -0
  323. package/workflows/README.md +81 -0
  324. package/workflows/_shared/agent-refs.cjs +40 -0
  325. package/workflows/chain.js +137 -0
  326. package/workflows/map-reduce.js +180 -0
  327. package/workflows/parallel.js +154 -0
  328. package/workflows/review-fix-loop-utils.cjs +1542 -0
  329. package/workflows/review-fix-loop.js +1605 -0
  330. package/workflows/scatter-gather.js +184 -0
@@ -0,0 +1,688 @@
1
+ // src/runtime/worktree-manager.ts
2
+ //
3
+ // git worktree 生命周期管理:创建、清理、patch 回传、孤儿 reaper。
4
+ //
5
+ // 设计约束:
6
+ // - gitRunAsync 是唯一 git 命令出口,统一超时/错误包装(旧同步 gitRun 已在 phase 2 删除)
7
+ // - gitRunAsync 输出保真(不 trim):diff stdout 直接落盘为 patch,裁掉尾换行会让
8
+ // `git apply` 报 corrupt patch;需要干净文本的消费点(baseCommit/status 拼接)自行 trim
9
+ // - recordId 白名单 `^[\w-]+$` 防止路径注入
10
+ // - clean tree 前置校验防止创建脏 worktree
11
+ // - checkout 放 os.tmpdir()(脱离 .git/),兼容普通 repo 与 bare+worktree 结构
12
+ // - mainCwd 存入 handle,不靠路径反推
13
+ // - scan 遍历全局注册表按 pid 死活判孤儿(绝不删有活进程的 worktree)
14
+ // - Object.freeze 保证 WorktreeHandle 不可变
15
+ //
16
+ // [全局注册表重构] scan 不再依赖当前 cwd 是否 git repo,改为遍历
17
+ // WorktreeRegistry(<agentDir>/subagents/worktrees.json)。判据从终态 marker
18
+ // 状态机降为 pid 死活一条——进程崩溃无人写终态时也能正确回收。
19
+ //
20
+ // [D5b 对账] scan 末尾追加双向 diff(reconcileWithPhysical):物理面(tmpdir
21
+ // checkout 目录 + git branch --list)与注册表互相对账——注册表条目丢失(锁前
22
+ // last-write-wins 遗留 / 锁降级窗口)或物理资源被外部清掉时收敛,注册表注释
23
+ // 声称的「tmpdir + 分支对账兜底」由此成为代码。
24
+
25
+ import { execFile } from "node:child_process";
26
+ import * as fs from "node:fs";
27
+ import * as os from "node:os";
28
+ import * as path from "node:path";
29
+
30
+ import { encodeCwd } from "./path-encoding.ts";
31
+ import type { PatchResult,WorktreeHandle } from "./types.ts";
32
+ import { DirtyWorktreeError } from "./types.ts";
33
+ import { bestEffort } from "./best-effort.ts";
34
+ import { getLogger } from "../core/logger.ts";
35
+ import { isProcessAlive, readAliveMarker } from "./alive-store.ts";
36
+ import { SPAWN_GRACE_MS,type WorktreeEntry,WorktreeRegistry } from "./worktree-registry.ts";
37
+
38
+ const logger = getLogger("subagents");
39
+
40
+ // recordId 白名单:字母数字下划线短横线
41
+ const SAFE_ID_RE = /^[\w-]+$/;
42
+
43
+ // 默认 git 命令超时(ms)
44
+ const GIT_TIMEOUT_MS = 30_000;
45
+
46
+ /** tmpdir 下的 worktree 根目录(与 create() 的路径拼装保持同源)。 */
47
+ const WORKTREE_TMP_ROOT = "pi-subagents";
48
+
49
+ /** 分支名前缀(create() 生成 `pi-sub-<recordId>`)。 */
50
+ const BRANCH_PREFIX = "pi-sub-";
51
+
52
+ /**
53
+ * 物理面发现的 worktree(tmpdir checkout 目录存在,无论注册表是否登记)。
54
+ * repo 从 checkout/.git 指针文件推导(普通 repo 与 bare+worktree 均覆盖);
55
+ * 推导失败(.git 文件缺失/损坏)时 undefined——checkout 视为无主残留。
56
+ */
57
+ interface PhysicalWorktree {
58
+ /** encodeCwd(mainCwd) 段名(checkout 路径中间层)。 */
59
+ readonly enc: string;
60
+ /** 分支名(checkout 目录名,= pi-sub-<recordId>)。 */
61
+ readonly branch: string;
62
+ /** checkout 绝对路径。 */
63
+ readonly checkout: string;
64
+ /** 推导出的主仓库路径(.git 指针解析失败则 undefined)。 */
65
+ readonly repo?: string;
66
+ /** checkout 目录 mtime(对账 SPAWN_GRACE 判据的 createdAt 近似)。 */
67
+ readonly mtimeMs: number;
68
+ }
69
+
70
+ /**
71
+ * gitRunAsync 的包装错误:message 格式与旧同步 gitRun 逐字一致(下游
72
+ * DirtyWorktreeError 判定与测试 toThrow 匹配零改动);exitCode/stderr/timedOut
73
+ * 为新增诊断属性(探针 P-errshape 实测 Node 24:execFile 的 err.stderr 为
74
+ * undefined——stderr 在 callback 第三参;退出码在 err.code(数字))。
75
+ */
76
+ export class GitRunError extends Error {
77
+ readonly exitCode?: number;
78
+ readonly stderr?: string;
79
+ readonly timedOut?: boolean;
80
+
81
+ constructor(
82
+ message: string,
83
+ props: { exitCode?: number; stderr?: string; timedOut?: boolean },
84
+ ) {
85
+ super(message);
86
+ this.name = "GitRunError";
87
+ this.exitCode = props.exitCode;
88
+ this.stderr = props.stderr;
89
+ this.timedOut = props.timedOut;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * 写类 git 命令判定(per-repo mutex 串行对象)。读类(status/rev-parse/diff/branch --list)
95
+ * 无副作用不加锁——并发读 git 自身安全,P-lock 实测冲突仅是写窗口假设。
96
+ */
97
+ function isWriteCommand(args: string[]): boolean {
98
+ if (args[0] === "worktree") return args[1] === "add" || args[1] === "remove" || args[1] === "prune";
99
+ if (args[0] === "branch") return args[1] === "-D";
100
+ return args[0] === "add";
101
+ }
102
+
103
+ /**
104
+ * 从 checkout 目录的 .git 指针文件推导主仓库路径(D5b 对账用)。
105
+ * worktree 的 .git 是文本文件(`gitdir: <repo>/.git/worktrees/<branch>`),
106
+ * 普通 repo(.git)与 bare+worktree(.bare)统一取 worktrees 段上两级。
107
+ * 解析失败(文件缺失/格式异常/路径越界)返回 undefined——调用方按无主残留处置。
108
+ */
109
+ function resolveRepoFromCheckout(checkout: string): string | undefined {
110
+ try {
111
+ const raw = fs.readFileSync(path.join(checkout, ".git"), "utf-8").trim();
112
+ if (!raw.startsWith("gitdir:")) return undefined;
113
+ const gitdir = raw.slice("gitdir:".length).trim();
114
+ // gitdir = <repo>/.git/worktrees/<branch> → 上三级 = repo root
115
+ // (bare 时 <ws>/.bare/worktrees/<br> → <ws>,git -C <bare> 操作合法)
116
+ const worktreesDir = path.dirname(gitdir);
117
+ if (path.basename(worktreesDir) !== "worktrees") return undefined;
118
+ const gitRootDir = path.dirname(worktreesDir);
119
+ return path.dirname(gitRootDir);
120
+ } catch {
121
+ return undefined;
122
+ }
123
+ }
124
+
125
+ export class WorktreeManager {
126
+ // 全局注册表:跨 repo 记录所有活 worktree,reaper 遍历此表判孤儿。
127
+ private readonly registry: WorktreeRegistry;
128
+ // agentDir(<agentDir>/subagents/<enc>/sessions 下扫 .alive 活信号,D5b 对账用)
129
+ private readonly agentDir: string;
130
+ // per-repo 写命令串行队列:value = 队尾(已吞 rejection 的)Promise。
131
+ // 入队形态 prev.catch(()=>{}).then(run)——后继只关心「自己已排队」,
132
+ // 不继承前驱错误(否则 1 个 worktree add 失败会传染同 repo 后续全部写命令,
133
+ // 替代旧同步版单线程天然全局串行的「各命令独立失败」语义)。
134
+ private readonly writeQueues = new Map<string, Promise<void>>();
135
+
136
+ constructor(agentDir: string) {
137
+ this.agentDir = agentDir;
138
+ this.registry = new WorktreeRegistry(agentDir);
139
+ }
140
+
141
+ /**
142
+ * 为子 agent 创建隔离 worktree。
143
+ *
144
+ * @param mainCwd 主仓库根目录
145
+ * @param recordId 执行记录 ID(必须匹配 `^[\w-]+$`)
146
+ * @returns 冻结的 WorktreeHandle
147
+ */
148
+ async create(mainCwd: string, recordId: string): Promise<WorktreeHandle> {
149
+ if (!SAFE_ID_RE.test(recordId)) {
150
+ throw new DirtyWorktreeError(
151
+ `recordId contains unsafe characters: "${recordId}" (must match ^[\\w-]+$)`,
152
+ );
153
+ }
154
+
155
+ // 脏树校验与 base commit 并行(读类无锁可并发)。allSettled 而非 all:
156
+ // status 先 reject 时 all 会短路,rev-parse 的后续 rejection 无人处理 →
157
+ // unhandledRejection。判定顺序固定:status 错误 → 脏树 → rev-parse 错误
158
+ // (脏树语义不变:仍先于 worktree add,rev-parse 结果在脏树时被丢弃)。
159
+ const [statusR, revR] = await Promise.allSettled([
160
+ this.gitRunAsync(["status", "--porcelain"], { cwd: mainCwd }),
161
+ this.gitRunAsync(["rev-parse", "HEAD"], { cwd: mainCwd }),
162
+ ]);
163
+ if (statusR.status === "rejected") throw statusR.reason;
164
+ // 消费点自行 trim:gitRunAsync 返回原始 stdout(保真),status 输出以 \n 结尾
165
+ const statusText = statusR.value.trim();
166
+ if (statusText.length > 0) {
167
+ throw new DirtyWorktreeError(
168
+ `Working tree is dirty in ${mainCwd}:\n${statusText}`,
169
+ );
170
+ }
171
+ if (revR.status === "rejected") throw revR.reason;
172
+ // 消费点自行 trim:rev-parse 输出 "hash\n",不 trim 会把换行带进后续 git args
173
+ const baseCommit = revR.value.trim();
174
+
175
+ const branch = `pi-sub-${recordId}`;
176
+ // checkout 放 tmpdir,脱离 .git/ 目录结构。
177
+ // 这样 git 自行把元数据注册到 <commonDir>/worktrees/<branch>/,
178
+ // 普通repo(.git/worktrees)与 bare+worktree(.bare/worktrees)都能正确工作。
179
+ // [MF3] 按 encodeCwd(mainCwd) 作用域——消除不同 repo / 不同 session 并发跑 sync
180
+ // fork subagent 时落到同一 /tmp/pi-sub-run-1 的冲突(recordId 是 per-session 自增,无 repo 作用域)。
181
+ const worktreePath = path.join(os.tmpdir(), "pi-subagents", encodeCwd(mainCwd), branch);
182
+
183
+ // 前置清理残留 checkout 目录:上次 create 的 MF#3 回滚可能因目录非空未删干净 tmpdir,
184
+ // 或跨进程竞态。路径在 tmpdir/pi-subagents/<enc>/<branch> 下,按设计只有本扩展创建,清理安全。
185
+ if (fs.existsSync(worktreePath)) {
186
+ try {
187
+ fs.rmSync(worktreePath, { recursive: true, force: true });
188
+ } catch (cleanErr) {
189
+ bestEffort(cleanErr, "pre-create checkout cleanup");
190
+ }
191
+ }
192
+
193
+ await this.gitRunAsync(["worktree", "add", "-b", branch, worktreePath, "HEAD"], {
194
+ cwd: mainCwd,
195
+ });
196
+
197
+ // 注册到全局表(pid=0 占位)。runSpawn 在 spawn() 返回后异步补 pid。
198
+ // 放在 worktree add 成功后、symlink 前——确保只有真正创建了 worktree 才登记。
199
+ await this.registry.add({
200
+ repo: mainCwd,
201
+ branch,
202
+ checkout: worktreePath,
203
+ pid: 0,
204
+ createdAt: Date.now(),
205
+ });
206
+
207
+ // [MF#3] worktree+分支+注册表条目已落盘,后续步骤(symlink)抛错时必须全部回滚,
208
+ // 否则 worktree+分支永久泄漏。create 后所有步骤包 try/catch。
209
+ try {
210
+ // 软链 node_modules(复用主仓库依赖)
211
+ const mainNodeModules = path.join(mainCwd, "node_modules");
212
+ const worktreeNodeModules = path.join(worktreePath, "node_modules");
213
+ if (fs.existsSync(mainNodeModules) && !fs.existsSync(worktreeNodeModules)) {
214
+ fs.symlinkSync(mainNodeModules, worktreeNodeModules);
215
+ }
216
+
217
+ return Object.freeze({
218
+ path: worktreePath,
219
+ branch,
220
+ baseCommit,
221
+ mainCwd,
222
+ });
223
+ } catch (err) {
224
+ // 回滚已创建的 worktree+分支+注册表条目,best-effort 吞清理异常(原始 err 仍外抛)
225
+ try {
226
+ await this.gitRunAsync(["worktree", "remove", "--force", worktreePath], { cwd: mainCwd });
227
+ } catch (cleanErr) {
228
+ bestEffort(cleanErr, "worktree remove (create rollback MF#3)");
229
+ }
230
+ try {
231
+ await this.gitRunAsync(["branch", "-D", branch], { cwd: mainCwd });
232
+ } catch (cleanErr) {
233
+ bestEffort(cleanErr, "branch delete (create rollback MF#3)");
234
+ }
235
+ await this.registry.remove(branch);
236
+ throw err;
237
+ }
238
+ }
239
+
240
+ /**
241
+ * 注册子进程 pid(runSpawn spawn() 返回后调)。
242
+ * create 时 pid 未知写 0 占位,子进程 spawn 返回后(child.pid 同步可得)由此补全。
243
+ * reaper 据 pid 死活判孤儿,pid=0 条目用 SPAWN_GRACE 宽限。
244
+ * sessionFile 可选补全:传入时填入 registry entry(reaper 据 pid 死活判孤儿,不读本字段;保留供诊断)。
245
+ *
246
+ * [D5a] async 化:pid 补全走跨进程锁内 RMW(互斥窗口消除 updatePid 与并发 add/remove
247
+ * 的交错)。永不 reject(锁降级 + best-effort save 均内部兜底),调用方可安全
248
+ * fire-and-forget(session-runner 的 stdout data 回调上下文)。
249
+ */
250
+ async registerPid(branch: string, pid: number, sessionFile?: string): Promise<void> {
251
+ await this.registry.updatePid(branch, pid, sessionFile);
252
+ }
253
+
254
+ /**
255
+ * 清理 worktree:git worktree remove --force + git branch -D + 注册表移除。
256
+ * 三步各自独立 try/catch——任一步失败不阻断其余(如 remove 失败仍尝试 branch -D + 注册表移除),
257
+ * 避免单步失败导致后续资源泄漏。
258
+ *
259
+ * @param handle 要清理的 worktree handle(含 mainCwd,不靠路径反推)
260
+ */
261
+ async cleanup(handle: WorktreeHandle): Promise<void> {
262
+ try {
263
+ await this.gitRunAsync(["worktree", "remove", "--force", handle.path], {
264
+ cwd: handle.mainCwd,
265
+ });
266
+ } catch (err) {
267
+ bestEffort(err, "worktree remove (cleanup)");
268
+ }
269
+
270
+ try {
271
+ await this.gitRunAsync(["branch", "-D", handle.branch], {
272
+ cwd: handle.mainCwd,
273
+ });
274
+ } catch (err) {
275
+ bestEffort(err, "branch delete (cleanup)");
276
+ }
277
+
278
+ await this.registry.remove(handle.branch);
279
+ }
280
+
281
+ /**
282
+ * 收集 worktree 的改动为 patch。
283
+ *
284
+ * [MF#3] patchFile 由调用方指定(写在 worktree 之外,避免被 cleanup 删除)。
285
+ * [MF#2] 先 git add -A 暂存全部改动(含未跟踪新文件),再 git diff --cached baseCommit
286
+ * 对比暂存区与 base commit。旧实现 `git diff HEAD baseCommit` 是树 vs 树对比:
287
+ * worktree HEAD 初始即 baseCommit,子 agent 不提交时 HEAD 仍 == baseCommit → diff 恒空 → 改动丢失。
288
+ *
289
+ * @param handle worktree handle
290
+ * @param patchFile patch 输出路径(须在 worktree 之外)
291
+ * @returns patch 结果(patchFile 路径 + failed/written 标记)。
292
+ * written=true 仅当 diff 非空且写盘成功;空 diff 或写失败均 written=false,
293
+ * 调用方据此回填 record.patchFile,避免悬空路径(`git apply` 不存在的文件)。
294
+ */
295
+ async collectPatch(handle: WorktreeHandle, patchFile: string): Promise<PatchResult> {
296
+ // git add -A:暂存全部改动(含未跟踪新文件),使后续 --cached diff 能捕获新建文件
297
+ try {
298
+ await this.gitRunAsync(["add", "-A"], { cwd: handle.path });
299
+ } catch (err) {
300
+ // add 失败不致命:继续尝试 diff,最差得到部分 diff(仅已跟踪文件的改动)
301
+ bestEffort(err, "git add -A (collectPatch)");
302
+ }
303
+ const diff = await this.gitRunAsync(
304
+ ["diff", "--cached", handle.baseCommit],
305
+ { cwd: handle.path },
306
+ );
307
+
308
+ if (diff.length === 0) {
309
+ // 无改动:不写文件,written=false(与有改动写成功区分)
310
+ return Object.freeze({ patchFile, failed: false, written: false });
311
+ }
312
+
313
+ try {
314
+ fs.writeFileSync(patchFile, diff, "utf-8");
315
+ return Object.freeze({ patchFile, failed: false, written: true });
316
+ } catch {
317
+ return Object.freeze({ patchFile, failed: true, written: false });
318
+ }
319
+ }
320
+
321
+ /**
322
+ * 扫描并清理 pi-sub-* 孤儿 worktree + 物理面对账(D5b)。
323
+ *
324
+ * 阶段一(既有):遍历全局注册表(<agentDir>/subagents/worktrees.json),
325
+ * 按 pid 死活判孤儿。不依赖当前 cwd 是否 git repo——注册表里记了 repo 路径,
326
+ * 直接 git -C <repo> 跨 repo 清理。
327
+ *
328
+ * 判据(唯一不删条件 = 进程还活着):
329
+ * pid > 0 且 isProcessAlive(pid) → 跳过(活进程,绝不删)
330
+ * pid > 0 且进程已死 → 孤儿(正常退出未 cleanup / 崩溃残留)
331
+ * pid == 0 且超 SPAWN_GRACE_MS → 孤儿(create 后崩溃,pid 永未补全)
332
+ * pid == 0 且未超宽限 → 跳过(可能正在 spawn)
333
+ *
334
+ * 阶段二(D5b):物理面(tmpdir checkout + 分支)与注册表双向 diff 收敛——
335
+ * 兑现 worktree-registry.ts 头注释声称的「tmpdir + 分支对账兜底」。全流程
336
+ * 幂等、失败仅日志(对账失败不阻断 session_start)。
337
+ */
338
+ async scan(): Promise<void> {
339
+ const entries = this.registry.load();
340
+ const now = Date.now();
341
+
342
+ // 逐孤儿串行 await(保持 for 循环串行语义,防止一次 reaper 打出 N 个并发 git)
343
+ for (const entry of entries) {
344
+ if (!this.isOrphan(entry, now)) {
345
+ continue;
346
+ }
347
+ await this.cleanupOrphan(entry);
348
+ }
349
+
350
+ await this.reconcileWithPhysical();
351
+ }
352
+
353
+ /**
354
+ * D5b 双向 diff 对账:物理面(tmpdir checkout 目录 + git branch --list)与
355
+ * 注册表互查,收敛三类漂移(锁消灭交错主因后,本对账兜底条目丢失/文件损坏的长尾)。
356
+ * 方向一/方向二的完整判据见 {@link removePhantomRegistryEntries} /
357
+ * {@link reconcileUnregisteredWorktrees}。
358
+ */
359
+ private async reconcileWithPhysical(): Promise<void> {
360
+ // 物理面发现失败(tmpdir 不可读等)→ 放弃本轮对账(失败仅日志,不阻断)
361
+ const physical = await this.discoverPhysicalWorktrees();
362
+ const registered = this.registry.load();
363
+ const registeredBranches = new Set(registered.map((e) => e.branch));
364
+
365
+ // ── 方向一:注册有 → 物理无 ──
366
+ // repo 集合 = 注册表条目 repo ∪ 物理推导 repo,per repo 查物理分支全集。
367
+ const repos = new Set<string>(registered.map((e) => e.repo));
368
+ for (const pt of physical) {
369
+ if (pt.repo) repos.add(pt.repo);
370
+ }
371
+ const branchesByRepo = await this.listPhysicalBranches(repos);
372
+ await this.removePhantomRegistryEntries(registered, branchesByRepo);
373
+
374
+ // ── 方向二:物理有 → 注册无 ──
375
+ const orphans = physical.filter((pt) => !registeredBranches.has(pt.branch));
376
+ await this.reconcileUnregisteredWorktrees(orphans);
377
+ }
378
+
379
+ /** 对账方向一(注册有 → 物理无):条目的分支与 checkout 目录都已不存在 → 条目指向
380
+ * 幻影资源 → 移除条目(纯清账,不删任何仍存在的资源,幂等安全)。 */
381
+ private async removePhantomRegistryEntries(
382
+ registered: WorktreeEntry[],
383
+ branchesByRepo: Map<string, Set<string>>,
384
+ ): Promise<void> {
385
+ for (const entry of registered) {
386
+ const branches = branchesByRepo.get(entry.repo);
387
+ // repo 分支查询失败(get undefined)→ 保守跳过:视为物理存在,不动条目。
388
+ if (branches === undefined) continue;
389
+ const branchGone = !branches.has(entry.branch);
390
+ const checkoutGone = !fs.existsSync(entry.checkout);
391
+ if (branchGone && checkoutGone) {
392
+ logger.warn("[worktree] reconcile: registry entry has no physical worktree/branch, removing entry", {
393
+ branch: entry.branch,
394
+ repo: entry.repo,
395
+ pid: entry.pid,
396
+ });
397
+ await this.registry.remove(entry.branch);
398
+ }
399
+ }
400
+ }
401
+
402
+ /** 对账方向二(物理有 → 注册无):按 enc 段(encodeCwd(mainCwd))聚合,活信号 =
403
+ * <agentDir>/subagents/<enc>/sessions/*.alive 中存活的 pid(session-runner
404
+ * first header 时写入,崩溃残留不删):
405
+ * - 无活 pid:残留判死,checkout mtime 超 SPAWN_GRACE_MS 才清(防误清另一
406
+ * 进程 worktree add 完成到 registry.add 落盘之间的 create 窗口);
407
+ * - 恰好 1 个活 pid 且恰好 1 个残留:补写回注册表(自愈——最常见的双 session
408
+ * 并发覆盖丢条目场景,补写后回归标准 pid 判据路径);
409
+ * - 多活 pid 或多残留无法建立 branch↔pid 对应:跳过 + warn——宁延迟勿误删;
410
+ * 活体自身 cleanup 路径正常(registry.remove 幂等),死体等活 pid 全灭后
411
+ * 下一周期收敛。 */
412
+ private async reconcileUnregisteredWorktrees(orphans: PhysicalWorktree[]): Promise<void> {
413
+ // 按 enc 段聚合处理(活信号以 enc 段为粒度——.alive 在 <enc>/sessions/ 下)
414
+ const orphansByEnc = new Map<string, PhysicalWorktree[]>();
415
+ for (const pt of orphans) {
416
+ const list = orphansByEnc.get(pt.enc) ?? [];
417
+ list.push(pt);
418
+ orphansByEnc.set(pt.enc, list);
419
+ }
420
+ for (const [enc, list] of orphansByEnc) {
421
+ await this.reconcileEncSegment(enc, list);
422
+ }
423
+ }
424
+
425
+ /** 单 enc 段的残留处置三分支:无活 pid 判死清理 / 唯一对应自愈补写 / 多对应保守跳过。 */
426
+ private async reconcileEncSegment(enc: string, list: PhysicalWorktree[]): Promise<void> {
427
+ const alivePids = this.collectAlivePids(enc);
428
+ if (alivePids.length === 0) {
429
+ await this.cleanupDeadSegment(list);
430
+ return;
431
+ }
432
+ if (alivePids.length === 1 && list.length === 1) {
433
+ // 唯一活 pid ↔ 唯一残留:对应关系无歧义,自愈补写回注册表。
434
+ // pid 若最终对应错误(理论上不该发生),后果是延迟清理而非误删(判活跳过)。
435
+ const pt = list[0];
436
+ logger.warn("[worktree] reconcile: unregistered physical worktree with one alive pid, re-registering (self-heal)", {
437
+ branch: pt.branch,
438
+ checkout: pt.checkout,
439
+ repo: pt.repo,
440
+ pid: alivePids[0],
441
+ });
442
+ await this.registry.add({
443
+ repo: pt.repo ?? path.dirname(pt.checkout),
444
+ branch: pt.branch,
445
+ checkout: pt.checkout,
446
+ pid: alivePids[0],
447
+ createdAt: pt.mtimeMs,
448
+ });
449
+ return;
450
+ }
451
+ // 多活 pid / 多残留:无法建立 branch↔pid 对应,保守跳过待下周期。
452
+ logger.warn("[worktree] reconcile: unregistered physical worktrees present but alive-pid mapping ambiguous, skipping this cycle", {
453
+ enc,
454
+ orphans: list.length,
455
+ alivePids: alivePids.length,
456
+ });
457
+ }
458
+
459
+ /** 无活 pid 段:残留判死清理——checkout mtime 超 SPAWN_GRACE_MS 才清(防误清另一
460
+ * 进程 worktree add 完成到 registry.add 落盘之间的 create 窗口)。 */
461
+ private async cleanupDeadSegment(list: PhysicalWorktree[]): Promise<void> {
462
+ for (const pt of list) {
463
+ const age = Date.now() - pt.mtimeMs;
464
+ if (age <= SPAWN_GRACE_MS) continue; // create 窗口(worktree add 后 add 落盘前)
465
+ logger.warn("[worktree] reconcile: unregistered physical worktree with no alive pid, cleaning up", {
466
+ branch: pt.branch,
467
+ checkout: pt.checkout,
468
+ repo: pt.repo,
469
+ ageMs: age,
470
+ });
471
+ await this.cleanupPhysical(pt);
472
+ }
473
+ }
474
+
475
+ /**
476
+ * 物理面发现:扫描 <tmpdir>/pi-subagents/<enc>/<pi-sub-*> checkout 目录。
477
+ * repo 从 checkout/.git 指针文件推导(`gitdir: <repo>/.git/worktrees/<branch>`,
478
+ * 普通 repo 与 bare+worktree(.bare/worktrees/...)统一取 worktrees 段上两级);
479
+ * 推导失败(残缺 checkout)repo=undefined,由调用方按无主残留处置。
480
+ */
481
+ private async discoverPhysicalWorktrees(): Promise<PhysicalWorktree[]> {
482
+ const root = path.join(os.tmpdir(), WORKTREE_TMP_ROOT);
483
+ let encDirs: string[];
484
+ try {
485
+ encDirs = fs.readdirSync(root, { withFileTypes: true })
486
+ .filter((d) => d.isDirectory())
487
+ .map((d) => d.name);
488
+ } catch {
489
+ return []; // tmpdir 根不存在(从未创建过 worktree)→ 空物理面
490
+ }
491
+
492
+ const result: PhysicalWorktree[] = [];
493
+ for (const enc of encDirs) {
494
+ let branchDirs: string[];
495
+ try {
496
+ branchDirs = fs.readdirSync(path.join(root, enc), { withFileTypes: true })
497
+ .filter((d) => d.isDirectory() && d.name.startsWith(BRANCH_PREFIX))
498
+ .map((d) => d.name);
499
+ } catch {
500
+ continue; // 单个 enc 段不可读:跳过该段(对账失败仅影响本段收敛)
501
+ }
502
+ for (const branch of branchDirs) {
503
+ const checkout = path.join(root, enc, branch);
504
+ try {
505
+ const mtimeMs = fs.statSync(checkout).mtimeMs;
506
+ result.push({ enc, branch, checkout, repo: resolveRepoFromCheckout(checkout), mtimeMs });
507
+ } catch (err) {
508
+ bestEffort(err, "physical worktree stat (reconcile)");
509
+ }
510
+ }
511
+ }
512
+ return result;
513
+ }
514
+
515
+ /**
516
+ * per repo 查物理分支全集:`git -C <repo> branch --list 'pi-sub-*' --format=%(refname:short)`。
517
+ * 读类命令不加写锁;单 repo 失败 → map 不含该 repo(get 返回 undefined),
518
+ * 调用方据此保守跳过该 repo 的条目判定(防把「查询失败」误判成「分支不存在」)。
519
+ */
520
+ private async listPhysicalBranches(repos: Set<string>): Promise<Map<string, Set<string>>> {
521
+ const map = new Map<string, Set<string>>();
522
+ for (const repo of repos) {
523
+ try {
524
+ const out = await this.gitRunAsync(
525
+ ["branch", "--list", `${BRANCH_PREFIX}*`, "--format=%(refname:short)"],
526
+ { cwd: repo },
527
+ );
528
+ const branches = new Set(
529
+ out.split("\n").map((l) => l.trim()).filter((l) => l.startsWith(BRANCH_PREFIX)),
530
+ );
531
+ map.set(repo, branches);
532
+ } catch (err) {
533
+ bestEffort(err, `git branch --list (reconcile, repo=${repo})`);
534
+ }
535
+ }
536
+ return map;
537
+ }
538
+
539
+ /**
540
+ * 收集 enc 段的活 pid:<agentDir>/subagents/<enc>/sessions/*.alive 中
541
+ * readAliveMarker 解析成功且 isProcessAlive 的 pid(去重)。
542
+ * 崩溃残留的 .alive(pid 已死)天然过滤掉——这正是「死活判据」的物理面来源。
543
+ */
544
+ private collectAlivePids(enc: string): number[] {
545
+ const sessionsDir = path.join(this.agentDir, "subagents", enc, "sessions");
546
+ let files: string[];
547
+ try {
548
+ files = fs.readdirSync(sessionsDir);
549
+ } catch {
550
+ return []; // enc 段无 sessions 目录(该 repo 从未跑过 subagent)→ 无活信号
551
+ }
552
+ const pids = new Set<number>();
553
+ for (const file of files) {
554
+ if (!file.endsWith(".alive")) continue;
555
+ const marker = readAliveMarker(path.join(sessionsDir, file.slice(0, -".alive".length)));
556
+ if (marker && isProcessAlive(marker.pid)) {
557
+ pids.add(marker.pid);
558
+ }
559
+ }
560
+ return [...pids];
561
+ }
562
+
563
+ /**
564
+ * 清理物理残留(D5b 方向二的死体处置):worktree remove → prune → branch -D
565
+ * → 目录 rm 兜底,四步各自 best-effort(幂等,失败仅日志)。
566
+ * prune 必要性:checkout 目录已不存在的 worktree,remove 会失败且 branch -D
567
+ * 被「used by worktree」拒绝——prune 清掉缺失目录的元数据后分支才可删。
568
+ */
569
+ private async cleanupPhysical(pt: PhysicalWorktree): Promise<void> {
570
+ if (pt.repo) {
571
+ try {
572
+ await this.gitRunAsync(["worktree", "remove", "--force", pt.checkout], { cwd: pt.repo });
573
+ } catch (err) {
574
+ bestEffort(err, "worktree remove (reconcile)");
575
+ }
576
+ try {
577
+ await this.gitRunAsync(["worktree", "prune"], { cwd: pt.repo });
578
+ } catch (err) {
579
+ bestEffort(err, "worktree prune (reconcile)");
580
+ }
581
+ try {
582
+ await this.gitRunAsync(["branch", "-D", pt.branch], { cwd: pt.repo });
583
+ } catch (err) {
584
+ bestEffort(err, "branch delete (reconcile)");
585
+ }
586
+ }
587
+ // 目录兜底:repo 未知(无主残留)或 remove 失败(元数据损坏)时直接删目录。
588
+ // 路径在 tmpdir/pi-subagents/<enc>/pi-sub-* 下,按设计只有本扩展创建,清理安全
589
+ // (与 create() 的前置清理同一安全边界)。
590
+ try {
591
+ if (fs.existsSync(pt.checkout)) {
592
+ fs.rmSync(pt.checkout, { recursive: true, force: true });
593
+ }
594
+ } catch (err) {
595
+ bestEffort(err, "checkout dir rm (reconcile)");
596
+ }
597
+ }
598
+
599
+ /**
600
+ * 判孤儿:pid 死活为主判据。pid=0 走 SPAWN_GRACE 宽限(create→spawn 窗口)。
601
+ */
602
+ private isOrphan(entry: WorktreeEntry, now: number): boolean {
603
+ if (entry.pid === 0) {
604
+ // create→spawn 窗口:超过宽限期仍未补 pid = create 后崩溃
605
+ const expired = now - entry.createdAt > SPAWN_GRACE_MS;
606
+ if (expired) {
607
+ // [worktree-reaper-fix] pid=0 超宽限 = create 后 spawn 前崩溃(或补全链路再次断链)。
608
+ // 正常路径 spawn 返回后 pid 已同步补全,此处不应命中活 worktree;命中即诊断信号,
609
+ // 与 updatePid 写盘失败的 warn 日志呼应(补全失败可观测闭环)。
610
+ logger.warn(
611
+ "[worktree] orphan reaper: pid=0 entry exceeded SPAWN_GRACE_MS, treating as orphan",
612
+ { branch: entry.branch, checkout: entry.checkout, createdAt: entry.createdAt, now },
613
+ );
614
+ }
615
+ return expired;
616
+ }
617
+ return !isProcessAlive(entry.pid);
618
+ }
619
+
620
+ /** 清理单个孤儿条目:worktree remove + branch -D + 注册表移除,三步各自 best-effort。 */
621
+ private async cleanupOrphan(entry: WorktreeEntry): Promise<void> {
622
+ try {
623
+ await this.gitRunAsync(["worktree", "remove", "--force", entry.checkout], { cwd: entry.repo });
624
+ } catch (err) {
625
+ bestEffort(err, "worktree remove (orphan reaper)");
626
+ }
627
+ try {
628
+ await this.gitRunAsync(["branch", "-D", entry.branch], { cwd: entry.repo });
629
+ } catch (err) {
630
+ bestEffort(err, "branch delete (orphan reaper)");
631
+ }
632
+ await this.registry.remove(entry.branch);
633
+ }
634
+
635
+ // ============================================================
636
+ // 内部工具
637
+ // ============================================================
638
+
639
+ /**
640
+ * git 命令异步执行器。与 gitRun 同一超时/错误包装约定(message 格式逐字一致),
641
+ * 差异仅在错误属性形态(GitRunError 挂 exitCode/stderr/timedOut)。
642
+ * 写类命令经 per-repo mutex 串行(不依赖 git 锁实现细节 + 并发限流 + 行为确定性)。
643
+ *
644
+ * stdout 保真返回(不 trim):collectPatch 把 diff 输出原样落盘为 patch 文件,
645
+ * 裁掉尾换行会产出 `git apply` 拒绝的 corrupt patch(2026-08-16 门 4 实测)。
646
+ * 需要干净文本的消费点(baseCommit / 脏树 status 拼接)自行 trim。
647
+ */
648
+ private async gitRunAsync(args: string[], opts: { cwd: string; timeout?: number }): Promise<string> {
649
+ const run = (): Promise<string> =>
650
+ new Promise((resolve, reject) => {
651
+ execFile(
652
+ "git",
653
+ args,
654
+ { cwd: opts.cwd, timeout: opts.timeout ?? GIT_TIMEOUT_MS, encoding: "utf-8" },
655
+ (err, stdout, stderr) => {
656
+ if (err) {
657
+ const execErr = err as Error & { code?: unknown; killed?: boolean; signal?: string };
658
+ reject(
659
+ new GitRunError(`git ${args[0]} failed: ${execErr.message}`, {
660
+ // P-errshape 实测:execFile 退出码在 err.code(数字时);超时 killed+SIGTERM
661
+ exitCode: typeof execErr.code === "number" ? execErr.code : undefined,
662
+ stderr: typeof stderr === "string" ? stderr : undefined,
663
+ timedOut: execErr.killed === true && execErr.signal === "SIGTERM",
664
+ }),
665
+ );
666
+ return;
667
+ }
668
+ resolve(stdout);
669
+ },
670
+ );
671
+ });
672
+
673
+ if (!isWriteCommand(args)) return run();
674
+ // per-repo 队列:吞前驱 rejection 后接续本命令;队尾比对自清理防 Map 泄漏
675
+ const repo = opts.cwd;
676
+ const prev = this.writeQueues.get(repo) ?? Promise.resolve();
677
+ const next = prev.catch(() => {}).then(run);
678
+ const tail: Promise<void> = next.then(
679
+ () => undefined,
680
+ () => undefined,
681
+ );
682
+ this.writeQueues.set(repo, tail);
683
+ void tail.finally(() => {
684
+ if (this.writeQueues.get(repo) === tail) this.writeQueues.delete(repo);
685
+ });
686
+ return next;
687
+ }
688
+ }