@platless/axom-cli 0.1.0 → 0.5.9

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 (281) hide show
  1. package/README.md +1 -1
  2. package/dist/authScopes.js +19 -0
  3. package/dist/axom-patch/cli/README.md +132 -0
  4. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  6. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  7. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  8. package/dist/axom-patch/cli/account-limits/generate.py +120 -0
  9. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
  10. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  11. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  12. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  13. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  14. package/dist/axom-patch/cli/nightly-task.md +152 -0
  15. package/dist/axom-patch/cli/pi-steering.json +37 -0
  16. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  17. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +38 -0
  18. package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
  19. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  20. package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
  21. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +61 -0
  22. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  23. package/dist/axom-patch/cli/qoder-background-terminal.md +82 -0
  24. package/dist/axom-patch/cli/qoder-foreground-output.md +42 -0
  25. package/dist/axom-patch/cli/qoder-steering.json +52 -0
  26. package/dist/axom-patch/cli/qoder-steering.md +30 -0
  27. package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
  28. package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
  29. package/dist/axom-patch/cli/runtime.json +63 -0
  30. package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
  31. package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
  32. package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
  33. package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
  34. package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
  35. package/dist/axom-patch/cli/zcode-models.json +38 -0
  36. package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
  37. package/dist/cli.js +36 -1
  38. package/dist/commands/auth.js +4 -13
  39. package/dist/commands/execute.js +4 -4
  40. package/dist/commands/record.js +6 -6
  41. package/dist/commands/struct.js +4 -4
  42. package/dist/commands/workstation.js +2 -2
  43. package/dist/credentials.js +2 -2
  44. package/dist/install/claude/commands.js +13 -13
  45. package/dist/keepers/acp/keeper.mjs +3083 -0
  46. package/dist/keepers/acp/manifest.json +9 -0
  47. package/dist/keepers/terminal/keeper.mjs +8469 -0
  48. package/dist/keepers/terminal/manifest.json +9 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  52. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  53. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  54. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  55. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  56. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  57. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  58. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  59. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  60. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  61. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  62. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  63. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  64. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  65. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  66. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  67. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  68. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  69. package/dist/mcp.js +32 -14
  70. package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
  71. package/dist/runtime-deps/qoder-sdk/package.json +8 -0
  72. package/dist/test/keeperKit.js +67 -0
  73. package/dist/workstation/acp/acpAgents.js +2514 -681
  74. package/dist/workstation/acp/acpClient.js +95 -318
  75. package/dist/workstation/acp/activityBusy.js +43 -0
  76. package/dist/workstation/acp/activityReporting.js +54 -0
  77. package/dist/workstation/acp/adoption.js +28 -356
  78. package/dist/workstation/acp/authUrlSniffer.js +36 -0
  79. package/dist/workstation/acp/binaryInstall.js +100 -27
  80. package/dist/workstation/acp/builds/gc.js +245 -0
  81. package/dist/workstation/acp/builds/install/binary.js +28 -0
  82. package/dist/workstation/acp/builds/install/npm.js +235 -0
  83. package/dist/workstation/acp/builds/install/uv.js +254 -0
  84. package/dist/workstation/acp/builds/migrate.js +195 -0
  85. package/dist/workstation/acp/builds/model.js +128 -0
  86. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  87. package/dist/workstation/acp/builds/store.js +352 -0
  88. package/dist/workstation/acp/commandTableProbe.js +68 -0
  89. package/dist/workstation/acp/configOptions.js +103 -1
  90. package/dist/workstation/acp/delegationMcp.js +24 -0
  91. package/dist/workstation/acp/envSecrets.js +27 -0
  92. package/dist/workstation/acp/initializeResponse.js +285 -0
  93. package/dist/workstation/acp/installErrors.js +12 -2
  94. package/dist/workstation/acp/installProbe.js +1 -1
  95. package/dist/workstation/acp/managedPatches.js +144 -0
  96. package/dist/workstation/acp/outbox.js +330 -0
  97. package/dist/workstation/acp/patchAssets.js +80 -0
  98. package/dist/workstation/acp/permission.js +335 -37
  99. package/dist/workstation/acp/processor/checkpoint.js +87 -0
  100. package/dist/workstation/acp/processor/commands.js +35 -0
  101. package/dist/workstation/acp/processor/control.js +355 -0
  102. package/dist/workstation/acp/processor/emit.js +26 -0
  103. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  104. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  105. package/dist/workstation/acp/processor/origins.js +48 -0
  106. package/dist/workstation/acp/processor/output.js +67 -0
  107. package/dist/workstation/acp/processor/projection.js +42 -0
  108. package/dist/workstation/acp/processor/record.js +128 -0
  109. package/dist/workstation/acp/processor/runtime.js +181 -0
  110. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  111. package/dist/workstation/acp/processor/session.js +525 -0
  112. package/dist/workstation/acp/processor/sideChannel.js +80 -0
  113. package/dist/workstation/acp/processor/state.js +74 -0
  114. package/dist/workstation/acp/processor/step.js +459 -0
  115. package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
  116. package/dist/workstation/acp/processor/test/harness.js +120 -0
  117. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  118. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  119. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  120. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  121. package/dist/workstation/acp/processor/transport/local.js +269 -0
  122. package/dist/workstation/acp/processor/transport/types.js +78 -0
  123. package/dist/workstation/acp/processor/updates.js +364 -0
  124. package/dist/workstation/acp/processorSession.js +1512 -0
  125. package/dist/workstation/acp/qoderStartup.js +45 -0
  126. package/dist/workstation/acp/registry/catalog.js +385 -4
  127. package/dist/workstation/acp/registry/detect.js +55 -0
  128. package/dist/workstation/acp/registry/launch.js +61 -119
  129. package/dist/workstation/acp/sessionUpdateMapper.js +56 -27
  130. package/dist/workstation/acp/steering.js +58 -0
  131. package/dist/workstation/acp/switching.js +114 -0
  132. package/dist/workstation/acp/terminalLogin.js +171 -0
  133. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  134. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  135. package/dist/workstation/acp/test/daemonLife.js +106 -0
  136. package/dist/workstation/acp/test/fakeAgent.js +185 -9
  137. package/dist/workstation/acp/test/keeperWorld.js +267 -0
  138. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  139. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  140. package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
  141. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  142. package/dist/workstation/acp/test/raw/sources.js +249 -0
  143. package/dist/workstation/acp/test/realAdapter.js +377 -0
  144. package/dist/workstation/acp/test/scriptAgent.js +206 -0
  145. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  146. package/dist/workstation/acp/timedCommand.js +4 -2
  147. package/dist/workstation/acp/uvxInstall.js +72 -122
  148. package/dist/workstation/acp/validate.js +33 -5
  149. package/dist/workstation/agentFlow.js +103 -0
  150. package/dist/workstation/agentSessions.js +1686 -348
  151. package/dist/workstation/channels/agent.js +14 -6
  152. package/dist/workstation/channels/computer.js +203 -7
  153. package/dist/workstation/channels/computerExecs.js +200 -0
  154. package/dist/workstation/channels/management.js +86 -6
  155. package/dist/workstation/config.js +3 -1
  156. package/dist/workstation/connection/encryptedChannel.js +88 -21
  157. package/dist/workstation/daemon.js +309 -6
  158. package/dist/workstation/daemonctl.js +28 -2
  159. package/dist/workstation/desktop/binding.js +36 -0
  160. package/dist/workstation/desktop/bridge.js +212 -0
  161. package/dist/workstation/desktop/flow-control.js +52 -0
  162. package/dist/workstation/desktop/focused-text.js +130 -0
  163. package/dist/workstation/desktop/protocol.js +35 -0
  164. package/dist/workstation/exec/attach.js +126 -0
  165. package/dist/workstation/exec/command.js +40 -0
  166. package/dist/workstation/exec/durable.js +517 -0
  167. package/dist/workstation/exec/fallback.js +106 -0
  168. package/dist/workstation/exec/policy.js +837 -0
  169. package/dist/workstation/exec/shaping.js +211 -0
  170. package/dist/workstation/exec.js +3 -112
  171. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  172. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  173. package/dist/workstation/files/client.js +455 -0
  174. package/dist/workstation/files/errors.js +28 -0
  175. package/dist/workstation/files/fence.js +350 -0
  176. package/dist/workstation/files/find/fileIndex.js +153 -0
  177. package/dist/workstation/files/find/fuzzy.js +155 -0
  178. package/dist/workstation/files/host.js +122 -0
  179. package/dist/workstation/files/hostProtocol.js +2 -0
  180. package/dist/workstation/files/media/cache.js +620 -0
  181. package/dist/workstation/files/media/encoding.js +308 -0
  182. package/dist/workstation/files/media/estimator.js +100 -0
  183. package/dist/workstation/files/media/media.js +325 -0
  184. package/dist/workstation/files/media/packedAudio.js +23 -0
  185. package/dist/workstation/files/media/scheduler.js +551 -0
  186. package/dist/workstation/files/media/types.js +5 -0
  187. package/dist/workstation/files/scope.js +45 -0
  188. package/dist/workstation/files/structured.js +284 -0
  189. package/dist/workstation/files/text.js +356 -0
  190. package/dist/workstation/files/uploads.js +351 -0
  191. package/dist/workstation/journal.js +65 -236
  192. package/dist/workstation/keepers/acp/client.js +101 -0
  193. package/dist/workstation/keepers/acp/envelope.js +62 -0
  194. package/dist/workstation/keepers/acp/host.js +419 -0
  195. package/dist/workstation/keepers/acp/main.js +18 -0
  196. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  197. package/dist/workstation/keepers/acp/params.js +137 -0
  198. package/dist/workstation/keepers/acp/protocol.js +66 -0
  199. package/dist/workstation/keepers/acp/recover.js +120 -0
  200. package/dist/workstation/keepers/acp/session.js +584 -0
  201. package/dist/workstation/keepers/acp/stderr.js +154 -0
  202. package/dist/workstation/keepers/kit/activity.js +103 -0
  203. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  204. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  205. package/dist/workstation/keepers/kit/bundle.js +120 -0
  206. package/dist/workstation/keepers/kit/client.js +432 -0
  207. package/dist/workstation/keepers/kit/identity.js +21 -0
  208. package/dist/workstation/keepers/kit/ipc.js +112 -0
  209. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  210. package/dist/workstation/keepers/kit/launcher.js +249 -0
  211. package/dist/workstation/keepers/kit/lock.js +123 -0
  212. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  213. package/dist/workstation/keepers/kit/paths.js +75 -0
  214. package/dist/workstation/keepers/kit/protocol.js +43 -0
  215. package/dist/workstation/keepers/kit/server.js +518 -0
  216. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  217. package/dist/workstation/keepers/legacy.js +296 -0
  218. package/dist/workstation/keepers/policy.js +576 -0
  219. package/dist/workstation/keepers/stop.js +34 -0
  220. package/dist/workstation/keepers/terminal/client.js +213 -0
  221. package/dist/workstation/keepers/terminal/exec.js +981 -0
  222. package/dist/workstation/keepers/terminal/generations.js +129 -0
  223. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  224. package/dist/workstation/keepers/terminal/main.js +43 -0
  225. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  226. package/dist/workstation/keepers/terminal/params.js +27 -0
  227. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  228. package/dist/workstation/keepers/terminal/pty.js +330 -0
  229. package/dist/workstation/oauth/deviceAuthorization.js +21 -12
  230. package/dist/workstation/protocol/accountLimits.js +47 -0
  231. package/dist/workstation/protocol/agent.js +201 -8
  232. package/dist/workstation/protocol/common.js +36 -0
  233. package/dist/workstation/protocol/computer.js +107 -7
  234. package/dist/workstation/protocol/management.js +50 -2
  235. package/dist/workstation/qoderAdapter/backgroundProcess.js +118 -0
  236. package/dist/workstation/qoderAdapter/backgroundTerminal.js +193 -0
  237. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
  238. package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
  239. package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
  240. package/dist/workstation/qoderAdapter/contract.js +78 -0
  241. package/dist/workstation/qoderAdapter/foregroundOutput.js +36 -0
  242. package/dist/workstation/qoderAdapter/history.js +85 -0
  243. package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
  244. package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
  245. package/dist/workstation/qoderAdapter/main.js +95 -0
  246. package/dist/workstation/qoderAdapter/mapper.js +117 -0
  247. package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
  248. package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
  249. package/dist/workstation/qoderAdapter/permissions.js +61 -0
  250. package/dist/workstation/qoderAdapter/profile.js +93 -0
  251. package/dist/workstation/qoderAdapter/sdkDriver.js +48 -0
  252. package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
  253. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
  254. package/dist/workstation/qoderAdapter/session.js +319 -0
  255. package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
  256. package/dist/workstation/qoderAdapter/subwork.js +85 -0
  257. package/dist/workstation/qoderAdapter/taskControl.js +38 -0
  258. package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
  259. package/dist/workstation/qoderAdapter/types.js +1 -0
  260. package/dist/workstation/qoderAdapter/usage.js +37 -0
  261. package/dist/workstation/register.js +4 -0
  262. package/dist/workstation/systemMetrics.js +110 -1
  263. package/dist/workstation/tools/browser.js +394 -0
  264. package/dist/workstation/tools/client.js +277 -0
  265. package/dist/workstation/tools/info.js +50 -0
  266. package/dist/workstation/tools/keeperProcess.js +124 -0
  267. package/dist/workstation/tools/router.js +55 -0
  268. package/dist/workstation/tools/terminalRouter.js +222 -0
  269. package/dist/workstation/tools/validation.js +86 -0
  270. package/dist/workstation/update/coordinator.js +32 -0
  271. package/dist/workstation/update/npmInstaller.js +2 -0
  272. package/dist/workstation/update/ptyPreflight.js +45 -0
  273. package/dist/workstation/update/taskRegistry.js +141 -23
  274. package/dist/workstation/update/watchdog.js +66 -0
  275. package/package.json +20 -7
  276. package/skills/axom/SKILL.md +55 -17
  277. package/skills/axom-definition/SKILL.md +32 -32
  278. package/skills/axom-platform/SKILL.md +39 -39
  279. package/dist/workstation/acp/acpSession.js +0 -1047
  280. package/dist/workstation/acp/contract.js +0 -494
  281. package/dist/workstation/acp/draftInterceptor.js +0 -154
