@zhushanwen/subagent-core 0.2.0 → 0.5.1

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 (347) hide show
  1. package/README.md +45 -2
  2. package/agents/analyst.md +60 -0
  3. package/agents/coder.md +69 -0
  4. package/agents/debugger.md +66 -0
  5. package/agents/doc-reviewer.md +49 -0
  6. package/agents/explorer.md +63 -0
  7. package/agents/general-purpose.md +32 -0
  8. package/agents/orchestrator.md +61 -0
  9. package/agents/planner.md +53 -0
  10. package/agents/researcher.md +67 -0
  11. package/agents/reviewer.md +73 -0
  12. package/dist/chunk-43ONBFZX.js +240 -0
  13. package/dist/chunk-APZY4IME.js +27 -0
  14. package/dist/chunk-V3VHZ2VX.js +59 -0
  15. package/dist/execution/engine/engines/zcode/constants.cjs +62 -9
  16. package/dist/execution/engine/engines/zcode/constants.d.cts +116 -12
  17. package/dist/execution/engine/engines/zcode/constants.d.ts +116 -12
  18. package/dist/execution/engine/engines/zcode/constants.js +39 -7
  19. package/dist/execution/engine/engines/zcode/reader.d.cts +2 -1
  20. package/dist/execution/engine/engines/zcode/reader.d.ts +2 -1
  21. package/dist/execution/engine/engines/zcode/reader.js +4 -234
  22. package/dist/execution/engine/paths.js +7 -19
  23. package/dist/index.cjs +18234 -1277
  24. package/dist/index.d.cts +5646 -673
  25. package/dist/index.d.ts +5646 -673
  26. package/dist/index.js +17828 -1277
  27. package/dist/{types-BxyAidGf.d.cts → types-Dv4QhSJ_.d.cts} +678 -112
  28. package/dist/{types-BxyAidGf.d.ts → types-Dv4QhSJ_.d.ts} +678 -112
  29. package/package.json +7 -4
  30. package/src/__tests__/agent-opts-resolver-schema-prompt.test.ts +0 -1
  31. package/src/__tests__/append-system-prompt-assembly.test.ts +8 -7
  32. package/src/__tests__/fr4-get-state-handshake.test.ts +2 -2
  33. package/src/__tests__/m2-append-content-probe.test.ts +0 -1
  34. package/src/__tests__/manifest-store.test.ts +10 -9
  35. package/src/__tests__/record-store-cache.test.ts +0 -2
  36. package/src/__tests__/record-store-index.test.ts +1 -2
  37. package/src/__tests__/review-fix-loop-script.test.ts +93 -3
  38. package/src/__tests__/review-fix-loop-utils.test.ts +31 -10
  39. package/src/__tests__/robustness-low-batch1.test.ts +6 -5
  40. package/src/__tests__/robustness-medium-batch1.test.ts +2 -2
  41. package/src/__tests__/robustness-medium-batch2.test.ts +17 -6
  42. package/src/__tests__/robustness-medium-batch4.test.ts +2 -2
  43. package/src/__tests__/session-runner.test.ts +1 -1
  44. package/src/__tests__/smoke.test.ts +9 -2
  45. package/src/core/__tests__/host-services.test.ts +2 -2
  46. package/src/core/__tests__/logger.test.ts +1 -1
  47. package/src/core/error-message.ts +9 -0
  48. package/src/core/host-services.ts +21 -5
  49. package/src/core/notify-ports.ts +18 -5
  50. package/src/execution/__tests__/agent-profile.test.ts +232 -0
  51. package/src/execution/__tests__/agent-registry.test.ts +370 -0
  52. package/src/execution/__tests__/agent-result-mapper.test.ts +18 -2
  53. package/src/execution/__tests__/agents-assembly.test.ts +219 -0
  54. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +6 -6
  55. package/src/execution/__tests__/chat-engine-routing.test.ts +136 -30
  56. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +5 -4
  57. package/src/execution/__tests__/cold-resurrect.test.ts +283 -0
  58. package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
  59. package/src/execution/__tests__/delivery-methods.test.ts +77 -62
  60. package/src/execution/__tests__/descendant-sweep-guards.test.ts +220 -0
  61. package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
  62. package/src/execution/__tests__/dialog-queue.test.ts +199 -1
  63. package/src/execution/__tests__/engine-model-validation.test.ts +741 -0
  64. package/src/execution/__tests__/epipe-fallback.test.ts +31 -24
  65. package/src/execution/__tests__/execute-and-await-worktree.test.ts +21 -91
  66. package/src/execution/__tests__/execute-nesting.test.ts +34 -188
  67. package/src/execution/__tests__/execution-record.test.ts +217 -60
  68. package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
  69. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +3 -3
  70. package/src/execution/__tests__/finalize-record.test.ts +278 -16
  71. package/src/execution/__tests__/gc-timer.test.ts +57 -2
  72. package/src/execution/__tests__/get-record-for-action-restart.test.ts +14 -12
  73. package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
  74. package/src/execution/__tests__/helpers/mock-extension-api.ts +1 -1
  75. package/src/execution/__tests__/helpers/session-runner-mocks.ts +113 -0
  76. package/src/execution/__tests__/helpers/spawn-mock.ts +150 -3
  77. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +137 -0
  78. package/src/execution/__tests__/keep-alive-no-progress.test.ts +280 -0
  79. package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
  80. package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
  81. package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
  82. package/src/execution/__tests__/lifecycle-predicates.test.ts +1 -2
  83. package/src/execution/__tests__/manifest-parentid.test.ts +4 -4
  84. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
  85. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
  86. package/src/execution/__tests__/model-resolver.test.ts +2 -2
  87. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +23 -121
  88. package/src/execution/__tests__/nested-visibility.test.ts +2 -0
  89. package/src/execution/__tests__/notify-ledger.test.ts +1249 -0
  90. package/src/execution/__tests__/output-collector.test.ts +117 -6
  91. package/src/execution/__tests__/pi-invocation.test.ts +1 -1
  92. package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
  93. package/src/execution/__tests__/record-store.test.ts +89 -5
  94. package/src/execution/__tests__/records-cwd-isolation.test.ts +2 -2
  95. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +29 -132
  96. package/src/execution/__tests__/recursive-visibility-env.test.ts +55 -179
  97. package/src/execution/__tests__/rpc-mode.test.ts +2 -2
  98. package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +387 -0
  99. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +19 -9
  100. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +5 -4
  101. package/src/execution/__tests__/run-spawn-edges.test.ts +418 -26
  102. package/src/execution/__tests__/run-spawn-integration.test.ts +3 -3
  103. package/src/execution/__tests__/run-spawn-resume.test.ts +2 -2
  104. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +2 -2
  105. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +16 -8
  106. package/src/execution/__tests__/service-kill-escalation.test.ts +91 -0
  107. package/src/execution/__tests__/session-context-resolver.test.ts +0 -2
  108. package/src/execution/__tests__/session-file-gc.test.ts +35 -0
  109. package/src/execution/__tests__/session-pending.test.ts +471 -0
  110. package/src/execution/__tests__/session-reconstructor.test.ts +366 -15
  111. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
  112. package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
  113. package/src/execution/__tests__/session-runner-dispatch.test.ts +489 -0
  114. package/src/execution/__tests__/session-runner-epipe.test.ts +4 -3
  115. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +227 -0
  116. package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +290 -0
  117. package/src/execution/__tests__/session-runner-schema-env.test.ts +7 -5
  118. package/src/execution/__tests__/settled-watchdog.test.ts +524 -0
  119. package/src/execution/__tests__/spawn-args.test.ts +11 -10
  120. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +26 -23
  121. package/src/execution/__tests__/spawn-event-adapter.test.ts +1 -1
  122. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +5 -4
  123. package/src/execution/__tests__/spawned-children.test.ts +155 -11
  124. package/src/execution/__tests__/start-sync-model-guard.test.ts +4 -4
  125. package/src/execution/__tests__/stdin-writer.test.ts +6 -4
  126. package/src/execution/__tests__/stream-sink-retirement.test.ts +6 -6
  127. package/src/execution/__tests__/subagent-actions-core.test.ts +1017 -0
  128. package/src/execution/__tests__/subagent-service-message-close.test.ts +99 -35
  129. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
  130. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +271 -0
  131. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +33 -29
  132. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +341 -0
  133. package/src/execution/__tests__/subagent-service.test.ts +37 -30
  134. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +88 -12
  135. package/src/execution/__tests__/subprocess-agent-runner-timeout.test.ts +75 -0
  136. package/src/execution/__tests__/subprocess-agent-runner.test.ts +22 -20
  137. package/src/execution/__tests__/temp-prompt.test.ts +1 -1
  138. package/src/execution/__tests__/timeout-integration.test.ts +13 -12
  139. package/src/execution/__tests__/turn-limiter-semantics.test.ts +1 -1
  140. package/src/execution/__tests__/turn-limiter.test.ts +1 -1
  141. package/src/execution/__tests__/ui-request-handler-factory.test.ts +107 -7
  142. package/src/execution/__tests__/ui-request-handler.test.ts +4 -3
  143. package/src/execution/__tests__/ui-request-queue.test.ts +167 -2
  144. package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
  145. package/src/execution/__tests__/worktree-manager.test.ts +14 -5
  146. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +4 -3
  147. package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
  148. package/src/execution/agent-registry.ts +298 -30
  149. package/src/execution/agent-result-mapper.ts +3 -0
  150. package/src/execution/agents-assembly.ts +88 -0
  151. package/src/execution/cold-resurrect.ts +199 -0
  152. package/src/execution/concurrency-pool.ts +71 -9
  153. package/src/execution/dialog-queue.ts +101 -3
  154. package/src/execution/engine/__tests__/common/capability-gate.test.ts +91 -0
  155. package/src/execution/engine/__tests__/common/data-dir.test.ts +1 -1
  156. package/src/execution/engine/__tests__/common/journal-wiring.test.ts +84 -0
  157. package/src/execution/engine/__tests__/common/kill-chain.test.ts +34 -7
  158. package/src/execution/engine/__tests__/common/persona-router.test.ts +4 -5
  159. package/src/execution/engine/__tests__/common/pool-manager.test.ts +211 -51
  160. package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +149 -0
  161. package/src/execution/engine/__tests__/common/session-view-service.test.ts +499 -0
  162. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +56 -99
  163. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +88 -15
  164. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +33 -7
  165. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +1 -1
  166. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +75 -54
  167. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +128 -0
  168. package/src/execution/engine/__tests__/engine-discovery.test.ts +15 -3
  169. package/src/execution/engine/__tests__/model-prompt.test.ts +15 -3
  170. package/src/execution/engine/__tests__/registry.test.ts +94 -3
  171. package/src/execution/engine/__tests__/routing.test.ts +115 -6
  172. package/src/execution/engine/common/capability-gate.ts +92 -0
  173. package/src/execution/engine/common/journal-wiring.ts +97 -0
  174. package/src/execution/engine/common/kill-chain.ts +42 -10
  175. package/src/execution/engine/common/nesting-guard.ts +66 -0
  176. package/src/execution/engine/common/persona-router.ts +18 -8
  177. package/src/execution/engine/common/pool-manager.ts +332 -78
  178. package/src/execution/engine/common/session-view-service.ts +506 -0
  179. package/src/execution/engine/common/session-view-types.ts +125 -0
  180. package/src/execution/engine/engine-discovery.ts +12 -16
  181. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +174 -28
  182. package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +257 -0
  183. package/src/execution/{argv-mirror.ts → engine/engines/pi/argv-mirror.ts} +43 -21
  184. package/src/execution/{get-state-handshake.ts → engine/engines/pi/get-state-handshake.ts} +86 -11
  185. package/src/execution/{output-collector.ts → engine/engines/pi/output-collector.ts} +108 -19
  186. package/src/execution/engine/engines/pi/pi-engine.ts +434 -38
  187. package/src/execution/{pi-invocation.ts → engine/engines/pi/pi-invocation.ts} +2 -2
  188. package/src/execution/engine/engines/pi/reader.ts +11 -0
  189. package/src/execution/engine/engines/pi/registration.ts +4 -2
  190. package/src/execution/engine/engines/pi/session-runner.ts +2840 -0
  191. package/src/execution/{spawn-event-adapter.ts → engine/engines/pi/spawn-event-adapter.ts} +75 -34
  192. package/src/execution/{stdin-writer.ts → engine/engines/pi/stdin-writer.ts} +10 -9
  193. package/src/execution/{temp-prompt.ts → engine/engines/pi/temp-prompt.ts} +2 -2
  194. package/src/execution/{turn-limiter.ts → engine/engines/pi/turn-limiter.ts} +1 -1
  195. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +301 -0
  196. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
  197. package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +321 -0
  198. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +473 -0
  199. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +7 -210
  200. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +6 -110
  201. package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +247 -0
  202. package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +451 -0
  203. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +792 -0
  204. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +782 -0
  205. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +168 -0
  206. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +210 -0
  207. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +353 -0
  208. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +282 -0
  209. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +372 -0
  210. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +65 -47
  211. package/src/execution/engine/engines/zcode/appserver-launcher.ts +167 -0
  212. package/src/execution/engine/engines/zcode/connection.ts +598 -0
  213. package/src/execution/engine/engines/zcode/constants.ts +147 -14
  214. package/src/execution/engine/engines/zcode/golden-sample.ts +42 -34
  215. package/src/execution/engine/engines/zcode/parser.ts +12 -346
  216. package/src/execution/engine/engines/zcode/preparer.ts +22 -150
  217. package/src/execution/engine/engines/zcode/session-channel.ts +968 -0
  218. package/src/execution/engine/engines/zcode/zcode-engine.ts +1023 -250
  219. package/src/execution/engine/host-task-spec.ts +32 -36
  220. package/src/execution/engine/model-validation.ts +177 -0
  221. package/src/execution/engine/port.ts +65 -7
  222. package/src/execution/engine/registry.ts +53 -0
  223. package/src/execution/engine/routing.ts +75 -1
  224. package/src/execution/engine/types.ts +30 -103
  225. package/src/execution/execution-record.ts +222 -144
  226. package/src/execution/finalize-record.ts +181 -85
  227. package/src/execution/idle-gc.ts +28 -0
  228. package/src/execution/lifecycle-manager.ts +27 -3
  229. package/src/execution/lifecycle-predicates.ts +1 -1
  230. package/src/execution/manifest-store.ts +53 -62
  231. package/src/execution/notifier.ts +10 -10
  232. package/src/execution/notify-host.ts +212 -0
  233. package/src/execution/notify-ledger.ts +117 -16
  234. package/src/execution/record-entry.ts +8 -2
  235. package/src/execution/record-store.ts +148 -88
  236. package/src/execution/round-settlement.ts +93 -0
  237. package/src/execution/session-file-gc.ts +96 -62
  238. package/src/execution/session-pending.ts +213 -62
  239. package/src/execution/session-reconstructor.ts +256 -143
  240. package/src/execution/sessions-index.ts +127 -102
  241. package/src/execution/settled-watchdog.ts +311 -0
  242. package/src/execution/subagent-actions-core.ts +686 -0
  243. package/src/execution/subagent-service.ts +1106 -907
  244. package/src/execution/subprocess-agent-runner.ts +144 -97
  245. package/src/execution/types.ts +9 -0
  246. package/src/execution/ui-request-handler-factory.ts +126 -75
  247. package/src/execution/ui-request-queue.ts +50 -19
  248. package/src/execution/worktree-git-ops.ts +397 -0
  249. package/src/execution/worktree-manager.ts +72 -10
  250. package/src/execution/worktree-registry.ts +6 -12
  251. package/src/index.ts +542 -9
  252. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
  253. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +3 -5
  254. package/src/orchestration/__tests__/agent-call-stream.test.ts +2 -2
  255. package/src/orchestration/__tests__/args-meta.test.ts +358 -0
  256. package/src/orchestration/__tests__/args-validator.test.ts +14 -0
  257. package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
  258. package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +384 -0
  259. package/src/orchestration/__tests__/execute-agent-call.test.ts +151 -11
  260. package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
  261. package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
  262. package/src/orchestration/__tests__/file-run-store.test.ts +390 -0
  263. package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
  264. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
  265. package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
  266. package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
  267. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +3 -3
  268. package/src/orchestration/__tests__/lifecycle.test.ts +84 -5
  269. package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +1 -1
  270. package/src/orchestration/__tests__/run-snapshot.test.ts +356 -0
  271. package/src/orchestration/__tests__/script-generate.test.ts +314 -0
  272. package/src/orchestration/__tests__/script-lint.test.ts +17 -0
  273. package/src/orchestration/__tests__/test-mocks.ts +2 -0
  274. package/src/orchestration/__tests__/worker-exit-without-result.test.ts +7 -6
  275. package/src/orchestration/__tests__/worker-message-pump-finalize-run.test.ts +229 -0
  276. package/src/orchestration/__tests__/{error-recovery-handlers.test.ts → worker-message-pump-handlers.test.ts} +28 -27
  277. package/src/orchestration/__tests__/{error-recovery-postmessage-defense.test.ts → worker-message-pump-postmessage-defense.test.ts} +8 -8
  278. package/src/orchestration/__tests__/{error-recovery-serialize-failed-result.test.ts → worker-message-pump-serialize-failed-result.test.ts} +2 -2
  279. package/src/orchestration/__tests__/{error-recovery-workflow-call.test.ts → worker-message-pump-workflow-call.test.ts} +1 -1
  280. package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
  281. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
  282. package/src/orchestration/__tests__/worker-script-builder.test.ts +1 -1
  283. package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
  284. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +1 -1
  285. package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
  286. package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
  287. package/src/orchestration/agent-opts-resolver.ts +8 -8
  288. package/src/orchestration/args-meta.ts +198 -0
  289. package/src/orchestration/args-validator.ts +53 -31
  290. package/src/orchestration/execute-agent-call.ts +25 -79
  291. package/src/orchestration/file-run-store.ts +327 -0
  292. package/src/orchestration/launcher.ts +160 -104
  293. package/src/orchestration/lifecycle.ts +371 -97
  294. package/src/orchestration/models/agent-call.ts +7 -7
  295. package/src/orchestration/models/budget.ts +5 -5
  296. package/src/orchestration/models/ports.ts +4 -4
  297. package/src/orchestration/models/run-runtime.ts +15 -15
  298. package/src/orchestration/models/trace.ts +9 -9
  299. package/src/orchestration/models/types.ts +111 -22
  300. package/src/orchestration/models/workflow-run.ts +28 -28
  301. package/src/orchestration/models/workflow-script.ts +4 -4
  302. package/src/orchestration/run-snapshot.ts +266 -0
  303. package/src/orchestration/script-generate.ts +154 -0
  304. package/src/orchestration/script-lint.ts +123 -90
  305. package/src/orchestration/worker-handle.ts +10 -10
  306. package/src/orchestration/worker-host.ts +10 -10
  307. package/src/orchestration/{error-recovery.ts → worker-message-pump.ts} +498 -221
  308. package/src/orchestration/worker-script-builder.ts +401 -346
  309. package/src/orchestration/workflow-files.ts +37 -11
  310. package/src/orchestration/workflow-run-summary.ts +69 -0
  311. package/src/orchestration/workflow-script-registry-impl.ts +31 -9
  312. package/src/shared/__tests__/agent-ref.test.ts +209 -2
  313. package/src/shared/__tests__/atomic-write.test.ts +267 -0
  314. package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
  315. package/src/shared/__tests__/injection-render.test.ts +518 -0
  316. package/src/shared/__tests__/meta-parser.test.ts +151 -0
  317. package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
  318. package/src/shared/__tests__/resource-discovery.test.ts +261 -217
  319. package/src/shared/agent-ref.ts +143 -2
  320. package/src/shared/atomic-write.ts +323 -0
  321. package/src/shared/bounded-serialize.ts +154 -0
  322. package/src/shared/injection-render.ts +279 -0
  323. package/src/shared/meta-parser.ts +182 -68
  324. package/src/shared/model-ref.ts +15 -2
  325. package/src/shared/resource-discovery.ts +97 -204
  326. package/src/shared/resource-meta.ts +14 -0
  327. package/src/shared/schema-jsonify.ts +1 -1
  328. package/src/shared/xml-injection.ts +9 -9
  329. package/workflows/README.md +9 -9
  330. package/workflows/chain.js +4 -2
  331. package/workflows/map-reduce.js +5 -3
  332. package/workflows/parallel.js +5 -3
  333. package/workflows/review-fix-loop-utils.cjs +165 -92
  334. package/workflows/review-fix-loop-utils.d.cts +287 -0
  335. package/workflows/review-fix-loop.js +18 -7
  336. package/workflows/scatter-gather.js +4 -2
  337. package/dist/chunk-3VOERJPJ.js +0 -22
  338. package/src/execution/__tests__/execute-options-mapper.test.ts +0 -200
  339. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +0 -164
  340. package/src/execution/engine/engines/pi/task-spec-mapper.ts +0 -100
  341. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +0 -39
  342. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +0 -150
  343. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +0 -580
  344. package/src/execution/engine/engines/zcode/launcher.ts +0 -161
  345. package/src/execution/execute-options-mapper.ts +0 -115
  346. package/src/execution/session-runner.ts +0 -1789
  347. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +0 -288
