@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,272 @@
1
+ // src/execution/__tests__/execution-runtime-face.test.ts
2
+ //
3
+ // U10① execution 运行时面导出形态测试 = 第三宿主最小构造示例(验收证据)。
4
+ //
5
+ // 示例语义(设计 docs/design/subagent-core-sink-design.md §3.3 D6 / §4 S5):第三宿主
6
+ // 仅凭模块导出面(不看 core 内部实现)完成——
7
+ // 1. createSubagentService({cwd, modelService, ...}) 参数注入构造(无全局查找);
8
+ // 2. record 状态查询面调用(SubagentService.lookupRecordAnyState 按 id 全态查询 +
9
+ // RecordStore 按状态枚举 listRunning/collectRecords 与按 id findLightById/getFullRecord);
10
+ // 3. 错误类型族 instanceof 分流。
11
+ //
12
+ // 构造先例同 subagent-service.test.ts:真实 ModelConfigService 指向 tmpdir 空目录
13
+ // (loadGlobalConfig 对不存在文件返回默认配置,AgentRegistry 空目录安全),不 mock
14
+ // service 内核;pi 仅在 initSession 场景以最小 duck-typed mock 注入。
15
+
16
+ import * as fs from "node:fs";
17
+ import * as os from "node:os";
18
+ import * as path from "node:path";
19
+
20
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
21
+
22
+ // MF-4:barrel 可达性用例的命名空间导入——「仅凭 src/index.ts 导入」的字面载体
23
+ //(不经深路径),证明第三宿主按文档用法可构造。
24
+ import * as subagentCore from "../../index.ts";
25
+ import { EngineError } from "../engine/common/errors.ts";
26
+ import { EngineNotFoundError } from "../engine/registry.ts";
27
+ import { ZcodePrepareError } from "../engine/engines/zcode/preparer.ts";
28
+ import { ZcodeReaderError } from "../engine/engines/zcode/reader.ts";
29
+ import { ZcodeTaskShapeError } from "../engine/engines/zcode/zcode-engine.ts";
30
+ import { ModelConfigService } from "../model-config-service.ts";
31
+ import {
32
+ SUBAGENT_RECORD_CUSTOM_TYPE,
33
+ toSubagentRecordEntry,
34
+ } from "../record-entry.ts";
35
+ import { RecordStore } from "../record-store.ts";
36
+ import { createSubagentService, getSubagentService } from "../subagent-service.ts";
37
+ import {
38
+ DirtyWorktreeError,
39
+ ForkDepthExceededError,
40
+ ResurrectDeniedError,
41
+ } from "../types.ts";
42
+ import { GitRunError as WorktreeManagerGitRunError } from "../worktree-manager.ts";
43
+ import { GitRunError as WorktreeGitOpsGitRunError } from "../worktree-git-ops.ts";
44
+
45
+ // ── 工具:tmpdir agentDir + 真实 ModelConfigService(第三宿主自备的最小依赖集)──
46
+
47
+ function makeAgentDir(): string {
48
+ return fs.mkdtempSync(path.join(os.tmpdir(), "exec-runtime-face-"));
49
+ }
50
+
51
+ function makeModelService(agentDir: string): ModelConfigService {
52
+ // cwd 为 ModelConfigServiceInit 必填键(与 agentDir 同指 tmp 目录;构造期仅消费 agentDir)
53
+ return new ModelConfigService({ agentDir, cwd: agentDir });
54
+ }
55
+
56
+ /** 最小 PiLike duck-type(initSession 场景用;本文件不触发真实 pi 调用)。 */
57
+ function makePi() {
58
+ return {
59
+ appendEntry: vi.fn(),
60
+ events: { emit: vi.fn() },
61
+ sendMessage: vi.fn(),
62
+ };
63
+ }
64
+
65
+ describe("execution 运行时面(U10① D6)", () => {
66
+ let agentDir: string;
67
+
68
+ beforeEach(() => {
69
+ agentDir = makeAgentDir();
70
+ });
71
+
72
+ afterEach(() => {
73
+ fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
74
+ });
75
+
76
+ // ============================================================
77
+ // ① createSubagentService 工厂:参数注入构造(第三宿主最小构造示例主体)
78
+ // ============================================================
79
+ describe("createSubagentService 工厂(参数注入构造)", () => {
80
+ it("仅凭参数注入构造成功,不读写全局单例槽位(无全局查找)", () => {
81
+ // 前置:全局槽位为空(工厂与 getSubagentService/setSubagentService 单例流程解耦)
82
+ expect(getSubagentService()).toBeNull();
83
+
84
+ const service = createSubagentService({
85
+ cwd: agentDir,
86
+ modelService: makeModelService(agentDir),
87
+ });
88
+
89
+ // 构造产物可用(实例方法存在)且未被登记进全局单例槽位——
90
+ // session_start 单例流程行为零改动,宿主自持实例。
91
+ expect(typeof service["lookupRecordAnyState"]).toBe("function");
92
+ expect(getSubagentService()).toBeNull();
93
+ });
94
+
95
+ it("构造后即可调用按 id 全态查询面:空 store 返回 undefined(未 initSession 按「不存在」处理)", () => {
96
+ const service = createSubagentService({
97
+ cwd: agentDir,
98
+ modelService: makeModelService(agentDir),
99
+ });
100
+
101
+ // lookupRecordAnyState 契约:未初始化/disposed 时 assertReady 抛错 → 按「不存在」处理返回 undefined
102
+ expect(service["lookupRecordAnyState"]("no-such-record")).toBeUndefined();
103
+ });
104
+
105
+ it("initSession(宿主注入最小 pi mock)后查询面同型可用", () => {
106
+ const service = createSubagentService({
107
+ cwd: agentDir,
108
+ modelService: makeModelService(agentDir),
109
+ });
110
+ service.initSession({ pi: makePi(), sessionId: "s1" });
111
+
112
+ expect(service["lookupRecordAnyState"]("no-such-record")).toBeUndefined();
113
+ service.dispose();
114
+ });
115
+ });
116
+
117
+ // ============================================================
118
+ // ② record 状态查询面:按状态枚举 / 按 id(RecordStore 公共构造)
119
+ // ============================================================
120
+ describe("record 状态查询面(RecordStore)", () => {
121
+ it("按状态枚举查询:空 store 下 listRunning 与 collectRecords 返回空数组", () => {
122
+ const sessionsDir = path.join(agentDir, "sessions");
123
+ fs.mkdirSync(sessionsDir, { recursive: true });
124
+ const store = new RecordStore(sessionsDir);
125
+
126
+ expect(store.listRunning()).toEqual([]);
127
+ expect(store.collectRecords(10, "all", undefined)).toEqual([]);
128
+ expect(store.collectRecords(10, "running", undefined)).toEqual([]);
129
+ });
130
+
131
+ it("按 id 查询:findLightById / getFullRecord / getMutable 对缺失 id 均返回 undefined", () => {
132
+ const sessionsDir = path.join(agentDir, "sessions");
133
+ fs.mkdirSync(sessionsDir, { recursive: true });
134
+ const store = new RecordStore(sessionsDir);
135
+
136
+ expect(store.findLightById("missing-id")).toBeUndefined();
137
+ expect(store.getFullRecord("missing-id")).toBeUndefined();
138
+ expect(store.getMutable("missing-id")).toBeUndefined();
139
+ });
140
+ });
141
+
142
+ // ============================================================
143
+ // ③ 错误类型族:instanceof 分流(导出形态 = 宿主可程序化判别)
144
+ // ============================================================
145
+ describe("错误类型族 instanceof", () => {
146
+ it("EngineError:code 前缀 message + 结构化投影", () => {
147
+ const err = new EngineError("model_not_available", "no provider", "pick another model");
148
+ expect(err).toBeInstanceOf(Error);
149
+ expect(err).toBeInstanceOf(EngineError);
150
+ expect(err.message).toBe("model_not_available: no provider");
151
+ expect(err.toStructured()).toEqual({
152
+ code: "model_not_available",
153
+ message: "model_not_available: no provider",
154
+ recovery: "pick another model",
155
+ });
156
+ });
157
+
158
+ it("EngineNotFoundError:携带 engineId / registered / source 供分流", () => {
159
+ const err = new EngineNotFoundError("nope", ["pi", "zcode"], "agent.md");
160
+ expect(err).toBeInstanceOf(Error);
161
+ expect(err).toBeInstanceOf(EngineNotFoundError);
162
+ expect(err.code).toBe("engine_not_found");
163
+ expect(err.engineId).toBe("nope");
164
+ expect(err.registered).toEqual(["pi", "zcode"]);
165
+ expect(err.source).toBe("agent.md");
166
+ });
167
+
168
+ it("zcode 引擎错误族:ZcodePrepareError / ZcodeReaderError / ZcodeTaskShapeError", () => {
169
+ const prepare = new ZcodePrepareError("model_not_available", "model gone");
170
+ expect(prepare).toBeInstanceOf(Error);
171
+ expect(prepare).toBeInstanceOf(ZcodePrepareError);
172
+ expect(prepare.code).toBe("model_not_available");
173
+
174
+ const reader = new ZcodeReaderError("db missing", "check journal");
175
+ expect(reader).toBeInstanceOf(Error);
176
+ expect(reader).toBeInstanceOf(ZcodeReaderError);
177
+
178
+ // [U10①] 原 class 未导出(仅内部 throw),本断言钉住 export 补齐后宿主可 instanceof
179
+ const shape = new ZcodeTaskShapeError("capability_unsupported", "no schema");
180
+ expect(shape).toBeInstanceOf(Error);
181
+ expect(shape).toBeInstanceOf(ZcodeTaskShapeError);
182
+ expect(shape.code).toBe("capability_unsupported");
183
+ expect(shape.name).toBe("ZcodeTaskShapeError");
184
+ });
185
+
186
+ it("执行守卫错误族:ResurrectDeniedError / ForkDepthExceededError / DirtyWorktreeError", () => {
187
+ const resurrect = new ResurrectDeniedError();
188
+ expect(resurrect).toBeInstanceOf(Error);
189
+ expect(resurrect).toBeInstanceOf(ResurrectDeniedError);
190
+
191
+ const forkDepth = new ForkDepthExceededError("depth > max");
192
+ expect(forkDepth).toBeInstanceOf(Error);
193
+ expect(forkDepth).toBeInstanceOf(ForkDepthExceededError);
194
+
195
+ const dirty = new DirtyWorktreeError("uncommitted changes");
196
+ expect(dirty).toBeInstanceOf(Error);
197
+ expect(dirty).toBeInstanceOf(DirtyWorktreeError);
198
+ });
199
+
200
+ it("GitRunError 两处同名列(worktree-manager / worktree-git-ops)各自可 instanceof", () => {
201
+ const fromManager = new WorktreeManagerGitRunError("git fail", { exitCode: 1 });
202
+ expect(fromManager).toBeInstanceOf(Error);
203
+ expect(fromManager).toBeInstanceOf(WorktreeManagerGitRunError);
204
+ expect(fromManager).not.toBeInstanceOf(WorktreeGitOpsGitRunError);
205
+
206
+ const fromGitOps = new WorktreeGitOpsGitRunError("git fail", { exitCode: 128 });
207
+ expect(fromGitOps).toBeInstanceOf(Error);
208
+ expect(fromGitOps).toBeInstanceOf(WorktreeGitOpsGitRunError);
209
+ expect(fromGitOps).not.toBeInstanceOf(WorktreeManagerGitRunError);
210
+ });
211
+ });
212
+
213
+ // ============================================================
214
+ // ④ record-entry 自描述 entry 面(导出形态确认;投影逐字段等值由
215
+ // execution-record.test.ts 钉住,此处不重复)
216
+ // ============================================================
217
+ describe("record-entry 面", () => {
218
+ it("SUBAGENT_RECORD_CUSTOM_TYPE 值契约:'subagent-record'", () => {
219
+ expect(SUBAGENT_RECORD_CUSTOM_TYPE).toBe("subagent-record");
220
+ });
221
+
222
+ it("toSubagentRecordEntry 可达且对最小 SubagentRecord 投影出 v===1", () => {
223
+ const entry = toSubagentRecordEntry({
224
+ id: "r1",
225
+ agent: "a.md",
226
+ task: "t",
227
+ slug: "s",
228
+ status: "running",
229
+ mode: "background",
230
+ startedAt: 1,
231
+ rootSessionId: undefined,
232
+ parentRecordId: undefined,
233
+ depth: 0,
234
+ endedAt: undefined,
235
+ turns: 0,
236
+ totalTokens: 0,
237
+ model: "m",
238
+ thinkingLevel: undefined,
239
+ eventLog: [],
240
+ displayItems: [],
241
+ });
242
+ expect(entry.v).toBe(1);
243
+ expect(entry.id).toBe("r1");
244
+ });
245
+ });
246
+
247
+ // ============================================================
248
+ // ⑤ barrel 可达性(MF-4):仅凭 src/index.ts 导入完成第三宿主构造
249
+ // (SubagentServiceInit.modelService: ModelConfigService——构造依赖
250
+ // 与 jsdoc 示例引用的访问器必须全部从 barrel 可达)
251
+ // ============================================================
252
+ describe("barrel 可达性(仅凭 src/index.ts 导入,MF-4)", () => {
253
+ it("ModelConfigService / getModelConfigService / createSubagentService 从 barrel 可达并可构造", () => {
254
+ // 符号面:类与访问器均为函数(导出存在性)
255
+ expect(typeof subagentCore.ModelConfigService).toBe("function");
256
+ expect(typeof subagentCore.getModelConfigService).toBe("function");
257
+
258
+ // 进程单例槽位未被本文件写入(同 ① 的全局查找断言口径)
259
+ expect(subagentCore.getModelConfigService()).toBeNull();
260
+
261
+ // 运行时可达:仅凭 barrel 符号完成参数注入构造(ModelConfigServiceInit
262
+ // = { cwd, agentDir },指向 tmp 目录——loadGlobalConfig 对不存在文件
263
+ // 返回默认配置,AgentRegistry 空目录安全,同文件头既有 stub 口径)
264
+ const modelService = new subagentCore.ModelConfigService({ cwd: agentDir, agentDir });
265
+ const service = subagentCore.createSubagentService({ cwd: agentDir, modelService });
266
+
267
+ // 构造产物可用(实例方法存在),且不写进程单例槽位(第三宿主自持实例)
268
+ expect(typeof service["lookupRecordAnyState"]).toBe("function");
269
+ expect(subagentCore.getModelConfigService()).toBeNull();
270
+ });
271
+ });
272
+ });
@@ -33,14 +33,14 @@ vi.mock("node:child_process", async () => {
33
33
  });