@@ -0,0 +1,38 @@
1
+ // Runs the actual installed adapter runtime classes, without launching Claude or accessing auth.
2
+ // node <probe> <package-directory> [--expect-bug]
3
+ import assert from 'node:assert/strict';
4
+ import path from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+
7
+ const directory = process.argv[2];
8
+ if (!directory) throw new Error('Supply the adapter package directory');
9
+ const { NativeSubagentRuntime } = await import(pathToFileURL(path.resolve(directory, 'dist/native-subagents.js')));
10
+ const { AsyncTaskRuntime } = await import(pathToFileURL(path.resolve(directory, 'dist/async-tasks.js')));
11
+ const expectBug = process.argv.includes('--expect-bug');
12
+ for (const state of ['completed', 'failed', 'stopped']) {
13
+ for (const closing of ['terminalState', 'terminalPromise']) {
14
+ const events = [];
15
+ const publish = async (notification) => { events.push(notification); };
16
+ // "air", not `true`: 0.90.0 renamed the first argument from `enabled` to `form` ("air" | "rfd"),
17
+ // and in the rfd form a finished child keeps receiving its own late updates by design. Axom
18
+ // declares the AIR extension meta, so "air" is the form it gets; on pre-0.90 versions the
19
+ // string is still truthy, so `enabled` behaves as before.
20
+ const native = new NativeSubagentRuntime('air', 'root', {}, publish, { log() {} });
21
+ const child = { sessionId: 'child', parentSessionId: 'root' };
22
+ native.childByToolCall.set('call', child);
23
+ const tasks = new AsyncTaskRuntime(true, 'root', publish, { routeOf: (call) => native.routeOfToolCall(call) });
24
+ await tasks.taskBackgrounded({ task_id: 'task', tool_use_id: 'call', task_type: 'shell', description: 'background shell' });
25
+ assert.ok(events.some((event) => event.update.sessionUpdate === 'async_task_spawned'));
26
+ child[closing] = closing === 'terminalState' ? 'completed' : Promise.resolve();
27
+ await tasks.taskNotification('task', state, 'done');
28
+ const terminal = events.filter((event) => event.update.sessionUpdate === 'async_task_state_update');
29
+ assert.equal(terminal.length, expectBug ? 0 : 1, `${closing}/${state}`);
30
+ if (!expectBug) {
31
+ assert.equal(terminal[0].sessionId, 'child');
32
+ assert.equal(terminal[0].update.state, state);
33
+ }
34
+ const lateText = { sessionId: 'root', update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: 'late' } } };
35
+ assert.equal(native.routeOfToolCall('call')(lateText), null, 'ordinary late transcript must remain filtered');
36
+ }
37
+ }
38
+ console.log(expectBug ? 'Reproduced all 6 upstream late-terminal losses' : 'Passed all 6 late-terminal cases; ordinary late transcript stays filtered');
@@ -0,0 +1,80 @@
1
+ // Execute the real published adapter classes, without models or credentials.
2
+ // node <probe> <pi-acp-package-directory> [--expect-unsupported]
3
+ import assert from 'node:assert/strict';
4
+ import { readFile, writeFile, mkdtemp, rm, chmod } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import path from 'node:path';
7
+ import { pathToFileURL } from 'node:url';
8
+ import { createRequire } from 'node:module';
9
+ const directory = path.resolve(process.argv[2]);
10
+ const require = createRequire(path.join(directory, 'package.json'));
11
+ const scratch = await mkdtemp(path.join(tmpdir(), 'axom-pi-runtime-'));
12
+ const previousCommand = process.env.PI_ACP_PI_COMMAND;
13
+ try {
14
+ const original = await readFile(path.join(directory, 'dist/index.js'), 'utf8');
15
+ let source = original.slice(0, original.lastIndexOf('// src/index.ts')).replace(/^#![^\n]*\n/, '');
16
+ source = source.replace(/from "([^"\n]+)"/g, (whole, specifier) => specifier.startsWith('@') || specifier === 'cross-spawn'
17
+ ? `from ${JSON.stringify(pathToFileURL(require.resolve(specifier)).href)}` : whole);
18
+ source += '\nexport { PiAcpAgent, PiAcpSession };\n';
19
+ await writeFile(path.join(scratch, 'package.json'), await readFile(path.join(directory, 'package.json')));
20
+ await writeFile(path.join(scratch, 'runtime.mjs'), source);
21
+ const { PiAcpAgent, PiAcpSession } = await import(pathToFileURL(path.join(scratch, 'runtime.mjs')));
22
+ const command = path.join(scratch, 'native-version');
23
+ await writeFile(command, '#!/bin/sh\nprintf "0.87.1\\n"\n');
24
+ await chmod(command, 0o700);
25
+ process.env.PI_ACP_PI_COMMAND = command;
26
+ const agent = Object.create(PiAcpAgent.prototype);
27
+ const initialized = await agent.initialize({ protocolVersion: 1, clientCapabilities: {} });
28
+ if (process.argv.includes('--expect-unsupported')) {
29
+ assert.equal(initialized._meta?.steering?.supported, undefined);
30
+ console.log('PASS pristine pi-acp has no steering extension');
31
+ } else {
32
+ assert.deepEqual(initialized._meta.steering, { supported: true, timings: ['next'] });
33
+ await writeFile(command, '#!/bin/sh\nprintf "0.70.6\\n"\n');
34
+ assert.equal((await agent.initialize({ protocolVersion: 1, clientCapabilities: {} }))._meta?.steering, undefined);
35
+ await writeFile(command, '#!/bin/sh\nprintf "0.87.1\\n"\n');
36
+ await agent.initialize({ protocolVersion: 1, clientCapabilities: {} });
37
+ console.log('PASS native version gating and next capability');
38
+ let streaming = false, nativeEvent, releaseReceipt;
39
+ const requests = [], updates = [];
40
+ const proc = {
41
+ onEvent(handler) { nativeEvent = handler; },
42
+ async getState() { return { isStreaming: streaming }; },
43
+ async prompt(message, images) { requests.push({ type: 'original', message, images }); streaming = true; },
44
+ async request(cmd) { requests.push(cmd); return new Promise(resolve => { releaseReceipt = resolve; }); },
45
+ };
46
+ const session = new PiAcpSession({ sessionId: 'fixture', cwd: scratch, mcpServers: [], proc,
47
+ conn: { async sessionUpdate(event) { updates.push(event); } } });
48
+ agent.sessions = { maybeGet(id) { return id === 'fixture' ? session : undefined; } };
49
+ const params = { sessionId: 'fixture', prompt: [{ type: 'text', text: 'inspect this' },
50
+ { type: 'image', mimeType: 'image/png', data: 'fixture-image' }], _meta: { steering: { idleBehavior: 'promptRequired', priority: 'next' } } };
51
+ assert.deepEqual(await agent.extMethod('_session/steering', params), { outcome: 'promptRequired' });
52
+ for (const priority of ['now', 'later']) {
53
+ await assert.rejects(agent.extMethod('_session/steering', { ...params, _meta: { steering: { priority } } }));
54
+ }
55
+ assert.equal(requests.length, 0);
56
+ let finished = false;
57
+ const originalTurn = session.prompt('original').then(reason => { finished = true; return reason; });
58
+ const injected = agent.extMethod('_session/steering', params);
59
+ while (!releaseReceipt) await new Promise(resolve => setTimeout(resolve, 1));
60
+ assert.deepEqual(requests[1], { type: 'prompt', message: 'inspect this',
61
+ images: [{ type: 'image', mimeType: 'image/png', data: 'fixture-image' }], streamingBehavior: 'steer' });
62
+ nativeEvent({ type: 'agent_settled' });
63
+ await new Promise(resolve => setTimeout(resolve, 5));
64
+ assert.equal(finished, false);
65
+ releaseReceipt({ success: true });
66
+ assert.deepEqual(await injected, { outcome: 'injected' });
67
+ await new Promise(resolve => setTimeout(resolve, 5));
68
+ assert.equal(finished, false, 'native continuation remains owned by original ACP prompt');
69
+ streaming = false;
70
+ nativeEvent({ type: 'agent_settled' });
71
+ assert.equal(await originalTurn, 'end_turn');
72
+ assert.equal(requests.length, 2, 'exactly one native submission for the inserted message');
73
+ assert.equal(updates.filter(event => event.update?._meta?.piAcp?.running === false).length, 1);
74
+ console.log('PASS real adapter content conversion, idle refusal, priority validation and tracked continuation');
75
+ }
76
+ } finally {
77
+ if (previousCommand === undefined) delete process.env.PI_ACP_PI_COMMAND;
78
+ else process.env.PI_ACP_PI_COMMAND = previousCommand;
79
+ await rm(scratch, { recursive: true, force: true });
80
+ }
@@ -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,89 @@
1
+ // Exercise the real native ACP methods, without model calls or credentials.
2
+ import assert from 'node:assert/strict';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+
7
+ export async function verifyQoderSteering(bundle) {
8
+ const slice = (start, end) => {
9
+ const from = bundle.indexOf(start), to = bundle.indexOf(end, from);
10
+ assert(from >= 0 && to > from, `Native Qoder lacks ${start}`);
11
+ return bundle.slice(from, to);
12
+ };
13
+ class RequestError extends Error {
14
+ static invalidParams(_data, message) { return new this(message); }
15
+ }
16
+ const methods = slice('axomQoderSteer(A){', 'async axomQoderPromptOnce(A,e,t,i,n){');
17
+ const Session = new Function('as', 'mte', `return class {
18
+ axomQoderSteeringMessages=[];active=true;disposed=false;promptRunning=true;
19
+ pendingPrompt=new AbortController;queryEngine={activeAbortController:{},abort:()=>{this.aborts++}};aborts=0;
20
+ ${methods}
21
+ }`)(RequestError, () => 'generated-id');
22
+ const request = text => ({ prompt: [{ type: 'text', text }],
23
+ _meta: { steering: { idleBehavior: 'promptRequired', priority: 'now' } } });
24
+ const session = new Session();
25
+ let finishFirst, finishSecond;
26
+ const inputs = [];
27
+ session.axomQoderPromptOnce = async (prompt, signal) => {
28
+ inputs.push(prompt);
29
+ assert.equal(signal, session.pendingPrompt.signal);
30
+ return new Promise(resolve => {
31
+ if (inputs.length === 1) finishFirst = resolve; else finishSecond = resolve;
32
+ });
33
+ };
34
+ let completed = false;
35
+ const pending = session.promptViaQueryEngine([{ type: 'text', text: 'original' }],
36
+ session.pendingPrompt.signal, 'prompt-id', 'user-id').then(value => { completed = true; return value; });
37
+ assert.deepEqual(session.axomQoderSteer(request('changed')), { outcome: 'injected' });
38
+ assert.equal(session.aborts, 1);
39
+ assert.equal(session.pendingPrompt.signal.aborted, false, 'steering must not cancel the host prompt');
40
+ finishFirst({ stopReason: 'cancelled' });
41
+ await new Promise(resolve => setImmediate(resolve));
42
+ assert.equal(completed, false, 'the interrupted native result must not finish the host turn');
43
+ assert.deepEqual(inputs[1], [{ type: 'text', text: 'changed' }]);
44
+ finishSecond({ stopReason: 'end_turn' });
45
+ assert.equal((await pending).stopReason, 'end_turn');
46
+ assert.equal(session.axomQoderSteeringMessages.length, 0);
47
+
48
+ // Native promptRunning remains true for a microtask after its query iterator finishes.
49
+ // Accepting a message there would report delivery after the wrapper already stopped draining.
50
+ const finished = new Session(); finished.queryEngine.activeAbortController = undefined;
51
+ assert.deepEqual(finished.axomQoderSteer(request('arrived at completion')),
52
+ { outcome: 'promptRequired', reason: 'noRunningTurn' });
53
+ assert.equal(finished.axomQoderSteeringMessages.length, 0);
54
+
55
+ for (const key of ['active', 'promptRunning']) {
56
+ const idle = new Session(); idle[key] = false;
57
+ assert.deepEqual(idle.axomQoderSteer(request('idle')), { outcome: 'promptRequired', reason: 'noRunningTurn' });
58
+ assert.equal(idle.aborts, 0);
59
+ }
60
+ const invalid = new Session();
61
+ for (const value of [request(' '), request('x'.repeat(131073)),
62
+ { ...request('x'), prompt: [{ type: 'image', data: 'x', mimeType: 'image/png' }] },
63
+ { ...request('x'), _meta: { steering: { priority: 'next' } } }]) {
64
+ assert.throws(() => invalid.axomQoderSteer(value));
65
+ }
66
+ assert.equal(invalid.aborts, 0);
67
+ assert.equal(invalid.axomQoderSteeringMessages.length, 0);
68
+
69
+ const stopped = new Session();
70
+ stopped.axomQoderPromptOnce = async () => { stopped.pendingPrompt.abort(); return { stopReason: 'cancelled' }; };
71
+ stopped.axomQoderSteer(request('must not run after Stop'));
72
+ assert.equal((await stopped.promptViaQueryEngine([], stopped.pendingPrompt.signal, 'p', 'u')).stopReason, 'cancelled');
73
+ assert.equal(stopped.axomQoderSteeringMessages.length, 0);
74
+
75
+ const broken = new Session();
76
+ broken.axomQoderPromptOnce = async () => { throw new Error('real failure'); };
77
+ broken.axomQoderSteer(request('cannot hide a failure'));
78
+ await assert.rejects(broken.promptViaQueryEngine([], broken.pendingPrompt.signal, 'p', 'u'), /real failure/);
79
+ assert.equal(broken.axomQoderSteeringMessages.length, 0);
80
+ }
81
+
82
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
83
+ const directory = process.argv[2];
84
+ if (!directory) throw new Error('Usage: node qoder-steering.mjs <patched-qoder-package>');
85
+ const metadata = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8'));
86
+ assert.equal(metadata.name, '@qoder-ai/qodercli'); assert.equal(metadata.version, '1.1.71');
87
+ await verifyQoderSteering(await readFile(join(directory, 'bundle/qodercli.js'), 'utf8'));
88
+ console.log('Native Qoder steering: host-turn lifetime, interruption, idle, validation, Stop and failure checks passed');
89
+ }
@@ -0,0 +1,61 @@
1
+ // Run against the hash-verified patched official bundle. Exercises its actual ACP dispatch methods
2
+ // without starting a model or reading credentials. Real model acceptance is separate.
3
+ import { readFile } from 'node:fs/promises';
4
+ import assert from 'node:assert/strict';
5
+ import { join } from 'node:path';
6
+ const directory = process.argv[2];
7
+ if (!directory) throw new Error('Usage: node qoder-subagent-origin.mjs <patched-package-directory>');
8
+ const metadata = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8'));
9
+ assert.equal(metadata.name, '@qoder-ai/qodercli'); assert.equal(metadata.version, '1.1.71');
10
+ const bundle = await readFile(join(directory, 'bundle/qodercli.js'), 'utf8');
11
+ const method = (start, end) => {
12
+ const from = bundle.indexOf(start), to = bundle.indexOf(end, from);
13
+ assert(from >= 0 && to > from, `Missing real ACP method ${start}`);
14
+ return bundle.slice(from, to);
15
+ };
16
+ const helper = method('axomQoderId(value)', 'supportsTerminalOutput(){');
17
+ const dispatch = method('async dispatchSDKMessage(A)', 'createAcpSlashCommandHandler(){');
18
+ // 1.1.71 moved tool-call emission behind a `toolCallPublisher` whose constructor callback is
19
+ // `A => this.sendUpdate(A)` — still the patched sendUpdate, so the meta injection survives; the
20
+ // stub reproduces that wiring. `dkA`/`Fc`/`CtA`/`Y9` are the slice's free variables.
21
+ const Session = new Function('dkA', 'Fc', 'CtA', 'Y9', `return class {
22
+ axomQoderToolOrigins=new Map;toolCallNames=new Map;permissionToolCallIds=new Set;
23
+ toolCallPublisher;
24
+ constructor(connection){this.connection=connection;this.id='root';this.context={toolRegistry:{getTool:()=>({kind:'read'})},config:{isTasksEnabled:()=>false}};
25
+ this.toolCallPublisher={
26
+ declare: async u => this.sendUpdate(u),
27
+ result: async u => this.sendUpdate({ sessionUpdate: 'tool_call_update', toolCallId: u.toolCallId, content: u.content, isError: u.isError }),
28
+ }}
29
+ supportsTerminalOutput(){return false} async sendPlanUpdate(){}
30
+ ${helper} ${dispatch}
31
+ }`)(v => ({ sessionUpdate: 'tool_call', toolCallId: v.toolCallId, kind: v.kind, rawInput: v.input, status: v.status }),
32
+ 'Plan', new Set(), () => false);
33
+ const events = [];
34
+ const session = new Session({ sessionUpdate: async value => events.push(value) });
35
+ const tool = (id, parent) => ({ type: 'assistant', parent_tool_use_id: parent,
36
+ message: { content: [{ type: 'tool_use', id, name: 'Read', input: { path: '/probe' } }] } });
37
+ await session.dispatchSDKMessage(tool('read-a', 'agent-a'));
38
+ await session.dispatchSDKMessage(tool('read-b', 'agent-b'));
39
+ await session.dispatchSDKMessage({ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 'read-b', content: 'B' }] } });
40
+ await session.dispatchSDKMessage({ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 'read-a', content: 'A' }] } });
41
+ for (const [id, parent] of [['read-a', 'agent-a'], ['read-b', 'agent-b']]) {
42
+ const owned = events.filter(v => v.update.toolCallId === id);
43
+ assert.equal(owned.length, 2);
44
+ for (const event of owned) assert.equal(event.update._meta.qoder.parentToolUseId, parent);
45
+ }
46
+ await session.dispatchSDKMessage({ type: 'stream_event', parent_tool_use_id: 'agent-b',
47
+ event: { type: 'content_block_delta', delta: { type: 'text_delta', text: 'child B' } } });
48
+ assert.equal(events.at(-1).update._meta.qoder.parentToolUseId, 'agent-b');
49
+ await session.dispatchSDKMessage({ type: 'stream_event', parent_tool_use_id: null,
50
+ event: { type: 'content_block_delta', delta: { type: 'text_delta', text: 'root' } } });
51
+ assert.equal(events.at(-1).update._meta, undefined);
52
+ await session.dispatchSDKMessage(tool('root-read', null));
53
+ assert.equal(events.at(-1).update._meta.qoder.parentToolUseId, undefined);
54
+ // 1.1.61 skipped a re-declared tool id entirely; 1.1.71 re-declares it, so the conflict shows up
55
+ // as a freshly emitted tool_call whose origin was invalidated — the intent (a conflicting parent
56
+ // must not tag the tool as a child) is asserted on whatever the conflict emitted.
57
+ await session.dispatchSDKMessage(tool('read-a', 'wrong-agent'));
58
+ const conflict = events.at(-1).update;
59
+ assert.equal(conflict.toolCallId, 'read-a');
60
+ assert.ok(!conflict._meta?.qoder?.parentToolUseId, 'conflicting re-declare must not carry a parent');
61
+ console.log('Official Qoder 1.1.71 ACP dispatch: parallel tool/result, child text, root and conflict checks passed');