@platless/axom-cli 0.3.13 → 0.6.36

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 (320) hide show
  1. package/README.md +18 -5
  2. package/dist/axom-patch/cli/README.md +119 -9
  3. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  4. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  6. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  7. package/dist/axom-patch/cli/account-limits/generate.py +123 -0
  8. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +81 -0
  9. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  10. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  11. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  12. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  13. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  14. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  15. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  16. package/dist/axom-patch/cli/nightly-task.md +194 -0
  17. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  18. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
  19. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  20. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  21. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  22. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  23. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
  24. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -0
  25. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -5
  26. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -7
  27. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  28. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  29. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  30. package/dist/axom-patch/cli/runtime.json +39 -15
  31. package/dist/axom-patch/cli/zcode-account-limits.json +30 -0
  32. package/dist/axom-patch/cli/zcode-models.json +37 -0
  33. package/dist/axom-patch/cli/zcode-subagent-subwork.json +72 -0
  34. package/dist/cli.js +4 -9
  35. package/dist/commands/knowledge.js +47 -0
  36. package/dist/commands/update.js +65 -3
  37. package/dist/commands/workstation.js +73 -7
  38. package/dist/install/claude/commands.js +13 -13
  39. package/dist/keepers/acp/keeper.mjs +3083 -0
  40. package/dist/keepers/acp/manifest.json +9 -0
  41. package/dist/keepers/terminal/keeper.mjs +8469 -0
  42. package/dist/keepers/terminal/manifest.json +9 -0
  43. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  44. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  45. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  46. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  47. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  48. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  52. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  53. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  54. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  55. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  56. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  57. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  58. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  59. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  60. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  61. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  62. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  63. package/dist/mcp.js +1 -1
  64. package/dist/native/darwin-arm64/axom-quic +0 -0
  65. package/dist/test/keeperKit.js +67 -0
  66. package/dist/workstation/acp/acpAgents.js +2192 -636
  67. package/dist/workstation/acp/acpClient.js +84 -495
  68. package/dist/workstation/acp/adoption.js +10 -396
  69. package/dist/workstation/acp/binaryInstall.js +100 -27
  70. package/dist/workstation/acp/builds/gc.js +245 -0
  71. package/dist/workstation/acp/builds/install/binary.js +28 -0
  72. package/dist/workstation/acp/builds/install/npm.js +324 -0
  73. package/dist/workstation/acp/builds/install/uv.js +254 -0
  74. package/dist/workstation/acp/builds/migrate.js +195 -0
  75. package/dist/workstation/acp/builds/model.js +128 -0
  76. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  77. package/dist/workstation/acp/builds/store.js +352 -0
  78. package/dist/workstation/acp/commandTableProbe.js +68 -0
  79. package/dist/workstation/acp/configOptions.js +103 -1
  80. package/dist/workstation/acp/envSecrets.js +27 -0
  81. package/dist/workstation/acp/initializeResponse.js +290 -0
  82. package/dist/workstation/acp/installErrors.js +12 -2
  83. package/dist/workstation/acp/installProbe.js +1 -1
  84. package/dist/workstation/acp/managedPatches.js +239 -159
  85. package/dist/workstation/acp/managedRuntime.js +314 -0
  86. package/dist/workstation/acp/outbox.js +330 -0
  87. package/dist/workstation/acp/patchAssets.js +60 -3
  88. package/dist/workstation/acp/permission.js +42 -10
  89. package/dist/workstation/acp/processor/checkpoint.js +78 -0
  90. package/dist/workstation/acp/processor/commands.js +35 -0
  91. package/dist/workstation/acp/processor/control.js +282 -0
  92. package/dist/workstation/acp/processor/emit.js +26 -0
  93. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  94. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  95. package/dist/workstation/acp/processor/origins.js +48 -0
  96. package/dist/workstation/acp/processor/output.js +67 -0
  97. package/dist/workstation/acp/processor/projection.js +42 -0
  98. package/dist/workstation/acp/processor/record.js +128 -0
  99. package/dist/workstation/acp/processor/runtime.js +105 -0
  100. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  101. package/dist/workstation/acp/processor/session.js +496 -0
  102. package/dist/workstation/acp/processor/state.js +67 -0
  103. package/dist/workstation/acp/processor/step.js +442 -0
  104. package/dist/workstation/acp/processor/test/fixtureLog.js +58 -0
  105. package/dist/workstation/acp/processor/test/harness.js +120 -0
  106. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  107. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  108. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  109. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  110. package/dist/workstation/acp/processor/transport/local.js +269 -0
  111. package/dist/workstation/acp/processor/transport/types.js +78 -0
  112. package/dist/workstation/acp/processor/updates.js +390 -0
  113. package/dist/workstation/acp/processorSession.js +1348 -0
  114. package/dist/workstation/acp/registry/catalog.js +189 -41
  115. package/dist/workstation/acp/registry/launch.js +76 -119
  116. package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
  117. package/dist/workstation/acp/steering.js +58 -0
  118. package/dist/workstation/acp/switching.js +111 -0
  119. package/dist/workstation/acp/terminalLogin.js +16 -42
  120. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  121. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  122. package/dist/workstation/acp/test/daemonLife.js +106 -0
  123. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  124. package/dist/workstation/acp/test/keeperWorld.js +295 -0
  125. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  126. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  127. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  128. package/dist/workstation/acp/test/raw/sources.js +135 -0
  129. package/dist/workstation/acp/test/realAdapter.js +331 -0
  130. package/dist/workstation/acp/test/scriptAgent.js +214 -0
  131. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  132. package/dist/workstation/acp/timedCommand.js +4 -2
  133. package/dist/workstation/acp/uvxInstall.js +72 -122
  134. package/dist/workstation/agentFlow.js +103 -0
  135. package/dist/workstation/agentSessions.js +1289 -544
  136. package/dist/workstation/channels/agent.js +7 -5
  137. package/dist/workstation/channels/computer.js +69 -21
  138. package/dist/workstation/channels/computerExecs.js +200 -0
  139. package/dist/workstation/channels/management.js +172 -8
  140. package/dist/workstation/channels/url.js +1 -1
  141. package/dist/workstation/config.js +16 -4
  142. package/dist/workstation/connection/encryptedChannel.js +4 -1
  143. package/dist/workstation/control/bindingStore.js +200 -0
  144. package/dist/workstation/control/commands.js +124 -0
  145. package/dist/workstation/control/daemonIdentity.js +53 -0
  146. package/dist/workstation/control/enrollment.js +106 -0
  147. package/dist/workstation/control/inspect.js +71 -0
  148. package/dist/workstation/control/locks.js +63 -0
  149. package/dist/workstation/control/protocol.js +40 -0
  150. package/dist/workstation/control/restartGuard.js +46 -0
  151. package/dist/workstation/control/runtimeClient.js +97 -0
  152. package/dist/workstation/control/runtimeHost.js +148 -0
  153. package/dist/workstation/control/serverIdentity.js +35 -0
  154. package/dist/workstation/daemon.js +393 -12
  155. package/dist/workstation/daemonctl.js +103 -14
  156. package/dist/workstation/desktopControl/audio.js +99 -0
  157. package/dist/workstation/desktopControl/capabilities.js +92 -0
  158. package/dist/workstation/desktopControl/captureModes.js +9 -0
  159. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  160. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  161. package/dist/workstation/desktopControl/helperProcess.js +353 -0
  162. package/dist/workstation/desktopControl/lobby.js +129 -0
  163. package/dist/workstation/desktopControl/macProvider.js +155 -0
  164. package/dist/workstation/desktopControl/media.js +331 -0
  165. package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
  166. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  167. package/dist/workstation/desktopControl/overview.js +58 -0
  168. package/dist/workstation/desktopControl/protocol.js +302 -0
  169. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  170. package/dist/workstation/desktopControl/rateController.js +226 -0
  171. package/dist/workstation/desktopControl/supervisor.js +1681 -0
  172. package/dist/workstation/desktopControl/transport.js +27 -0
  173. package/dist/workstation/desktopControl/wallpaper.js +78 -0
  174. package/dist/workstation/exec/attach.js +126 -0
  175. package/dist/workstation/exec/command.js +40 -0
  176. package/dist/workstation/exec/durable.js +517 -0
  177. package/dist/workstation/exec/fallback.js +106 -0
  178. package/dist/workstation/exec/policy.js +837 -0
  179. package/dist/workstation/exec/shaping.js +211 -0
  180. package/dist/workstation/exec.js +3 -112
  181. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  182. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  183. package/dist/workstation/files/client.js +455 -0
  184. package/dist/workstation/files/credentials.js +142 -0
  185. package/dist/workstation/files/errors.js +28 -0
  186. package/dist/workstation/files/fence.js +350 -0
  187. package/dist/workstation/files/find/fileIndex.js +153 -0
  188. package/dist/workstation/files/find/fuzzy.js +155 -0
  189. package/dist/workstation/files/host.js +122 -0
  190. package/dist/workstation/files/hostProtocol.js +2 -0
  191. package/dist/workstation/files/media/cache.js +620 -0
  192. package/dist/workstation/files/media/encoding.js +308 -0
  193. package/dist/workstation/files/media/estimator.js +100 -0
  194. package/dist/workstation/files/media/media.js +325 -0
  195. package/dist/workstation/files/media/packedAudio.js +23 -0
  196. package/dist/workstation/files/media/scheduler.js +551 -0
  197. package/dist/workstation/files/media/types.js +5 -0
  198. package/dist/workstation/files/scope.js +45 -0
  199. package/dist/workstation/files/structured.js +284 -0
  200. package/dist/workstation/{fileOps.js → files/text.js} +152 -39
  201. package/dist/workstation/files/uploads.js +351 -0
  202. package/dist/workstation/journal.js +65 -236
  203. package/dist/workstation/keepers/acp/client.js +101 -0
  204. package/dist/workstation/keepers/acp/envelope.js +62 -0
  205. package/dist/workstation/keepers/acp/host.js +419 -0
  206. package/dist/workstation/keepers/acp/main.js +18 -0
  207. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  208. package/dist/workstation/keepers/acp/params.js +137 -0
  209. package/dist/workstation/keepers/acp/protocol.js +66 -0
  210. package/dist/workstation/keepers/acp/recover.js +120 -0
  211. package/dist/workstation/keepers/acp/session.js +584 -0
  212. package/dist/workstation/keepers/acp/stderr.js +154 -0
  213. package/dist/workstation/keepers/kit/activity.js +103 -0
  214. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  215. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  216. package/dist/workstation/keepers/kit/bundle.js +124 -0
  217. package/dist/workstation/keepers/kit/client.js +432 -0
  218. package/dist/workstation/keepers/kit/identity.js +21 -0
  219. package/dist/workstation/keepers/kit/ipc.js +112 -0
  220. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  221. package/dist/workstation/keepers/kit/launcher.js +289 -0
  222. package/dist/workstation/keepers/kit/lock.js +123 -0
  223. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  224. package/dist/workstation/keepers/kit/paths.js +76 -0
  225. package/dist/workstation/keepers/kit/protocol.js +43 -0
  226. package/dist/workstation/keepers/kit/server.js +518 -0
  227. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  228. package/dist/workstation/keepers/legacy.js +296 -0
  229. package/dist/workstation/keepers/policy.js +576 -0
  230. package/dist/workstation/keepers/stop.js +34 -0
  231. package/dist/workstation/keepers/terminal/client.js +213 -0
  232. package/dist/workstation/keepers/terminal/exec.js +981 -0
  233. package/dist/workstation/keepers/terminal/generations.js +129 -0
  234. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  235. package/dist/workstation/keepers/terminal/main.js +43 -0
  236. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  237. package/dist/workstation/keepers/terminal/params.js +27 -0
  238. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  239. package/dist/workstation/keepers/terminal/pty.js +330 -0
  240. package/dist/workstation/managedInstall.js +57 -0
  241. package/dist/workstation/protocol/accountLimits.js +47 -0
  242. package/dist/workstation/protocol/agent.js +50 -19
  243. package/dist/workstation/protocol/common.js +30 -0
  244. package/dist/workstation/protocol/computer.js +15 -4
  245. package/dist/workstation/protocol/management.js +141 -4
  246. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  247. package/dist/workstation/rd/artifact.js +119 -0
  248. package/dist/workstation/rd/control.js +119 -0
  249. package/dist/workstation/rd/display.js +29 -0
  250. package/dist/workstation/rd/provider.js +222 -0
  251. package/dist/workstation/rd/sessions.js +391 -0
  252. package/dist/workstation/rd/state.js +45 -0
  253. package/dist/workstation/rd/update.js +171 -0
  254. package/dist/workstation/rd/upkeep.js +38 -0
  255. package/dist/workstation/register.js +27 -37
  256. package/dist/workstation/service/installer.js +104 -13
  257. package/dist/workstation/service/launchd.js +1 -0
  258. package/dist/workstation/storageMetrics.js +118 -0
  259. package/dist/workstation/systemMetrics.js +154 -93
  260. package/dist/workstation/toolchain.js +94 -0
  261. package/dist/workstation/tools/browser.js +28 -4
  262. package/dist/workstation/tools/client.js +9 -1
  263. package/dist/workstation/tools/info.js +50 -0
  264. package/dist/workstation/tools/keeperProcess.js +15 -5
  265. package/dist/workstation/tools/router.js +55 -0
  266. package/dist/workstation/tools/terminalRouter.js +222 -0
  267. package/dist/workstation/tools/validation.js +37 -1
  268. package/dist/workstation/update/artifact.js +2 -1
  269. package/dist/workstation/update/coordinator.js +3 -0
  270. package/dist/workstation/update/globalInstall.js +116 -0
  271. package/dist/workstation/update/npmInstaller.js +49 -23
  272. package/dist/workstation/update/ptyPreflight.js +21 -0
  273. package/dist/workstation/update/rdRollback.js +145 -0
  274. package/dist/workstation/update/runUpdate.js +3 -2
  275. package/dist/workstation/update/taskRegistry.js +105 -11
  276. package/dist/workstation/update/watchdog.js +16790 -334
  277. package/package.json +11 -8
  278. package/skills/axom/SKILL.md +11 -11
  279. package/skills/axom-definition/SKILL.md +3 -3
  280. package/skills/axom-platform/SKILL.md +3 -3
  281. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  282. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -49
  283. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  284. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  285. package/dist/commands/guide.js +0 -38
  286. package/dist/workstation/acp/acpSession.js +0 -1475
  287. package/dist/workstation/acp/contract.js +0 -499
  288. package/dist/workstation/acp/delegationMcp.js +0 -24
  289. package/dist/workstation/acp/draftInterceptor.js +0 -163
  290. package/dist/workstation/acp/qoderStartup.js +0 -45
  291. package/dist/workstation/acp/sessionScope.js +0 -19
  292. package/dist/workstation/filePaths.js +0 -174
  293. package/dist/workstation/fileWorker.js +0 -81
  294. package/dist/workstation/fileWorkerClient.js +0 -121
  295. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -106
  296. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -262
  297. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  298. package/dist/workstation/qoderAdapter/contract.js +0 -78
  299. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -30
  300. package/dist/workstation/qoderAdapter/history.js +0 -87
  301. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  302. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  303. package/dist/workstation/qoderAdapter/main.js +0 -93
  304. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  305. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  306. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  307. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  308. package/dist/workstation/qoderAdapter/profile.js +0 -93
  309. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -49
  310. package/dist/workstation/qoderAdapter/session.js +0 -319
  311. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  312. package/dist/workstation/qoderAdapter/subwork.js +0 -77
  313. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  314. package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
  315. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  316. package/dist/workstation/qoderAdapter/usage.js +0 -37
  317. package/dist/workstation/tools/core.js +0 -50
  318. package/dist/workstation/tools/files.js +0 -525
  319. package/dist/workstation/tools/terminal.js +0 -214
  320. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/provider.js} +0 -0