34
34
 
35
35
  // buildSpawnArgs 包装为 spy(保留真实实现)——「显式 ref 拒单不触达 spawn 参数组装」的承重断言。
36
- vi.mock("../session-runner.ts", async (importOriginal) => {
37
- const actual = await importOriginal<typeof import("../session-runner.ts")>();
36
+ vi.mock("../engine/engines/pi/session-runner.ts", async (importOriginal) => {
37
+ const actual = await importOriginal<typeof import("../engine/engines/pi/session-runner.ts")>();
38
38
  return { ...actual, buildSpawnArgs: vi.fn(actual.buildSpawnArgs) };
39
39
  });
40
40
 
41
41
  import { ModelConfigService } from "../model-config-service.ts";
42
42
  import type { ModelInfo, ModelRegistryLike } from "../model-resolver.ts";
43
- import { buildSpawnArgs } from "../session-runner.ts";
43
+ import { buildSpawnArgs } from "../engine/engines/pi/session-runner.ts";
44
44
  import type { PiLike } from "../subagent-service.ts";
45
45
  import { SubagentService } from "../subagent-service.ts";
46
46
 
@@ -26,7 +26,8 @@ vi.mock("../../core/logger.ts", () => ({
26
26
 
27
27
  import { doFinalizeRecord, doFinalizeRoundToIdle } from "../finalize-record.ts";
28
28
  import { ManifestStore } from "../manifest-store.ts";
29
- import type { AgentResult, ExecutionRecord } from "../types.ts";
29
+ import { getSubagentSessionDir } from "../path-encoding.ts";
30
+ import type { AgentResult, ExecutionRecord, WorktreeHandle } from "../types.ts";
30
31
 
31
32
  function makeMinimalRecord(overrides: Partial<ExecutionRecord> = {}): ExecutionRecord {
32
33
  return {
@@ -36,7 +37,6 @@ function makeMinimalRecord(overrides: Partial<ExecutionRecord> = {}): ExecutionR
36
37
  thinkingLevel: undefined,
37
38
  mode: "background",
38
39
  task: "test",
39
- slug: "test",
40
40
  startedAt: 1000,
41
41
  rootSessionId: "session-main",
42
42
  parentRecordId: undefined,
@@ -77,19 +77,19 @@ describe("doFinalizeRecord — manifest status 透传 (M3 4 态)", () => {
77
77
  });
78
78
 
79
79
  afterEach(() => {
80
- fs.rmSync(tmpDir, { recursive: true, force: true });
80
+ fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
81
81
  });
82
82
 
83
83
  /** 构造最小 FinalizeDeps:record 无 sessionFile/worktreeHandle,跳过 Step 0/3 文件操作。 */
84
- function makeDeps() {
84
+ function makeDeps(): Parameters<typeof doFinalizeRecord>[0] {
85
85
  return {
86
86
  manifestStore,
87
- worktreeManager: {} as never,
88
- store: { archive: vi.fn(), reportRecordTransition: vi.fn() } as never,
89
- modelService: {} as never,
87
+ worktreeManager: {},
88
+ store: { archive: vi.fn(), reportRecordTransition: vi.fn() },
89
+ modelService: {},
90
90
  pi: { appendEntry: vi.fn() },
91
91
  emitUnregister: vi.fn(),
92
- };
92
+ } as unknown as Parameters<typeof doFinalizeRecord>[0];
93
93
  }
94
94
 
95
95
  it("status=closed + cancelled reason → manifest 写 closed(v4 B-1:cancelled 折入 closed)", async () => {
@@ -165,7 +165,7 @@ describe("doFinalizeRecord — manifest status 透传 (M3 4 态)", () => {
165
165
  expect(errMsg).toContain("disk full");
166
166
 
167
167
  // ── 核心 claim 6:pi.appendEntry 记录 "subagent:manifest-write-failed" 事件 ──
168
- expect(deps.pi.appendEntry).toHaveBeenCalledWith(
168
+ expect(deps.pi!.appendEntry).toHaveBeenCalledWith(
169
169
  "subagent:manifest-write-failed",
170
170
  expect.objectContaining({ id: "rec-cleanup-first", error: "disk full" }),
171
171
  );
@@ -181,6 +181,252 @@ describe("doFinalizeRecord — manifest status 透传 (M3 4 态)", () => {
181
181
  // 清理 mock 调用记录防污染
182
182
  loggerMock.error.mockClear();
183
183
  });
184
+
185
+ // ── [T1/PS-9] sessionFile 缺失 → sessionDir 反查后 marker/alive 清理仍落地 ──
186
+ //
187
+ // PS-9:finalize 的 tombstone/finalized sidecar 与 removeAliveMarker 全部 gated on
188
+ // record.sessionFile——RC-1(握手失败)+ LC-4(收尾反查未命中)的残余形态下,
189
+ // 「sessionFile 缺失 → 终态原因丢失 + alive marker 残留」。修复:sessionFile 缺失时
190
+ // 用 deps.sessionDir 按 identity(record.id)反查真实 session 文件作依据。
191
+ // 真实 fs + tmp 目录(与文件既有策略一致):session JSONL 写入 identity custom entry
192
+ //(子进程 session_start hook 的写入形态),readIdentityHeader/readIdentityTail 真实解析。
193
+ describe("[T1/PS-9] sessionFile 缺失 sessionDir 反查", () => {
194
+ let sessionDir: string;
195
+
196
+ beforeEach(() => {
197
+ sessionDir = fs.mkdtempSync(path.join(os.tmpdir(), "finalize-ps9-sessions-"));
198
+ });
199
+
200
+ afterEach(() => {
201
+ fs.rmSync(sessionDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
202
+ });
203
+
204
+ /** 在 sessionDir 写入带 identity 的 session 文件(1 行 identity entry,位于头部)。 */
205
+ function writeSessionFileWithIdentity(fileName: string, recordId: string): string {
206
+ const sessionFile = path.join(sessionDir, fileName);
207
+ const identityEntry = JSON.stringify({
208
+ type: "custom",
209
+ customType: "subagent-identity",
210
+ data: { id: recordId, agent: "worker", mode: "background", task: "t", startedAt: 1000 },
211
+ });
212
+ fs.writeFileSync(sessionFile, `${identityEntry}\n`, "utf-8");
213
+ return sessionFile;
214
+ }
215
+
216
+ it("sessionFile 缺失 + 反查命中 → .finalized 落盘 + .alive 清理 + record/manifest 回填", async () => {
217
+ const sessionFile = writeSessionFileWithIdentity("20260901T12000000_ps9.jsonl", "rec-ps9");
218
+ // 预写 .alive 残留(模拟 running 期崩溃恢复窗口的 marker)
219
+ fs.writeFileSync(
220
+ `${sessionFile}.alive`,
221
+ `${JSON.stringify({ pid: 99999, id: "rec-ps9", startedAt: 1000 })}\n`,
222
+ "utf-8",
223
+ );
224
+ const record = makeMinimalRecord({ id: "rec-ps9", sessionFile: undefined });
225
+ const deps = { ...makeDeps(), sessionDir };
226
+
227
+ await doFinalizeRecord(deps, record, makeMinimalResult(), "closed", "gc");
228
+
229
+ // 反查回填 record.sessionFile
230
+ expect(record.sessionFile).toBe(sessionFile);
231
+ // 终态原因持久化不再丢失(closedReason gc 写入 .finalized 内容)
232
+ expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true);
233
+ expect(fs.readFileSync(`${sessionFile}.finalized`, "utf-8")).toBe("gc");
234
+ // alive marker 不再残留
235
+ expect(fs.existsSync(`${sessionFile}.alive`)).toBe(false);
236
+ // manifest 拿到真实 sessionFile(诊断/重建源不再失真)
237
+ const manifest = await manifestStore.readManifest("rec-ps9");
238
+ expect(manifest?.sessionFile).toBe(sessionFile);
239
+ });
240
+
241
+ it("cancelled + 反查命中 → tombstone 写到反查路径(cancelled 语义不因 sessionFile 缺失丢失)", async () => {
242
+ const sessionFile = writeSessionFileWithIdentity("20260901T12000001_ps9c.jsonl", "rec-ps9c");
243
+ const record = makeMinimalRecord({ id: "rec-ps9c", sessionFile: undefined });
244
+ const deps = { ...makeDeps(), sessionDir };
245
+
246
+ await doFinalizeRecord(deps, record, makeMinimalResult(), "closed", "cancelled");
247
+
248
+ expect(record.sessionFile).toBe(sessionFile);
249
+ expect(fs.existsSync(`${sessionFile}.cancelled`)).toBe(true);
250
+ const tomb = JSON.parse(
251
+ fs.readFileSync(`${sessionFile}.cancelled`, "utf-8"),
252
+ ) as { id: string; status: string };
253
+ expect(tomb.id).toBe("rec-ps9c");
254
+ expect(tomb.status).toBe("cancelled");
255
+ });
256
+
257
+ it("反查未命中(目录无 identity 匹配文件)→ 不抛、sessionFile 保持缺失、无 marker(行为退回修复前)", async () => {
258
+ // sessionDir 存在但内容与 record 不匹配
259
+ writeSessionFileWithIdentity("20260901T12000000_other.jsonl", "rec-someone-else");
260
+ const record = makeMinimalRecord({ id: "rec-ps9-miss", sessionFile: undefined });
261
+ const deps = { ...makeDeps(), sessionDir };
262
+
263
+ await expect(
264
+ doFinalizeRecord(deps, record, makeMinimalResult(), "closed", "gc"),
265
+ ).resolves.toBeUndefined();
266
+
267
+ expect(record.sessionFile).toBeUndefined();
268
+ // manifest sessionFile 同步保持 undefined
269
+ const manifest = await manifestStore.readManifest("rec-ps9-miss");
270
+ expect(manifest?.sessionFile).toBeUndefined();
271
+ loggerMock.error.mockClear();
272
+ });
273
+ });
274
+
275
+ // ── Step 3a 判别 wiring:closedReason → tombstone vs finalized(MF-1 fix / v4 B-1)──
276
+ //
277
+ // record 带 sessionFile 直接命中 writeFinalizedOrTombstone 的核心判别分支:
278
+ // cancelled → writeCancelledTombstone(防重建丢失 cancelled 语义)
279
+ // 其余 reason → writeFinalized(真实 reason 进 sidecar 内容,磁盘重建还原 closedReason)
280
+ // 两路 sidecar 互斥(BC-4):断言「写了哪个 + 没写哪个」双向锁定。
281
+ describe("writeFinalizedOrTombstone 判别 wiring(closedReason → tombstone vs finalized)", () => {
282
+ it("closedReason=cancelled → tombstone sidecar 携带 id/status/agent/startedAt/endedAt 且不写 .finalized", async () => {
283
+ const sessionFile = path.join(tmpDir, "session.jsonl");
284
+ const record = makeMinimalRecord({
285
+ id: "rec-wire-cancelled",
286
+ sessionFile,
287
+ // 预设 endedAt 会被 Step 1 completeRecord 冻结值覆盖(见下方 endedAt 断言注释)
288
+ endedAt: 5678,
289
+ });
290
+
291
+ await doFinalizeRecord(makeDeps(), record, makeMinimalResult(), "closed", "cancelled");
292
+
293
+ // tombstone 写出且内容为完整 CancelledTombstone(经 readCancelledTombstone 形态校验的
294
+ // 字段逐项断言,不用 objectContaining 放宽——重建链路靠这些字段还原)
295
+ expect(fs.existsSync(`${sessionFile}.cancelled`)).toBe(true);
296
+ const tomb = JSON.parse(fs.readFileSync(`${sessionFile}.cancelled`, "utf-8")) as {
297
+ id: string;
298
+ status: string;
299
+ agent: string;
300
+ startedAt: number;
301
+ endedAt: number;
302
+ };
303
+ expect(tomb.id).toBe("rec-wire-cancelled");
304
+ expect(tomb.status).toBe("cancelled");
305
+ expect(tomb.agent).toBe("worker");
306
+ expect(tomb.startedAt).toBe(1000);
307
+ // endedAt 来自 completeRecord 冻结后的 record.endedAt(Step 1 先于 Step 3a,
308
+ // record 预设的 endedAt 会被冻结值覆盖)——tombstone 携带真实收尾时间戳
309
+ expect(tomb.endedAt).toBe(record.endedAt);
310
+ expect(typeof tomb.endedAt).toBe("number");
311
+ // 互斥:cancelled 路径绝不写 finalized(否则重建判「正常结束」,cancelled 语义丢失)
312
+ expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(false);
313
+ });
314
+
315
+ it.each(["user-close", "gc"] as const)(
316
+ "closedReason=%s → .finalized 内容携带真实 reason 且不写 tombstone",
317
+ async (reason) => {
318
+ const sessionFile = path.join(tmpDir, "session.jsonl");
319
+ const record = makeMinimalRecord({ id: `rec-wire-${reason}`, sessionFile });
320
+
321
+ await doFinalizeRecord(makeDeps(), record, makeMinimalResult(), "closed", reason);
322
+
323
+ // finalized sidecar 写出且内容 = 真实 reason(磁盘重建用它还原 closedReason,
324
+ // 不再一律硬编码 gc)
325
+ expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true);
326
+ expect(fs.readFileSync(`${sessionFile}.finalized`, "utf-8")).toBe(reason);
327
+ // 互斥:非 cancelled 路径不写 tombstone
328
+ expect(fs.existsSync(`${sessionFile}.cancelled`)).toBe(false);
329
+ },
330
+ );
331
+ });
332
+
333
+ // ── Step 0 collectPatch wiring:worktreeHandle 置位 → patch 收集 + patchFile 回填 ──
334
+ //
335
+ // [MF#3] patchFile 必须写到 worktree 之外(sessionsDir/<branch>.patch),避免被
336
+ // cleanup 删除;仅 patch.written=true(diff 非空且写盘成功)才回填 record.patchFile,
337
+ // 避免悬空路径让 `git apply` 打不存在的文件。
338
+ describe("collectPatchIfWorktree wiring(worktreeHandle 置位 + patch.written 回填)", () => {
339
+ // agentDir 是真实写目标:finalize-record Step 0 对 sessionsDir 做 mkdirSync(recursive),
340
+ // 会在 agentDir 下创建整棵 subagents/<enc>/sessions 树。必须 mkdtemp 自建 + afterEach
341
+ // 清理(测试红线),禁止硬编码真实 /tmp 路径。mainCwd / handle.path 仅作编码键与
342
+ // startsWith 断言,源码不落盘,保持字面量即可。
343
+ let agentDir: string;
344
+
345
+ beforeEach(() => {
346
+ agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "collect-patch-agent-"));
347
+ });
348
+
349
+ afterEach(() => {
350
+ fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
351
+ });
352
+
353
+ const mainCwd = "/tmp/collect-patch-main-repo";
354
+ const branch = "subagent-b1";
355
+ const handle: WorktreeHandle = {
356
+ path: "/tmp/collect-patch-checkout",
357
+ branch,
358
+ baseCommit: "abc123",
359
+ mainCwd,
360
+ };
361
+
362
+ function makeWorktreeDeps(
363
+ collectPatch: ReturnType<typeof vi.fn>,
364
+ ): ReturnType<typeof makeDeps> & { agentDir: string } {
365
+ // 覆写 worktreeManager / modelService 为 stub(与外层 makeDeps 同款 unknown 断言——
366
+ // FinalizeDeps 的 ModelConfigService/WorktreeManager 是宽接口,stub 只需覆盖本次路径)
367
+ return {
368
+ ...makeDeps(),
369
+ modelService: { getAgentDir: () => agentDir },
370
+ worktreeManager: { collectPatch, cleanup: vi.fn() },
371
+ agentDir,
372
+ } as unknown as ReturnType<typeof makeDeps> & { agentDir: string };
373
+ }
374
+
375
+ it("patch.written=true → collectPatch 收到 handle + sessionsDir/<branch>.patch,record.patchFile 回填", async () => {
376
+ const { agentDir, ...deps } = makeWorktreeDeps(vi.fn().mockResolvedValue({
377
+ patchFile: "/ignored/collector-picked-path.patch",
378
+ failed: false,
379
+ written: true,
380
+ }));
381
+ const sessionsDir = getSubagentSessionDir(agentDir, mainCwd);
382
+ const expectedPatchFile = path.join(sessionsDir, `${branch}.patch`);
383
+ const record = makeMinimalRecord({ id: "rec-patch", worktreeHandle: handle });
384
+
385
+ await doFinalizeRecord(deps, record, makeMinimalResult(), "closed", "gc");
386
+
387
+ // collectPatch 被调用,patchFile 由 finalize-record 拼装(sessionsDir/<branch>.patch)
388
+ expect(deps.worktreeManager.collectPatch).toHaveBeenCalledTimes(1);
389
+ expect(deps.worktreeManager.collectPatch).toHaveBeenCalledWith(handle, expectedPatchFile);
390
+ // sessionsDir 已 mkdir(recursive)——patchFile 的父目录真实存在
391
+ expect(fs.existsSync(sessionsDir)).toBe(true);
392
+ // [MF#3] patchFile 落在 worktree 之外(不会被 Step 3 cleanup 连带删除)
393
+ expect(expectedPatchFile.startsWith(handle.path)).toBe(false);
394
+ // patch.written=true → record.patchFile 回填为 finalize-record 拼装的路径
395
+ expect(record.patchFile).toBe(expectedPatchFile);
396
+ });
397
+
398
+ it("patch.written=false(空 diff / 写失败)→ record.patchFile 不回填(避免悬空路径)", async () => {
399
+ const { agentDir, ...deps } = makeWorktreeDeps(vi.fn().mockResolvedValue({
400
+ patchFile: "/ignored/never-written.patch",
401
+ failed: false,
402
+ written: false,
403
+ }));
404
+ const record = makeMinimalRecord({ id: "rec-patch-empty", worktreeHandle: handle });
405
+
406
+ await doFinalizeRecord(deps, record, makeMinimalResult(), "closed", "gc");
407
+
408
+ expect(deps.worktreeManager.collectPatch).toHaveBeenCalledTimes(1);
409
+ expect(record.patchFile).toBeUndefined();
410
+ });
411
+
412
+ it("collectPatch 抛错 → best-effort 不阻断 finalize 链(manifest 照写、cleanup 照执行)", async () => {
413
+ const { agentDir, ...deps } = makeWorktreeDeps(vi.fn().mockRejectedValue(new Error("git died")));
414
+ const sessionFile = path.join(tmpDir, "session.jsonl");
415
+ const record = makeMinimalRecord({ id: "rec-patch-err", sessionFile, worktreeHandle: handle });
416
+
417
+ await expect(
418
+ doFinalizeRecord(deps, record, makeMinimalResult(), "closed", "gc"),
419
+ ).resolves.toBeUndefined();
420
+
421
+ // Step 0 失败不影响后续步骤:finalized sidecar 与 manifest 照常落地
422
+ expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true);
423
+ const manifest = await manifestStore.readManifest("rec-patch-err");
424
+ expect(manifest?.status).toBe("closed");
425
+ expect(deps.worktreeManager.cleanup).toHaveBeenCalledTimes(1);
426
+ expect(record.patchFile).toBeUndefined();
427
+ loggerMock.error.mockClear();
428
+ });
429
+ });
184
430
  });
185
431
 
186
432
  // ============================================================
@@ -197,19 +443,19 @@ describe("doFinalizeRoundToIdle — chatMode 轮次完成进 idle (M2-A)", () =>
197
443
  });
198
444
 
199
445
  afterEach(() => {
200
- fs.rmSync(tmpDir, { recursive: true, force: true });
446
+ fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
201
447
  });
202
448
 
203
449
  /** 构造 FinalizeDeps:worktreeManager.cleanup / store.archive 为 vi.fn 以断言「不调」。 */
204
- function makeDeps() {
450
+ function makeDeps(): Parameters<typeof doFinalizeRoundToIdle>[0] {
205
451
  return {
206
452
  manifestStore,
207
- worktreeManager: { cleanup: vi.fn(), collectPatch: vi.fn() } as never,
208
- store: { archive: vi.fn(), reportRecordTransition: vi.fn() } as never,
209
- modelService: {} as never,
210
- pi: { appendEntry: vi.fn() } as never,
453
+ worktreeManager: { cleanup: vi.fn(), collectPatch: vi.fn() },
454
+ store: { archive: vi.fn(), reportRecordTransition: vi.fn() },
455
+ modelService: {},
456
+ pi: { appendEntry: vi.fn() },
211
457
  emitUnregister: vi.fn(),
212
- };
458
+ } as unknown as Parameters<typeof doFinalizeRoundToIdle>[0];
213
459
  }
214
460
 
215
461
  it("record 带 sessionFile → 删 .alive + record.status=running + round 0→1", async () => {
@@ -317,6 +563,22 @@ describe("doFinalizeRoundToIdle — chatMode 轮次完成进 idle (M2-A)", () =>
317
563
  expect(record.result).toBe("round did not complete: spawn timeout");
318
564
  });
319
565
 
566
+ it("T2-③/LC-1: 失败轮 text 与 error 并存时 error 优先——collectResult 恒带 getFullText 正文,text 优先会覆盖恢复指引(S-B-1 首轮 settled watchdog 实测)", async () => {
567
+ const record = makeMinimalRecord({ id: "rec-result-err-text", chatMode: true });
568
+ record.status = "closed";
569
+ const result = makeMinimalResult();
570
+ // runSpawn settledWatchdogFired 分支的 collectResult 形态:正文(T1 回补采集)与
571
+ // watchdog 错误并存
572
+ result.text = "DONE";
573
+ result.success = false;
574
+ result.error =
575
+ "subagent did not reach agent_settled within 10 min (settled watchdog); the process was terminated to bound the wait. Recovery: check state with subagents action:'list', then re-send your message to continue.";
576
+ await doFinalizeRoundToIdle(makeDeps(), record, result);
577
+ // 失败的 record.result(= 通知正文)必须携带失败原因与恢复指引,不得回显轮内旧正文
578
+ expect(record.result).toBe(`round did not complete: ${result.error}`);
579
+ expect(record.result).not.toBe("DONE");
580
+ });
581
+
320
582
  it("C1TC10: chatMode 空增量轮占位——record.result 固定 (no output this round),不含上一轮文本(D5)", async () => {
321
583
  const record = makeMinimalRecord({ id: "rec-increment-empty", chatMode: true });
322
584
  record.status = "closed";