@@ -0,0 +1,181 @@
1
+ // src/execution/__tests__/session-runner-close-prune.test.ts
2
+ //
3
+ // [LC-6/T6②] 进程 close 接线:waitForChildExit close handler 调 prunePendingCursor
4
+ // 剪枝该进程 sessionFile 的 pending 增量游标(session-runner 领地内的单点接线,主
5
+ // agent 裁决授权)。
6
+ //
7
+ // 覆盖:
8
+ // 1. close(resume 锁定 sessionFile)后 cursor 被剪枝——行为观察面:覆写同名文件
9
+ // (新 register 行落在旧 offset 之内、size ≥ 旧 offset),判定应从头全量读出
10
+ // 新条目;cursor 未剪时会从旧 offset 续读丢掉它。
11
+ // 2. sessionFile 未回填(无 resume、无 header 回填)时 close 跳过剪枝不抛错
12
+ // (此时也无 cursor:判定从未发生)。
13
+ //
14
+ // mock 拓扑对齐 run-spawn-resume.test.ts(FakeChild / fs 部分 mock / temp-prompt /
15
+ // alive-store);session-pending 刻意**不 mock**——接线行为的观察面就是真实游标
16
+ // 剪枝语义(statSync/readFileSync 不在 fs mock 名单,走真实 fs)。
17
+
18
+ import { spawn } from "node:child_process";
19
+ import * as fs from "node:fs";
20
+ import { writeFile } from "node:fs/promises";
21
+ import * as os from "node:os";
22
+ import * as path from "node:path";
23
+
24
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
25
+
26
+ const { loggerMock } = vi.hoisted(() => ({
27
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
28
+ }));
29
+ vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
30
+
31
+ vi.mock("node:child_process", async () => {
32
+ const { FakeChild } = await import("./helpers/spawn-mock.ts");
33
+ return {
34
+ spawn: vi.fn(() => new FakeChild()),
35
+ execFile: vi.fn(
36
+ (
37
+ _cmd: string,
38
+ _args: readonly string[],
39
+ _opts: unknown,
40
+ cb: (err: Error | null, stdout?: string, stderr?: string) => void,
41
+ ) => cb(new Error("execFile not configured in this test")),
42
+ ),
43
+ };
44
+ });
45
+
46
+ // [注意] 工厂内必须 importOriginal 拿真 actual——不带它 `await import("node:fs")` 自引用
47
+ // 返回 mock 面。命名面先 ...actual 展开(session-pending 的 statSync/readFileSync、
48
+ // 测试 fixture 的 mkdtempSync/rmSync 走真实 fs;fixture 写盘走真实透传的
49
+ // node:fs/promises——writeFileSync 本身被 mock 隔离 runSpawn 的生产写入),再覆盖
50
+ // runSpawn 生产写入面的 5 个方法(隔离 ~/.xyz-agent 生产路径写)。
51
+ vi.mock("node:fs", async (importOriginal) => {
52
+ const actual = await importOriginal<typeof import("node:fs")>();
53
+ return {
54
+ default: {
55
+ ...actual,
56
+ mkdirSync: vi.fn(),
57
+ existsSync: vi.fn(() => false),
58
+ appendFileSync: vi.fn(),
59
+ writeFileSync: vi.fn(),
60
+ readdirSync: vi.fn(() => []),
61
+ },
62
+ ...actual,
63
+ mkdirSync: vi.fn(),
64
+ existsSync: vi.fn(() => false),
65
+ appendFileSync: vi.fn(),
66
+ writeFileSync: vi.fn(),
67
+ readdirSync: vi.fn(() => []),
68
+ promises: actual.promises,
69
+ };
70
+ });
71
+
72
+ vi.mock("../alive-store.ts", () => ({
73
+ writeAliveMarker: vi.fn(),
74
+ }));
75
+
76
+ vi.mock("../engine/engines/pi/temp-prompt.ts", () => ({
77
+ writePromptToTempFile: vi.fn(async (agent: string) => {
78
+ const safeName = agent.replace(/[^\w.-]+/g, "_");
79
+ return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
80
+ }),
81
+ cleanupTempPrompt: vi.fn(async () => {}),
82
+ }));
83
+
84
+ import { runSpawn } from "../engine/engines/pi/session-runner.ts";
85
+ import { clearPendingCursors, listActivePendingFromSessionFile } from "../session-pending.ts";
86
+ import {
87
+ type FakeChild,
88
+ lastSpawnedChild as lastSpawnedChildOf,
89
+ makeCtx,
90
+ makeOpts,
91
+ makeRecord,
92
+ waitForSpawn as waitForSpawnOf,
93
+ } from "./helpers/spawn-mock.ts";
94
+
95
+ const mockSpawn = vi.mocked(spawn);
96
+ const mockExistsSync = vi.mocked(fs.existsSync);
97
+
98
+ const lastSpawnedChild = (): FakeChild => lastSpawnedChildOf(mockSpawn);
99
+ const waitForSpawn = (timeoutMs = 1000): Promise<void> => waitForSpawnOf(mockSpawn, timeoutMs);
100
+
101
+ let sessionDir = "";
102
+
103
+ function entryLine(customType: string, data: Record<string, unknown>): string {
104
+ return `${JSON.stringify({
105
+ type: "custom",
106
+ customType,
107
+ data,
108
+ timestamp: "2026-09-01T00:00:00.000Z",
109
+ id: `e-${Math.random().toString(36).slice(2)}`,
110
+ })}\n`;
111
+ }
112
+
113
+ function registerData(id: string, sessionId: string): Record<string, unknown> {
114
+ return { id, type: "session", name: `desc-${id}`, status: "active", registeredAt: 1, expiresAt: undefined, sessionId };
115
+ }
116
+
117
+ /** resume + close 的最短驱动:spawn 后直接 end stdout + close(handshake 经 abandon 放弃)。 */
118
+ async function runToClose(sessionFile: string): Promise<void> {
119
+ const record = makeRecord();
120
+ const promise = runSpawn(record, "Task: close prune", makeOpts(), makeCtx(), {
121
+ sessionFile,
122
+ });
123
+ await waitForSpawn();
124
+ const child = lastSpawnedChild();
125
+ // resume.sessionFile 真实存在 → existsSync 对它 true(identity 补写路径走通)
126
+ mockExistsSync.mockImplementation((p: unknown) => String(p) === sessionFile);
127
+ child.stdin.on("data", () => {}); // 消费 stdin 防背压
128
+ child.stdout.end();
129
+ child.emit("close", 0);
130
+ await promise;
131
+ }
132
+
133
+ beforeEach(() => {
134
+ vi.clearAllMocks();
135
+ clearPendingCursors();
136
+ mockExistsSync.mockReturnValue(false);
137
+ sessionDir = fs.mkdtempSync(path.join(os.tmpdir(), "close-prune-"));
138
+ });
139
+
140
+ afterEach(() => {
141
+ clearPendingCursors();
142
+ vi.restoreAllMocks();
143
+ fs.rmSync(sessionDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
144
+ });
145
+
146
+ describe("[LC-6] 进程 close 剪枝 pending 游标", () => {
147
+ it("close 后 cursor 被剪枝:覆写同名文件后判定从头全量读(旧 offset 不残留)", async () => {
148
+ const f = path.join(sessionDir, "closed-proc.jsonl");
149
+ // gen1:15 字节/行 × 10 行 filler → 预建 cursor offset = 150
150
+ const gen1 = Array.from({ length: 10 }, (_, i) => `filler-line-${String(i).padStart(2, "0")}\n`).join("");
151
+ await writeFile(f, gen1); // 真实写盘(同步 writeFileSync 被 mock 隔离生产写入)
152
+ expect(listActivePendingFromSessionFile(f).items).toHaveLength(0); // cursor 建立
153
+
154
+ // 覆写(模拟磁盘状态变化):register bg-y 落在旧 offset(150)之内、size > 150——
155
+ // cursor 未剪时会从 byte 150 续读丢掉它。
156
+ const gen2 =
157
+ entryLine("pending:register", registerData("bg-y", "sess-y")) +
158
+ "filler".padEnd(150, ".") +
159
+ "\n";
160
+ expect(Buffer.byteLength(gen2, "utf-8")).toBeGreaterThan(150);
161
+ await writeFile(f, gen2);
162
+
163
+ await runToClose(f); // resume 锁定 sessionFile → close handler 剪枝
164
+
165
+ const items = listActivePendingFromSessionFile(f).items;
166
+ expect(items.map((i) => i.id)).toEqual(["bg-y"]); // 剪枝生效:从头全量读
167
+ });
168
+
169
+ it("sessionFile 未回填(无 resume/无 header)时 close 跳过剪枝,流程正常完成", async () => {
170
+ const record = makeRecord();
171
+ const promise = runSpawn(record, "Task: no sessionFile", makeOpts(), makeCtx());
172
+ await waitForSpawn();
173
+ const child = lastSpawnedChild();
174
+ child.stdin.on("data", () => {});
175
+ child.stdout.end();
176
+ child.emit("close", 0);
177
+ // sessionFile undefined → guard 跳过(不抛错);record.sessionFile 保持未回填
178
+ await expect(promise).resolves.toBeDefined();
179
+ expect(record.sessionFile).toBeUndefined();
180
+ });
181
+ });
@@ -0,0 +1,489 @@
1
+ // src/execution/__tests__/session-runner-dispatch.test.ts
2
+ //
3
+ // [S-17a / runSpawn 阶段拆分] 重构提取辅助函数的可观察行为锁定(stdout 分派域)。
4
+ //
5
+ // 背景:session-runner.ts 按执行阶段拆分(文件顶部「runSpawn 的阶段拆分」注释块)后,
6
+ // 新提取的辅助函数(dispatchSdkEventByType / accumulateMessageEnd / handleHeaderLine /
7
+ // setupFreshChild / buildChildEnv / buildSpawnInvocation / startGetStateHandshake /
8
+ // writeAliveMarkerBestEffort)残留分支无断言锁定。本文件经 runSpawn 全链路(FakeChild
9
+ // stdout 行喂入)锁定可观察行为:
10
+ // - stdout 分派:message_end usage 累计 / stopReason=error 错误收口 / compaction_start
11
+ // 事件外发 / 空行不打 invalid 计数 / 末尾残留 invalid 行 close 时聚合留痕;
12
+ // - header 行:worktree pid 补全(onWorktreePid 收到推导 sessionFile)、回调抛错
13
+ // best-effort 不中断解析、alive marker 写失败不中断;
14
+ // - setupFreshChild:worktree pid 注册失败(防御 catch)warn 留痕 spawn 继续;
15
+ // - buildChildEnv:relay 激活时归属 env 注入子进程;
16
+ // - buildSpawnInvocation:resume model 无 / 时 provider 兜底 unknown;
17
+ // - startGetStateHandshake:[F2] get_state 写 stdin 同步 EPIPE → error 留痕 +
18
+ // 握手立即放弃,close 收尾不被阻塞。
19
+ //
20
+ // mock 布局与 session-runner-epipe.test.ts 一致(FakeChild + fs/alive-store/
21
+ // session-pending/temp-prompt mock + 共享 logger mock)。
22
+
23
+ import { spawn } from "node:child_process";
24
+ import type { ChildProcess } from "node:child_process";
25
+
26
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
27
+
28
+ const { loggerMock } = vi.hoisted(() => ({
29
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
30
+ }));
31
+ vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
32
+
33
+ vi.mock("node:child_process", async () => {
34
+ const { FakeChild } = await import("./helpers/spawn-mock.ts");
35
+ return {
36
+ spawn: vi.fn(() => new FakeChild()),
37
+ // buildEnvBlock 的 git branch 调用(execFile 异步):默认 err-first 兜底 → branch=""
38
+ execFile: vi.fn(
39
+ (
40
+ _cmd: string,
41
+ _args: readonly string[],
42
+ _opts: unknown,
43
+ cb: (err: Error | null, stdout?: string, stderr?: string) => void,
44
+ ) => cb(new Error("execFile not configured in this test")),
45
+ ),
46
+ };
47
+ });
48
+
49
+ vi.mock("node:fs", async () => {
50
+ const actual = await import("node:fs");
51
+ return {
52
+ default: {
53
+ ...actual,
54
+ mkdirSync: vi.fn(),
55
+ existsSync: vi.fn(() => false),
56
+ appendFileSync: vi.fn(),
57
+ writeFileSync: vi.fn(),
58
+ readdirSync: vi.fn(() => []),
59
+ },
60
+ mkdirSync: vi.fn(),
61
+ existsSync: vi.fn(() => false),
62
+ appendFileSync: vi.fn(),
63
+ writeFileSync: vi.fn(),
64
+ readdirSync: vi.fn(() => []),
65
+ promises: actual.promises,
66
+ };
67
+ });
68
+
69
+ vi.mock("../alive-store.ts", () => ({
70
+ writeAliveMarker: vi.fn(),
71
+ readAliveMarker: vi.fn(() => undefined),
72
+ isProcessAlive: vi.fn(() => false),
73
+ }));
74
+
75
+ vi.mock("../session-pending.ts", () => ({
76
+ readActivePendingFromSessionFile: vi.fn(() => ({ count: 0 })),
77
+ prunePendingCursor: vi.fn(),
78
+ listActivePendingFromSessionFile: vi.fn(() => ({ items: [] })),
79
+ }));
80
+
81
+ vi.mock("../engine/engines/pi/temp-prompt.ts", () => ({
82
+ writePromptToTempFile: vi.fn(async (agent: string) => {
83
+ const safeName = agent.replace(/[^\w.-]+/g, "_");
84
+ return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
85
+ }),
86
+ cleanupTempPrompt: vi.fn(async () => {}),
87
+ }));
88
+
89
+ import { writeAliveMarker } from "../alive-store.ts";
90
+ import { runSpawn, type RunOptions } from "../engine/engines/pi/session-runner.ts";
91
+ import {
92
+ RELAY_ENV_NODE,
93
+ RELAY_ENV_RECORD_ID,
94
+ RELAY_ENV_SCRIPT,
95
+ RELAY_ENV_SESSION_ID,
96
+ RELAY_ENV_SOCKET,
97
+ } from "../relay-env.ts";
98
+ import { _resetLifecycleState } from "../lifecycle-manager.ts";
99
+ import {
100
+ FakeChild,
101
+ lastSpawnedChild as lastSpawnedChildOf,
102
+ makeCtx,
103
+ makeOpts,
104
+ makeRecord,
105
+ sessionHeader,
106
+ waitForSpawn as waitForSpawnOf,
107
+ } from "./helpers/spawn-mock.ts";
108
+ import type { SessionRunnerContext } from "../engine/engines/pi/session-runner.ts";
109
+
110
+ const mockSpawn = vi.mocked(spawn);
111
+ const mockWriteAliveMarker = vi.mocked(writeAliveMarker);
112
+ const lastSpawnedChild = (): FakeChild => lastSpawnedChildOf(mockSpawn);
113
+ const waitForSpawn = (timeoutMs = 1000): Promise<void> => waitForSpawnOf(mockSpawn, timeoutMs);
114
+
115
+ /** FakeChild → spawn mock 实现(结构缺 ChildProcess 私有字段,仅类型层收窄;运行时形状由 runSpawn 消费面界定)。 */
116
+ const fakeSpawnImpl = (): ChildProcess => new FakeChild() as unknown as ChildProcess;
117
+
118
+ /** runSpawn 收尾:end 流 + close(code),await runSpawn 返回 AgentResult。 */
119
+ async function settleRunSpawn(
120
+ child: FakeChild,
121
+ promise: ReturnType<typeof runSpawn>,
122
+ code = 0,
123
+ ): Promise<Awaited<ReturnType<typeof runSpawn>>> {
124
+ child.stdout.end();
125
+ child.stderr.end();
126
+ child.emit("close", code);
127
+ return promise;
128
+ }
129
+
130
+ describe("[S-17a] stdout 分派辅助函数行为(dispatchSdkEventByType / accumulateMessageEnd)", () => {
131
+ beforeEach(() => {
132
+ vi.clearAllMocks();
133
+ // 显式重置 spawn 实现(防止个别用例的 mockImplementation 泄漏到后续用例)
134
+ mockSpawn.mockImplementation(fakeSpawnImpl);
135
+ _resetLifecycleState();
136
+ });
137
+
138
+ afterEach(() => {
139
+ _resetLifecycleState();
140
+ vi.unstubAllEnvs();
141
+ });
142
+
143
+ it("message_end 带 usage → 外发 message_end 事件(cost 从 cost.total 提取)+ totalTokens 累计", async () => {
144
+ const onEvent = vi.fn();
145
+ const record = makeRecord("sa-usage");
146
+ const promise = runSpawn(record, "Task: usage", makeOpts({ onEvent }), makeCtx());
147
+ await waitForSpawn();
148
+ const child = lastSpawnedChild();
149
+
150
+ child.stdout.write(
151
+ `${JSON.stringify({
152
+ type: "message_end",
153
+ message: {
154
+ usage: { input: 10, output: 20, cacheRead: 5, cacheWrite: 0, cost: { total: 0.42 } },
155
+ stopReason: "stop",
156
+ },
157
+ })}\n`,
158
+ );
159
+ await new Promise((r) => setImmediate(r));
160
+
161
+ // 事件外发(AgentEvent 形状):cost 字段拍平自 usage.cost.total
162
+ expect(onEvent).toHaveBeenCalledWith({
163
+ type: "message_end",
164
+ usage: { input: 10, output: 20, cacheRead: 5, cacheWrite: 0, cost: 0.42 },
165
+ });
166
+ // 累计副作用:totalTokens = input+output+cacheRead+cacheWrite
167
+ expect(record.totalTokens).toBe(35);
168
+
169
+ const result = await settleRunSpawn(child, promise);
170
+ expect(result.success).toBe(true);
171
+ });
172
+
173
+ it("message_end stopReason=error → error 事件收口进 record.lastError → runSpawn 结果 failed", async () => {
174
+ const onEvent = vi.fn();
175
+ const record = makeRecord("sa-merr");
176
+ const promise = runSpawn(record, "Task: stop-error", makeOpts({ onEvent }), makeCtx());
177
+ await waitForSpawn();
178
+ const child = lastSpawnedChild();
179
+
180
+ child.stdout.write(
181
+ `${JSON.stringify({
182
+ type: "message_end",
183
+ message: { stopReason: "error", errorMessage: "provider exploded" },
184
+ })}\n`,
185
+ );
186
+ await new Promise((r) => setImmediate(r));
187
+
188
+ // error 事件外发(errorMessage 优先,缺省回退 reason/stopReason)
189
+ expect(onEvent).toHaveBeenCalledWith({ type: "error", message: "provider exploded" });
190
+ expect(record.lastError).toBe("provider exploded");
191
+
192
+ // close 后 outcome 判定:lastError 分支 → success=false + error 原文
193
+ const result = await settleRunSpawn(child, promise, 0);
194
+ expect(result.success).toBe(false);
195
+ expect(result.error).toContain("provider exploded");
196
+ });
197
+
198
+ it("compaction_start → 外发 compaction 事件(record 无数据副作用)", async () => {
199
+ const onEvent = vi.fn();
200
+ const record = makeRecord("sa-compaction");
201
+ const promise = runSpawn(record, "Task: compaction", makeOpts({ onEvent }), makeCtx());
202
+ await waitForSpawn();
203
+ const child = lastSpawnedChild();
204
+
205
+ child.stdout.write(`${JSON.stringify({ type: "compaction_start" })}\n`);
206
+ await new Promise((r) => setImmediate(r));
207
+
208
+ expect(onEvent).toHaveBeenCalledWith({ type: "compaction" });
209
+ expect(record.lastError).toBeUndefined();
210
+
211
+ const result = await settleRunSpawn(child, promise);
212
+ expect(result.success).toBe(true);
213
+ });
214
+
215
+ it("空行不打 invalid 计数;stdout 末尾残留 invalid 行(无换行)close 时聚合 debug 留痕", async () => {
216
+ const record = makeRecord("sa-invalid");
217
+ const promise = runSpawn(record, "Task: invalid-lines", makeOpts(), makeCtx());
218
+ await waitForSpawn();
219
+ const child = lastSpawnedChild();
220
+
221
+ // 空行(parseSpawnLine 返回 null):静默跳过,不算 invalid
222
+ child.stdout.write("\n");
223
+ // 末尾残留半行(无换行,close 前未 flush):非法 JSON → close 路径计入 invalid 统计
224
+ child.stdout.write("this is not json at all");
225
+ await new Promise((r) => setImmediate(r));
226
+
227
+ loggerMock.debug.mockClear();
228
+ const result = await settleRunSpawn(child, promise, 0);
229
+
230
+ expect(result.success).toBe(true);
231
+ // [LC-9/T7②] close 聚合留痕:总数 + 样本(事件行损坏被静默丢弃的排查入口)
232
+ expect(loggerMock.debug).toHaveBeenCalledWith(
233
+ expect.stringContaining("stdout had 1 invalid line(s) dropped in total"),
234
+ );
235
+ });
236
+ });
237
+
238
+ describe("[S-17a] handleHeaderLine:header 行分派 + worktree pid 补全", () => {
239
+ const WORKTREE = {
240
+ path: "/tmp/wt-checkout",
241
+ branch: "wt/dispatch-1",
242
+ baseCommit: "abc123",
243
+ mainCwd: "/tmp/test",
244
+ } as const;
245
+
246
+ beforeEach(() => {
247
+ vi.clearAllMocks();
248
+ mockSpawn.mockImplementation(fakeSpawnImpl);
249
+ _resetLifecycleState();
250
+ });
251
+
252
+ afterEach(() => {
253
+ _resetLifecycleState();
254
+ });
255
+
256
+ it("header 到达 → sessionFile 推导回填 + onWorktreePid 收到 (branch, pid, 推导路径)", async () => {
257
+ const onWorktreePid = vi.fn();
258
+ const ctx = makeCtx({ onWorktreePid } as Partial<SessionRunnerContext>);
259
+ const opts = makeOpts({ worktree: WORKTREE });
260
+ const record = makeRecord("sa-wt-header");
261
+ const promise = runSpawn(record, "Task: wt-header", opts, ctx);
262
+ await waitForSpawn();
263
+ const child = lastSpawnedChild();
264
+
265
+ child.stdout.write(`${JSON.stringify(sessionHeader("sa-sess-wt"))}\n`);
266
+ await new Promise((r) => setImmediate(r));
267
+
268
+ // header 分支的 worktree pid 补全:带推导出的 sessionFile(registry entry 数据源)
269
+ expect(onWorktreePid).toHaveBeenCalledWith(
270
+ "wt/dispatch-1",
271
+ child.pid,
272
+ expect.stringContaining("sa-sess-wt"),
273
+ );
274
+ expect(record.sessionFile).toContain("sa-sess-wt");
275
+
276
+ await settleRunSpawn(child, promise);
277
+ });
278
+
279
+ it("onWorktreePid 回调同步抛错 → best-effort error 留痕,stdout 解析不中断(后续事件照常分派)", async () => {
280
+ const onEvent = vi.fn();
281
+ // setupFreshChild 内两处注册调用先成功,header 分支的第三处抛错
282
+ const onWorktreePid = vi
283
+ .fn()
284
+ .mockReturnValueOnce(undefined)
285
+ .mockReturnValueOnce(undefined)
286
+ .mockImplementation(() => {
287
+ throw new Error("registry lock contention");
288
+ });
289
+ const ctx = makeCtx({ onWorktreePid } as Partial<SessionRunnerContext>);
290
+ const opts = makeOpts({ worktree: WORKTREE });
291
+ const promise = runSpawn(makeRecord("sa-wt-throw"), "Task: wt-throw", makeOpts({ ...opts, onEvent }), ctx);
292
+ await waitForSpawn();
293
+ const child = lastSpawnedChild();
294
+
295
+ child.stdout.write(`${JSON.stringify(sessionHeader("sa-sess-wt2"))}\n`);
296
+ child.stdout.write(`${JSON.stringify({ type: "compaction_start" })}\n`);
297
+ await new Promise((r) => setImmediate(r));
298
+
299
+ // bestEffort 缺省 debug 级(msg + { detail } 两参):同步段异常不阻断 stdout 解析
300
+ expect(loggerMock.debug).toHaveBeenCalledWith(
301
+ expect.stringContaining("onWorktreePid callback (first header) failed"),
302
+ expect.objectContaining({ detail: "registry lock contention" }),
303
+ );
304
+ // 解析继续:header 之后的 compaction 事件照常外发
305
+ expect(onEvent).toHaveBeenCalledWith({ type: "compaction" });
306
+
307
+ await settleRunSpawn(child, promise);
308
+ });
309
+
310
+ it("alive marker 写失败(writeAliveMarker 抛错)→ best-effort debug 留痕,header 回填与解析照常", async () => {
311
+ const onEvent = vi.fn();
312
+ mockWriteAliveMarker.mockImplementation(() => {
313
+ throw new Error("disk full");
314
+ });
315
+ const record = makeRecord("sa-marker-fail");
316
+ const promise = runSpawn(record, "Task: marker-fail", makeOpts({ onEvent }), makeCtx());
317
+ await waitForSpawn();
318
+ const child = lastSpawnedChild();
319
+
320
+ child.stdout.write(`${JSON.stringify(sessionHeader("sa-sess-mf"))}\n`);
321
+ child.stdout.write(`${JSON.stringify({ type: "compaction_start" })}\n`);
322
+ await new Promise((r) => setImmediate(r));
323
+
324
+ // [持久化 C] marker 是崩溃恢复的增强信号,缺失只降低可恢复性,不影响执行主流程
325
+ expect(loggerMock.debug).toHaveBeenCalledWith(
326
+ expect.stringContaining("alive marker write failed (best-effort continue)"),
327
+ );
328
+ expect(record.sessionFile).toContain("sa-sess-mf");
329
+ expect(onEvent).toHaveBeenCalledWith({ type: "compaction" });
330
+
331
+ await settleRunSpawn(child, promise);
332
+ });
333
+ });
334
+
335
+ describe("[S-17a] setupFreshChild:worktree pid 注册防御 catch", () => {
336
+ beforeEach(() => {
337
+ vi.clearAllMocks();
338
+ mockSpawn.mockImplementation(fakeSpawnImpl);
339
+ _resetLifecycleState();
340
+ });
341
+
342
+ afterEach(() => {
343
+ _resetLifecycleState();
344
+ });
345
+
346
+ it("第二处 onWorktreePid 注册抛错(防御 catch)→ warn 留痕,spawn 主流程不被阻断", async () => {
347
+ // 第一处(无保护)成功,第二处(try/catch 保护)抛错
348
+ const onWorktreePid = vi
349
+ .fn()
350
+ .mockReturnValueOnce(undefined)
351
+ .mockImplementation(() => {
352
+ throw new Error("registry write failed");
353
+ });
354
+ const ctx = makeCtx({ onWorktreePid } as Partial<SessionRunnerContext>);
355
+ const opts = makeOpts({
356
+ worktree: { path: "/tmp/wt-checkout", branch: "wt/defensive", baseCommit: "abc", mainCwd: "/tmp/test" },
357
+ });
358
+ const record = makeRecord("sa-wt-defensive");
359
+ const promise = runSpawn(record, "Task: wt-defensive", opts, ctx);
360
+ await waitForSpawn();
361
+ const child = lastSpawnedChild();
362
+
363
+ // [S1] 注册表写失败最坏后果是条目停留 pid=0(reaper 宽限兜底),不阻断 spawn
364
+ expect(loggerMock.warn).toHaveBeenCalledWith(
365
+ expect.stringContaining("worktree pid registration failed (defensive)"),
366
+ expect.objectContaining({ branch: "wt/defensive" }),
367
+ );
368
+ // prompt 命令已照常写入 stdin(spawn 装配未被中断)
369
+ expect(child.stdin.writable).toBe(true);
370
+
371
+ const result = await settleRunSpawn(child, promise);
372
+ expect(result.success).toBe(true);
373
+ });
374
+ });
375
+
376
+ describe("[S-17a] buildChildEnv / buildSpawnInvocation:env 与参数组装", () => {
377
+ beforeEach(() => {
378
+ vi.clearAllMocks();
379
+ mockSpawn.mockImplementation(fakeSpawnImpl);
380
+ _resetLifecycleState();
381
+ });
382
+
383
+ afterEach(() => {
384
+ _resetLifecycleState();
385
+ vi.unstubAllEnvs();
386
+ });
387
+
388
+ it("relay 激活(SOCKET/NODE/SCRIPT 三 env 齐备)→ 子进程 env 注入归属键 SESSION_ID/RECORD_ID", async () => {
389
+ vi.stubEnv(RELAY_ENV_SOCKET, "/tmp/relay.sock");
390
+ vi.stubEnv(RELAY_ENV_NODE, "node-1");
391
+ vi.stubEnv(RELAY_ENV_SCRIPT, "/tmp/relay.mjs");
392
+ const record = makeRecord("sa-relay");
393
+ const promise = runSpawn(record, "Task: relay-env", makeOpts(), makeCtx());
394
+ await waitForSpawn();
395
+
396
+ const call = mockSpawn.mock.calls.at(-1);
397
+ expect(call).toBeDefined();
398
+ const env = call?.[2]?.env as Record<string, string | undefined> | undefined;
399
+ // [E 方案 §5.2-2] tee 帧路由键:SESSION_ID = ctx.sessionRootId、RECORD_ID = record.id
400
+ expect(env?.[RELAY_ENV_SESSION_ID]).toBe("root-session-test");
401
+ expect(env?.[RELAY_ENV_RECORD_ID]).toBe("sa-relay");
402
+
403
+ await settleRunSpawn(lastSpawnedChild(), promise);
404
+ });
405
+
406
+ it("relay 未激活 → 归属 env 不注入(未激活环境下携带 record 值 env 是误导噪声)", async () => {
407
+ const promise = runSpawn(makeRecord("sa-norelay"), "Task: no-relay", makeOpts(), makeCtx());
408
+ await waitForSpawn();
409
+
410
+ const call = mockSpawn.mock.calls.at(-1);
411
+ const env = call?.[2]?.env as Record<string, string | undefined> | undefined;
412
+ expect(env?.[RELAY_ENV_SESSION_ID]).toBeUndefined();
413
+ expect(env?.[RELAY_ENV_RECORD_ID]).toBeUndefined();
414
+
415
+ await settleRunSpawn(lastSpawnedChild(), promise);
416
+ });
417
+
418
+ it("resume model 无 / 分隔(异常形态回显)→ provider 兜底 unknown,--model 仍可执行", async () => {
419
+ const promise = runSpawn(
420
+ makeRecord("sa-resume-noslash"),
421
+ "Task: resume",
422
+ makeOpts() as RunOptions,
423
+ makeCtx(),
424
+ { sessionFile: "/tmp/prev-session.jsonl", model: "legacy-model-no-slash" },
425
+ );
426
+ await waitForSpawn();
427
+
428
+ const call = mockSpawn.mock.calls.at(-1);
429
+ const args = call?.[1] as string[];
430
+ // [U1 D2 豁免] resume 回显不经 registry 断言,无 / 时按 unknown provider 兜底
431
+ //(与 subagent-service record 回读同构)
432
+ const modelIdx = args.indexOf("--model");
433
+ expect(modelIdx).toBeGreaterThan(-1);
434
+ expect(args[modelIdx + 1]).toBe("unknown/legacy-model-no-slash");
435
+ // resume 锁定 sessionFile:--session 参数追加
436
+ const sessionIdx = args.indexOf("--session");
437
+ expect(args[sessionIdx + 1]).toBe("/tmp/prev-session.jsonl");
438
+
439
+ await settleRunSpawn(lastSpawnedChild(), promise);
440
+ });
441
+ });
442
+
443
+ describe("[S-17a] startGetStateHandshake:[F2] 同步写失败兜底", () => {
444
+ beforeEach(() => {
445
+ vi.clearAllMocks();
446
+ _resetLifecycleState();
447
+ });
448
+
449
+ afterEach(() => {
450
+ _resetLifecycleState();
451
+ });
452
+
453
+ it("get_state 写 stdin 同步 EPIPE(stdin 已断)→ error 留痕 + 握手立即放弃,close 收尾不被阻塞", async () => {
454
+ // spawn 即返回 stdin 已断语义的 child:对 get_state 命令的 write 同步抛 EPIPE
455
+ //(prompt 命令写入正常,区分 writeStdinLine 的两条调用链)
456
+ mockSpawn.mockImplementation((): ChildProcess => {
457
+ const c = new FakeChild();
458
+ const realWrite = c.stdin.write.bind(c.stdin);
459
+ c.stdin.write = ((chunk: string | Uint8Array, ...rest: unknown[]) => {
460
+ if (typeof chunk === "string" && chunk.includes('"get_state"')) {
461
+ const err = new Error("write EPIPE") as NodeJS.ErrnoException;
462
+ err.code = "EPIPE";
463
+ throw err;
464
+ }
465
+ return realWrite(chunk as Buffer, ...(rest as []));
466
+ }) as typeof c.stdin.write;
467
+ return c as unknown as ChildProcess;
468
+ });
469
+
470
+ const record = makeRecord("sa-hs-epipe");
471
+ const promise = runSpawn(record, "Task: hs-epipe", makeOpts(), makeCtx());
472
+ await waitForSpawn();
473
+ const child = lastSpawnedChild();
474
+ await new Promise((r) => setImmediate(r));
475
+
476
+ // [F2] unhandledRejection 防线:executor 同步异常转 reject 后被 .catch 接住,
477
+ // 错误消息带 EPIPE 语义与冷路径恢复指引
478
+ expect(loggerMock.error).toHaveBeenCalledWith(
479
+ expect.stringContaining("get_state handshake failed"),
480
+ );
481
+ expect(loggerMock.error).toHaveBeenCalledWith(expect.stringContaining("EPIPE"));
482
+
483
+ // 握手已放弃(abandonHandshake):close 收尾不被阻塞,runSpawn 正常 resolve
484
+ const result = await settleRunSpawn(child, promise, 0);
485
+ expect(result.success).toBe(true);
486
+ // sessionFile 未回填(握手失败形态,收尾由 existsSync + findSessionFileByHeaderId 兜底)
487
+ expect(record.sessionFile).toBeUndefined();
488
+ });
489
+ });
@@ -54,9 +54,10 @@ vi.mock("../alive-store.ts", () => ({ writeAliveMarker: vi.fn() }));
54
54
 
55
55
  vi.mock("../session-pending.ts", () => ({
56
56
  readActivePendingFromSessionFile: vi.fn(() => ({ count: 0 })),
57
+ prunePendingCursor: vi.fn(),
57
58
  }));
58
59
 
59
- vi.mock("../temp-prompt.ts", () => ({
60
+ vi.mock("../engine/engines/pi/temp-prompt.ts", () => ({
60
61
  writePromptToTempFile: vi.fn(async (agent: string) => {
61
62
  const safeName = agent.replace(/[^\w.-]+/g, "_");
62
63
  return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
@@ -68,8 +69,8 @@ import {
68
69
  EPIPE_FAILURE_THRESHOLD,
69
70
  recordEpipeFailure,
70
71
  resetAllEpipeFailures,
71
- } from "../stdin-writer.ts";
72
- import { runSpawn, spawnedChildren } from "../session-runner.ts";
72
+ } from "../engine/engines/pi/stdin-writer.ts";
73
+ import { runSpawn, spawnedChildren } from "../engine/engines/pi/session-runner.ts";
73
74
  import { _resetLifecycleState } from "../lifecycle-manager.ts";
74
75
  import {
75
76
  FakeChild,