@@ -0,0 +1,211 @@
1
+ // Execute the real published adapter classes, without models or credentials.
2
+ // node <probe> <pi-acp-package-directory> [--expect-no-subagents]
3
+ //
4
+ // Pristine pi-acp emits no subagent variants and drops custom messages; the patched
5
+ // adapter normalizes pi-subagents children into draft ACP subagent variants with
6
+ // ids derived only from the root session id and toolCallId/runId, so the same input
7
+ // always produces byte-identical frames.
8
+ import assert from 'node:assert/strict';
9
+ import { readFile, writeFile, mkdtemp, rm } from 'node:fs/promises';
10
+ import { tmpdir } from 'node:os';
11
+ import path from 'node:path';
12
+ import { pathToFileURL } from 'node:url';
13
+ import { createRequire } from 'node:module';
14
+
15
+ const directory = path.resolve(process.argv[2]);
16
+ const expectNone = process.argv.includes('--expect-no-subagents');
17
+ const require = createRequire(path.join(directory, 'package.json'));
18
+ const scratch = await mkdtemp(path.join(tmpdir(), 'axom-pi-subagents-'));
19
+ try {
20
+ const original = await readFile(path.join(directory, 'dist/index.js'), 'utf8');
21
+ let source = original.slice(0, original.lastIndexOf('// src/index.ts')).replace(/^#![^\n]*\n/, '');
22
+ source = source.replace(/from "([^"\n]+)"/g, (whole, specifier) => specifier.startsWith('@') || specifier === 'cross-spawn'
23
+ ? `from ${JSON.stringify(pathToFileURL(require.resolve(specifier)).href)}` : whole);
24
+ source += '\nexport { PiAcpAgent, PiAcpSession };\n';
25
+ await writeFile(path.join(scratch, 'package.json'), await readFile(path.join(directory, 'package.json')));
26
+ await writeFile(path.join(scratch, 'runtime.mjs'), source);
27
+ const { PiAcpSession } = await import(pathToFileURL(path.join(scratch, 'runtime.mjs')));
28
+
29
+ const COLLECT = () => {
30
+ const updates = [];
31
+ return { updates, conn: { async sessionUpdate(event) { updates.push(event); } } };
32
+ };
33
+ const newSession = () => {
34
+ const collect = COLLECT();
35
+ let events;
36
+ const proc = { onEvent(handler) { events = handler; } };
37
+ const session = new PiAcpSession({ sessionId: 'fixture', cwd: scratch, mcpServers: [], proc, conn: collect.conn });
38
+ return { session, collect, fire: ev => { events(ev); return session.flushEmits(); } };
39
+ };
40
+
41
+ // One full drive; every awaited fire drains the ordered emit chain first.
42
+ // verify=false collects the frames without the per-step assertions, for the pristine run.
43
+ const drive = async (verify = true) => {
44
+ const { session, collect, fire } = newSession();
45
+ const updates = collect.updates;
46
+ const names = () => updates.map(u => `${u.sessionId}:${u.update.sessionUpdate}`);
47
+
48
+ // Foreground launch: spawned precedes the adapter's own tool_call frame.
49
+ await fire({ type: 'tool_execution_start', toolCallId: 't1', toolName: 'subagent',
50
+ args: { agent: 'scout', task: 'map the repo' } });
51
+ if (verify) {
52
+ assert.deepEqual(updates[0].update, { sessionUpdate: 'subagent_spawned', subagentSessionId: 'pi-agent:fixture:t1',
53
+ name: 'scout', task: 'map the repo', capabilities: {}, _meta: { pi: { toolCallId: 't1' } } });
54
+ assert.equal(updates[0].sessionId, 'fixture');
55
+ assert.equal(updates[1].update.sessionUpdate, 'tool_call');
56
+ }
57
+
58
+ // Foreground completion: child preview then terminal state, then the adapter's frame.
59
+ await fire({ type: 'tool_execution_end', toolCallId: 't1', isError: false,
60
+ result: { content: [{ type: 'text', text: 'all clear' }], details: { mode: 'single', runId: 'fg1' } } });
61
+ if (verify) {
62
+ const afterEnd = names();
63
+ assert.deepEqual(afterEnd.slice(2, 4), ['pi-agent:fixture:t1:agent_message_chunk', 'fixture:subagent_state_update']);
64
+ assert.equal(updates[3].update.state, 'completed');
65
+ }
66
+
67
+ // Management and steering calls are not launches.
68
+ await fire({ type: 'tool_execution_start', toolCallId: 't2', toolName: 'subagent', args: { action: 'list' } });
69
+ await fire({ type: 'tool_execution_end', toolCallId: 't2', isError: false,
70
+ result: { content: [{ type: 'text', text: 'agents: ...' }], details: {} } });
71
+ await fire({ type: 'tool_execution_start', toolCallId: 't3', toolName: 'subagent',
72
+ args: { id: 'x', message: 'steer' } });
73
+ await fire({ type: 'tool_execution_end', toolCallId: 't3', isError: false,
74
+ result: { content: [{ type: 'text', text: 'queued' }], details: {} } });
75
+ if (verify) assert.ok(!updates.some(u => u.update.sessionUpdate === 'subagent_spawned' && u.update.name !== 'scout'));
76
+
77
+ // Async launch: the receipt is not a terminal state.
78
+ await fire({ type: 'tool_execution_start', toolCallId: 't4', toolName: 'subagent',
79
+ args: { agent: 'researcher', task: 'study', async: true } });
80
+ if (verify) assert.equal(updates.find(u => u.update.sessionUpdate === 'subagent_spawned' && u.update._meta?.pi?.toolCallId === 't4')?.update.subagentSessionId, 'pi-agent:fixture:t4');
81
+ await fire({ type: 'tool_execution_end', toolCallId: 't4', isError: false,
82
+ result: { content: [{ type: 'text', text: 'Async single: researcher [r9]' }],
83
+ details: { mode: 'single', runId: 'r9', asyncId: 'r9', asyncDir: '/tmp/async' } } });
84
+ if (verify) assert.ok(!updates.some(u => u.update.sessionUpdate === 'subagent_state_update' && u.update.subagentSessionId === 'pi-agent:fixture:t4'));
85
+
86
+ // The completion notice closes the async child by run id.
87
+ await fire({ type: 'message_end', message: { role: 'custom', customType: 'subagent-notify',
88
+ content: 'researcher finished', details: { runs: [{ runId: 'r9', agent: 'researcher', status: 'completed',
89
+ summary: 'study done', resultPath: '/tmp/async/r9/result.json' }] } } });
90
+ await fire({ type: 'message_start', message: { role: 'custom', customType: 'subagent-notify',
91
+ content: 'duplicate pair member', details: { runs: [{ runId: 'r9', agent: 'researcher', status: 'completed' }] } } });
92
+ if (verify) {
93
+ const closed = updates.find(u => u.update.sessionUpdate === 'subagent_state_update' && u.update.subagentSessionId === 'pi-agent:fixture:t4');
94
+ assert.equal(closed.update.state, 'completed');
95
+ assert.equal(closed.update._meta.pi.runId, 'r9');
96
+ assert.equal(closed.update._meta.pi.resultPath, '/tmp/async/r9/result.json');
97
+ }
98
+
99
+ // A repeated notice is idempotent.
100
+ const countBefore = updates.length;
101
+ if (verify) {
102
+ const beforePair = updates.length;
103
+ await fire({ type: 'message_start', message: { role: 'custom', customType: 'subagent-notify',
104
+ content: 'pair member', details: { runs: [{ runId: 'r9', agent: 'researcher', status: 'completed' }] } } });
105
+ assert.equal(updates.length, beforePair);
106
+ }
107
+ await fire({ type: 'message_end', message: { role: 'custom', customType: 'subagent-notify',
108
+ content: 'researcher finished', details: { runs: [{ runId: 'r9', agent: 'researcher', status: 'completed' }] } } });
109
+ if (verify) assert.equal(updates.length, countBefore + 0);
110
+
111
+ // Unknown runs (adapter restart) get an adopted spawn plus their state.
112
+ await fire({ type: 'message_end', message: { role: 'custom', customType: 'subagent-notify',
113
+ content: 'worker stopped', details: { runs: [{ runId: 'zz', agent: 'worker', status: 'stopped', summary: '' }] } } });
114
+ if (verify) {
115
+ const adopted = updates.find(u => u.update.sessionUpdate === 'subagent_spawned' && u.update._meta?.pi?.adopted);
116
+ assert.equal(adopted.update.subagentSessionId, 'pi-agent:fixture:run:zz');
117
+ assert.equal(adopted.update.name, 'worker');
118
+ assert.equal(updates.find(u => u.update.sessionUpdate === 'subagent_state_update' && u.update.subagentSessionId === 'pi-agent:fixture:run:zz').update.state, 'cancelled');
119
+ }
120
+
121
+ // Paused is not terminal; other custom messages are ignored.
122
+ await fire({ type: 'message_end', message: { role: 'custom', customType: 'subagent-notify',
123
+ content: 'paused', details: { runs: [{ runId: 'pp', agent: 'worker', status: 'paused' }] } } });
124
+ if (verify) {
125
+ assert.ok(updates.some(u => u.update.sessionUpdate === 'subagent_spawned' && u.update.subagentSessionId === 'pi-agent:fixture:run:pp'));
126
+ assert.ok(!updates.some(u => u.update.sessionUpdate === 'subagent_state_update' && u.update.subagentSessionId === 'pi-agent:fixture:run:pp'));
127
+ }
128
+ const beforeOther = updates.length;
129
+ await fire({ type: 'message_end', message: { role: 'custom', customType: 'something-else',
130
+ content: 'x', details: {} } });
131
+ await fire({ type: 'message_end', message: { role: 'assistant', content: [{ type: 'text', text: 'hi' }] } });
132
+ assert.equal(updates.length, beforeOther);
133
+
134
+ // Progress dedup: identical-heartbeat tool_execution_update frames are dropped, real
135
+ // changes pass, other tools are never deduplicated.
136
+ await fire({ type: 'tool_execution_start', toolCallId: 't5', toolName: 'subagent',
137
+ args: { agent: 'worker', task: 'long job', async: false } });
138
+ await fire({ type: 'tool_execution_update', toolCallId: 't5', toolName: 'subagent',
139
+ partialResult: { content: [{ type: 'text', text: '(running...)' }],
140
+ details: { progress: [{ agent: 'worker', durationMs: 1000, lastActivityAt: 1791891393036, status: 'running tool bash' }] } } });
141
+ const afterFirstUpdate = updates.filter(u => u.update.sessionUpdate === 'tool_call_update').length;
142
+ await fire({ type: 'tool_execution_update', toolCallId: 't5', toolName: 'subagent',
143
+ partialResult: { content: [{ type: 'text', text: '(running...)' }],
144
+ details: { progress: [{ agent: 'worker', durationMs: 2000, lastActivityAt: 1791891394047,
145
+ recentOutput: ['(no output)', '(no output)'], status: 'running tool bash' }] } } });
146
+ await fire({ type: 'tool_execution_update', toolCallId: 't5', toolName: 'subagent',
147
+ partialResult: { content: [{ type: 'text', text: '(running...)' }],
148
+ details: { progress: [{ agent: 'worker', durationMs: 3000, lastActivityAt: 1791891395047,
149
+ recentOutput: ['(no output)', '(no output)', '(no output)'], status: 'running tool bash' }] } } });
150
+ await fire({ type: 'tool_execution_update', toolCallId: 't5', toolName: 'subagent',
151
+ partialResult: { content: [{ type: 'text', text: '(running...)' }],
152
+ details: { progress: [{ agent: 'worker', durationMs: 3100, lastActivityAt: 1791891396047,
153
+ recentOutput: ['(no output)', '(no output)', '(no output)'], status: 'running tool bash',
154
+ inputTokens: 2730, outputTokens: 69, cacheRead: 160, tokens: 2799, window: 2890, windowPeak: 2890 }],
155
+ results: [{ usage: { input: 2730, output: 69, cost: 0.0009, turns: 1 } }] } } });
156
+ await fire({ type: 'tool_execution_update', toolCallId: 't5', toolName: 'subagent',
157
+ partialResult: { content: [{ type: 'text', text: '(running...)' }],
158
+ details: { progress: [{ agent: 'worker', durationMs: 3200, lastActivityAt: 1791891397047,
159
+ recentOutput: ['(no output)', '(no output)', '(no output)'], status: 'running tool bash',
160
+ inputTokens: 5483, outputTokens: 1852, cacheRead: 3049, tokens: 7335, window: 5642, windowPeak: 5642 }],
161
+ results: [{ usage: { input: 5483, output: 1852, cost: 0.0040, turns: 1 } }] } } });
162
+ if (verify) {
163
+ assert.equal(updates.filter(u => u.update.sessionUpdate === 'tool_call_update').length, afterFirstUpdate,
164
+ 'heartbeat frames differing only in timers/recentOutput/token telemetry are dropped');
165
+ }
166
+ await fire({ type: 'tool_execution_update', toolCallId: 't5', toolName: 'subagent',
167
+ partialResult: { content: [{ type: 'text', text: '(running...)' }],
168
+ details: { progress: [{ agent: 'worker', durationMs: 3500, lastActivityAt: 1791891396047,
169
+ recentOutput: ['(no output)', '(no output)', '(no output)'], status: 'running tool bash',
170
+ recentTools: [{ tool: 'bash', args: 'ls', endMs: 1 }] }] } } });
171
+ if (verify) {
172
+ assert.equal(updates.filter(u => u.update.sessionUpdate === 'tool_call_update').length, afterFirstUpdate + 1,
173
+ 'a real milestone (recentTools growth / status change) still passes');
174
+ }
175
+ const updatesBeforeOther = updates.length;
176
+ await fire({ type: 'tool_execution_update', toolCallId: 'tx', toolName: 'read',
177
+ partialResult: { content: [{ type: 'text', text: 'x' }] } });
178
+ await fire({ type: 'tool_execution_update', toolCallId: 'tx', toolName: 'read',
179
+ partialResult: { content: [{ type: 'text', text: 'x' }] } });
180
+ if (verify) assert.equal(updates.length, updatesBeforeOther + 2, 'other tools are never deduplicated');
181
+ await fire({ type: 'tool_execution_end', toolCallId: 't5', isError: false,
182
+ result: { content: [{ type: 'text', text: 'long job done' }], details: { mode: 'single', runId: 'fg5' } } });
183
+ if (verify) {
184
+ assert.equal(updates.find(u => u.update.sessionUpdate === 'subagent_state_update' && u.update.subagentSessionId === 'pi-agent:fixture:t5')?.update.state, 'completed',
185
+ 'a deduplicated run still closes normally');
186
+ }
187
+
188
+ return updates;
189
+ };
190
+
191
+ if (expectNone) {
192
+ const first = await drive(false);
193
+ const spawns = first.filter(u => u.update.sessionUpdate === 'subagent_spawned').length;
194
+ const states = first.filter(u => u.update.sessionUpdate === 'subagent_state_update').length;
195
+ assert.equal(spawns, 0);
196
+ assert.equal(states, 0);
197
+ assert.ok(first.some(u => u.update.sessionUpdate === 'tool_call'), 'adapter still maps tool calls');
198
+ console.log('PASS pristine pi-acp emits no subagent variants and ignores custom messages');
199
+ } else {
200
+ const first = await drive();
201
+ const spawns = first.filter(u => u.update.sessionUpdate === 'subagent_spawned').length;
202
+ const states = first.filter(u => u.update.sessionUpdate === 'subagent_state_update').length;
203
+ assert.equal(spawns, 5); // t1, t4, adopted zz, paused pp, t5
204
+ assert.equal(states, 4); // t1, r9(t4), zz, t5
205
+ const second = await drive();
206
+ assert.deepEqual(second, first); // deterministic: no ids beyond the inputs
207
+ console.log('PASS subagent variants, ordering, idempotence and determinism');
208
+ }
209
+ } finally {
210
+ await rm(scratch, { recursive: true, force: true });
211
+ }
@@ -0,0 +1,257 @@
1
+ // Execute the real published adapter modules, without models or credentials.
2
+ // node <probe> <zcode-acp-server-package-directory> [--expect-no-subagents]
3
+ //
4
+ // Pristine zcode-acp-server emits no subagent/async-task variants (the backend's
5
+ // sub-agent lifecycle events carry no taskId, so every listener drops them) and
6
+ // stamps `_meta.subagent` on every Agent dispatch; the patched adapter forwards
7
+ // the backend's own `session.updated` agentId/taskId events as draft ACP
8
+ // subagent variants (RFD #1992) and AIR async-task events, re-scopes the
9
+ // sub-agent's internal `tool_subagent_*` calls onto its `sess_subagent_*` child
10
+ // session, and drops the mark when the client negotiated native subagents.
11
+ // Every emitted frame derives from backend-supplied ids only — no randomness,
12
+ // so the same event replay produces byte-identical output.
13
+ import assert from 'node:assert/strict';
14
+ import path from 'node:path';
15
+ import { pathToFileURL } from 'node:url';
16
+
17
+ const directory = path.resolve(process.argv[2]);
18
+ const expectNone = process.argv.includes('--expect-no-subagents');
19
+
20
+ const serverUrl = pathToFileURL(path.join(directory, 'dist/server.js')).href;
21
+ const dispatchUrl = pathToFileURL(path.join(directory, 'dist/handlers/dispatch.js')).href;
22
+ const bgUrl = pathToFileURL(path.join(directory, 'dist/handlers/background-tasks.js')).href;
23
+ const serverMod = await import(serverUrl);
24
+ const dispatchMod = await import(dispatchUrl);
25
+ const bgMod = await import(bgUrl);
26
+
27
+ if (expectNone) {
28
+ assert.equal(serverMod.AxomZcodeSubagents, undefined, 'pristine package must not export AxomZcodeSubagents');
29
+ // Pristine dispatch: no child re-scoping, mark always stamped.
30
+ const cx = recorder();
31
+ await dispatchMod.dispatchEvent(fakeServer({ subagents: true }), cx, 'root-1', {
32
+ kind: 'ToolCallNew', tool: 'Agent', callId: 'tool_subagent_agent_a_call_b', title: 'inner', status: 'pending',
33
+ });
34
+ assert.equal(cx.sent.length, 1);
35
+ assert.equal(cx.sent[0].params.sessionId, 'root-1', 'pristine dispatch addresses sub-agent frames to the root');
36
+ assert.equal(cx.sent[0].params.update._meta.subagent, true, 'pristine dispatch stamps the subagent mark unconditionally');
37
+ console.log('PASS zcode-subagent-subwork (pristine: no variants, no re-scoping, mark unconditional)');
38
+ process.exit(0);
39
+ }
40
+
41
+ assert.ok(serverMod.AxomZcodeSubagents, 'patched package must export AxomZcodeSubagents');
42
+
43
+ function recorder() {
44
+ const sent = [];
45
+ return { sent, notify: async (method, params) => { sent.push({ method, params }); } };
46
+ }
47
+
48
+ function fakeServer(caps) {
49
+ const childCx = recorder();
50
+ return {
51
+ clientCapabilities: caps,
52
+ sessionAliases: (acpSid) => [acpSid],
53
+ notifyByZcodeSid: async (sid, update) => { void sid; rootCx.sent.push({ method: 'session/update', params: { sessionId: 'root-1', update } }); return true; },
54
+ clients: { size: 1, broadcast: () => childCx },
55
+ supportsTerminalOutput: () => false,
56
+ noteDispatchedToolCall: () => {},
57
+ dispatchedToolCalls: new Map(),
58
+ terminalSentData: new Map(),
59
+ workflowRunNames: new Map(),
60
+ sessionPlanActive: new Set(),
61
+ notifyTurnActiveSince: new Map(),
62
+ lastMode: new Map(),
63
+ __childCx: childCx,
64
+ };
65
+ }
66
+
67
+ const CAPS_NATIVE = { subagents: {}, _meta: { jetbrains: { air: { version: 1, capabilities: ['nativeSubagentSessions', 'asyncTasks'] } } } };
68
+ const CAPS_PLAIN = {};
69
+
70
+ const rootCx = recorder();
71
+
72
+ // ---------------------------------------------------------------- scenarios
73
+ const agentSpawn = (over = {}) => ({ type: 'session.updated', payload: {
74
+ agentId: 'agent_u1', agentType: 'general-purpose', childSessionId: 'sess_subagent_agent_u1',
75
+ description: 'Count kts files', prompt: 'Count the .kts files.', parentToolCallId: 'call_p1', status: 'running', ...over } });
76
+ const agentTerminal = (over = {}) => agentSpawn({ status: 'completed', totalDurationMs: 4477, totalToolUseCount: 1, totalTokens: 21127, ...over });
77
+ const parentResult = (result) => ({ type: 'tool.updated', payload: { toolCallId: 'call_p1', kind: 'result', result } });
78
+ const taskEvent = (over = {}) => ({ type: 'session.updated', payload: {
79
+ taskId: 'exec_t1', toolCallId: 'call_b1', toolName: 'Bash', taskKind: 'bash', cancellable: true,
80
+ command: 'sleep 6', description: 'Sleep 6 seconds', status: 'running', outputPath: '/tmp/out.log', ...over } });
81
+
82
+ let rootCxReset = () => { rootCx.sent.length = 0; };
83
+
84
+ // 1. Foreground: spawned at running; state held until the parent tool result,
85
+ // then child-scoped preview precedes the terminal state.
86
+ {
87
+ const server = fakeServer(CAPS_NATIVE);
88
+ const obs = new serverMod.AxomZcodeSubagents(server, 'z-1');
89
+ obs.handleEvent(agentSpawn());
90
+ let names = rootCx.sent.map((s) => s.params.update.sessionUpdate);
91
+ assert.deepEqual(names, ['subagent_spawned']);
92
+ const spawned = rootCx.sent[0].params.update;
93
+ assert.equal(spawned.subagentSessionId, 'sess_subagent_agent_u1');
94
+ assert.equal(spawned.name, 'Count kts files');
95
+ assert.equal(spawned.task, 'Count the .kts files.');
96
+ assert.deepEqual(spawned.capabilities, {});
97
+ obs.handleEvent(agentTerminal()); // terminal arrives first (measured order)
98
+ assert.equal(rootCx.sent.length, 1, 'foreground completion is held for the parent result');
99
+ obs.handleEvent(parentResult({ success: true, content: '8\nagentId: agent_u1 (continue)\n<usage>subagent_tokens: 21127\ntool_uses: 1\nduration_ms: 4477</usage>' }));
100
+ names = rootCx.sent.map((s) => s.params.update.sessionUpdate);
101
+ assert.deepEqual(names, ['subagent_spawned', 'subagent_state_update']);
102
+ assert.equal(rootCx.sent[1].params.update.state, 'completed');
103
+ assert.equal(server.__childCx.sent.length, 1, 'the preview lands on the child session');
104
+ assert.equal(server.__childCx.sent[0].params.sessionId, 'sess_subagent_agent_u1');
105
+ assert.equal(server.__childCx.sent[0].params.update.content.text, '8');
106
+ }
107
+
108
+ // 1b. Foreground with a string-form result payload (both encodings measured).
109
+ rootCxReset();
110
+ {
111
+ const server = fakeServer(CAPS_NATIVE);
112
+ const obs = new serverMod.AxomZcodeSubagents(server, 'z-1');
113
+ obs.handleEvent(agentSpawn());
114
+ obs.handleEvent(parentResult(JSON.stringify({ success: true, content: 'nine\nagentId: agent_u1 (x)\n<usage>d</usage>' })));
115
+ obs.handleEvent(agentTerminal());
116
+ assert.equal(server.__childCx.sent[0].params.update.content.text, 'nine');
117
+ assert.deepEqual(rootCx.sent.map((s) => s.params.update.sessionUpdate), ['subagent_spawned', 'subagent_state_update']);
118
+ }
119
+
120
+ // 2. Background agent: state fires at the agent terminal, no preview chunk.
121
+ rootCxReset();
122
+ {
123
+ const server = fakeServer(CAPS_NATIVE);
124
+ const obs = new serverMod.AxomZcodeSubagents(server, 'z-1');
125
+ obs.handleEvent(agentSpawn({ background: true, outputFile: '/tmp/agent/out.txt' }));
126
+ obs.handleEvent({ type: 'session.updated', payload: { taskId: 'agent_u1', taskKind: 'subagent', status: 'running', toolCallId: 'call_p1' } });
127
+ assert.deepEqual(rootCx.sent.map((s) => s.params.update.sessionUpdate), ['subagent_spawned'],
128
+ 'taskKind=subagent events belong to the agent path, never async_task');
129
+ obs.handleEvent(agentTerminal({ background: true, outputFile: '/tmp/agent/out.txt' }));
130
+ assert.deepEqual(rootCx.sent.map((s) => s.params.update.sessionUpdate), ['subagent_spawned', 'subagent_state_update']);
131
+ assert.equal(server.__childCx.sent.length, 0, 'background agents stream no child preview');
132
+ }
133
+
134
+ // 3. Adoption: first sighting already terminal emits spawned(adopted) then state.
135
+ rootCxReset();
136
+ {
137
+ const server = fakeServer(CAPS_NATIVE);
138
+ const obs = new serverMod.AxomZcodeSubagents(server, 'z-1');
139
+ obs.handleEvent(agentTerminal());
140
+ assert.deepEqual(rootCx.sent.map((s) => s.params.update.sessionUpdate), ['subagent_spawned', 'subagent_state_update']);
141
+ assert.equal(rootCx.sent[0].params.update._meta.zcode.adopted, true);
142
+ }
143
+
144
+ // 4. Failed / cancelled mapping; unknown statuses ignored; replay idempotent.
145
+ rootCxReset();
146
+ {
147
+ const server = fakeServer(CAPS_NATIVE);
148
+ const obs = new serverMod.AxomZcodeSubagents(server, 'z-1');
149
+ obs.handleEvent(agentSpawn({ agentId: 'agent_f', childSessionId: 'sess_subagent_agent_f', parentToolCallId: 'call_pf' }));
150
+ obs.handleEvent(agentSpawn({ agentId: 'agent_f', childSessionId: 'sess_subagent_agent_f', status: 'failed' }));
151
+ obs.handleEvent(agentSpawn({ agentId: 'agent_c', childSessionId: 'sess_subagent_agent_c', parentToolCallId: 'call_pc', status: 'cancelled' }));
152
+ obs.handleEvent(agentSpawn({ agentId: 'agent_r', childSessionId: 'sess_subagent_agent_r', status: 'paused' }));
153
+ const states = rootCx.sent.filter((s) => s.params.update.sessionUpdate === 'subagent_state_update').map((s) => s.params.update.state);
154
+ assert.deepEqual(states.sort(), ['cancelled', 'failed'], 'paused is not terminal; failed maps directly (foreground, non-completed)');
155
+ obs.handleEvent(agentSpawn({ agentId: 'agent_f', childSessionId: 'sess_subagent_agent_f', status: 'completed' }));
156
+ const statesAfter = rootCx.sent.filter((s) => s.params.update.sessionUpdate === 'subagent_state_update').map((s) => s.params.update.state);
157
+ assert.equal(statesAfter.filter((s) => s === 'completed').length, 0, 'terminal is single-shot');
158
+ }
159
+
160
+ // 5. Background bash task → AIR async_task pair.
161
+ rootCxReset();
162
+ {
163
+ const server = fakeServer(CAPS_NATIVE);
164
+ const obs = new serverMod.AxomZcodeSubagents(server, 'z-1');
165
+ obs.handleEvent(taskEvent());
166
+ obs.handleEvent(taskEvent({ status: 'running', pid: 1 })); // duplicate sighting
167
+ assert.deepEqual(rootCx.sent.map((s) => s.params.update.sessionUpdate), ['async_task_spawned']);
168
+ const spawned = rootCx.sent[0].params.update;
169
+ assert.equal(spawned.asyncTaskId, 'exec_t1');
170
+ assert.equal(spawned.toolCallId, 'call_b1');
171
+ assert.equal(spawned.outputFilePath, '/tmp/out.log');
172
+ assert.equal(spawned.canStop, true);
173
+ assert.equal(spawned.showInTranscript, false);
174
+ obs.handleEvent(taskEvent({ status: 'failed' }));
175
+ obs.handleEvent(taskEvent({ status: 'completed' })); // single-shot terminal
176
+ const states = rootCx.sent.filter((s) => s.params.update.sessionUpdate === 'async_task_state_update').map((s) => s.params.update.state);
177
+ assert.deepEqual(states, ['failed']);
178
+ }
179
+
180
+ // 6. Workflow tasks and non-session.updated events are ignored.
181
+ rootCxReset();
182
+ {
183
+ const server = fakeServer(CAPS_NATIVE);
184
+ const obs = new serverMod.AxomZcodeSubagents(server, 'z-1');
185
+ obs.handleEvent(taskEvent({ taskId: 'wf1', taskKind: 'workflow' }));
186
+ obs.handleEvent({ type: 'turn.started', payload: { inputSource: 'background_task' } });
187
+ obs.handleEvent({ type: 'model.streaming', payload: { kind: 'text_delta', delta: 'x' } });
188
+ obs.handleEvent(undefined);
189
+ assert.equal(rootCx.sent.length, 0);
190
+ }
191
+
192
+ // 7. Client without the draft capability: nothing is emitted (upstream behavior).
193
+ rootCxReset();
194
+ {
195
+ const server = fakeServer(CAPS_PLAIN);
196
+ const obs = new serverMod.AxomZcodeSubagents(server, 'z-1');
197
+ obs.handleEvent(agentSpawn());
198
+ obs.handleEvent(agentTerminal());
199
+ obs.handleEvent(taskEvent({ status: 'completed' }));
200
+ assert.equal(rootCx.sent.length, 0);
201
+ }
202
+
203
+ // 8. Dispatch re-scoping + mark gating against the patched dispatch module.
204
+ {
205
+ const server = fakeServer(CAPS_NATIVE);
206
+ const cx = recorder();
207
+ await dispatchMod.dispatchEvent(server, cx, 'root-1', {
208
+ kind: 'ToolCallNew', tool: 'Bash', callId: 'tool_subagent_agent_u1_call_inner1', title: 'inner bash', status: 'pending',
209
+ input: { command: 'ls' },
210
+ });
211
+ assert.equal(cx.sent.length, 1);
212
+ assert.equal(cx.sent[0].params.sessionId, 'sess_subagent_agent_u1', 'internal sub-agent frames are re-scoped to the child session');
213
+ assert.equal(cx.sent[0].params.update.toolCallId, 'tool_subagent_agent_u1_call_inner1');
214
+ assert.equal(cx.sent[0].params.update._meta.subagent, undefined, 'Bash frames never carry the mark');
215
+
216
+ const cxAgent = recorder();
217
+ await dispatchMod.dispatchEvent(server, cxAgent, 'root-1', {
218
+ kind: 'ToolCallNew', tool: 'Agent', callId: 'call_p1', title: 'Agent: x', status: 'pending', input: {},
219
+ });
220
+ assert.equal(cxAgent.sent[0].params.update._meta.subagent, undefined,
221
+ 'with native subagents negotiated the mark is suppressed (the native events own the projection)');
222
+
223
+ const plainServer = fakeServer(CAPS_PLAIN);
224
+ const cxPlain = recorder();
225
+ await dispatchMod.dispatchEvent(plainServer, cxPlain, 'root-1', {
226
+ kind: 'ToolCallNew', tool: 'Agent', callId: 'call_p1', title: 'Agent: x', status: 'pending', input: {},
227
+ });
228
+ assert.equal(cxPlain.sent[0].params.update._meta.subagent, true, 'clients without the draft capability still get the mark');
229
+ }
230
+
231
+ // 9. Background-task card suppression: with the AIR async-task capability declared,
232
+ // the upstream [background] card must not mint beside the async_task events.
233
+ rootCxReset();
234
+ {
235
+ const server = fakeServer(CAPS_NATIVE);
236
+ const listener = new bgMod.BackgroundTaskListener(server, 'z-1');
237
+ listener.handleEvent(taskEvent());
238
+ listener.handleEvent(taskEvent({ status: 'completed' }));
239
+ assert.equal(rootCx.sent.length, 0, 'with asyncTasks declared the bg listener mints no card — AxomZcodeSubagents owns the task');
240
+ // Sub-agent background tasks are equally owned by the subagent path.
241
+ listener.handleEvent({ type: 'session.updated', payload: { taskId: 'agent_u1', taskKind: 'subagent', toolCallId: 'call_p1', status: 'running', description: 'd' } });
242
+ assert.equal(rootCx.sent.length, 0);
243
+ // Workflow runs keep the upstream fold/poller path — terminal status, so no poller
244
+ // arms; an unfolded terminal-first sighting still mints the upstream fallback card.
245
+ listener.handleEvent({ type: 'session.updated', payload: { taskId: 'wf1', taskKind: 'workflow', toolCallId: 'call_w1', status: 'completed', description: 'w' } });
246
+ assert.equal(rootCx.sent.length, 1, 'workflow tasks keep the upstream background-card path (not suppressed)');
247
+
248
+ rootCxReset();
249
+ const plainServer = fakeServer(CAPS_PLAIN);
250
+ const plainListener = new bgMod.BackgroundTaskListener(plainServer, 'z-1');
251
+ plainListener.handleEvent(taskEvent());
252
+ assert.equal(rootCx.sent.length, 1, 'clients without the AIR capability still get the upstream [background] card');
253
+ assert.equal(rootCx.sent[0].params.update.sessionUpdate, 'tool_call');
254
+ assert.ok(rootCx.sent[0].params.update.toolCallId.startsWith('bg_'), 'the upstream card keeps its bg_* id');
255
+ }
256
+
257
+ console.log('PASS zcode-subagent-subwork (patched: variants, child scoping, mark gating, no double task cards)');