@desplega.ai/agent-swarm 1.145.0 → 1.147.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/README.md +2 -1
  2. package/dist/{acp-adapter-580t41c0.js → acp-adapter-9n319wqc.js} +61 -6
  3. package/dist/{actions-m662tjng.js → actions-2vxqvpr9.js} +9 -8
  4. package/dist/{app-zb3rndm8.js → app-w066xfy0.js} +6 -5
  5. package/dist/assistant-kt31dj2h.js +236 -0
  6. package/dist/{boot-reembed-072x00vn.js → boot-reembed-az5rassp.js} +7 -6
  7. package/dist/{boot-reembed-65zz2smt.js → boot-reembed-j3mm3rfz.js} +8 -7
  8. package/dist/{boot-scrub-logs-yz0ams04.js → boot-scrub-logs-b6h54817.js} +4 -4
  9. package/dist/{claude-adapter-paj270y2.js → claude-adapter-w1z04ann.js} +8 -6
  10. package/dist/{claude-managed-setup-w619jncn.js → claude-managed-setup-f5pmsyrp.js} +24 -3
  11. package/dist/{claude-sdk-session-swt80j9z.js → claude-sdk-session-f0cc1kec.js} +8 -6
  12. package/dist/{cli-dt553ktb.js → cli-0b6y7kxm.js} +1 -1
  13. package/dist/{cli-bxdhw615.js → cli-12fz972k.js} +4 -4
  14. package/dist/{cli-s2f20r5w.js → cli-1th1728g.js} +2 -2
  15. package/dist/{cli-rs6ec3b9.js → cli-3tgwnf43.js} +3 -3
  16. package/dist/{cli-gxnzc6wh.js → cli-4nr988h6.js} +1 -1
  17. package/dist/{cli-axs8e6d4.js → cli-53s590z8.js} +1 -1
  18. package/dist/{cli-ygbjp3z8.js → cli-54wwve05.js} +5 -4
  19. package/dist/{cli-v0asr5yv.js → cli-719k9j2c.js} +2 -2
  20. package/dist/{cli-anktweqr.js → cli-89xfd7f9.js} +21 -47
  21. package/dist/{cli-q34a0e10.js → cli-8v0g1yc8.js} +1 -1
  22. package/dist/cli-9d8ntetj.js +42 -0
  23. package/dist/{cli-z9fprcrf.js → cli-atcve9y8.js} +881 -738
  24. package/dist/{cli-jmb7g47h.js → cli-b5bq9crk.js} +14 -31
  25. package/dist/cli-d8brzxjb.js +190 -0
  26. package/dist/{cli-gzky0gkr.js → cli-de14znh8.js} +2 -2
  27. package/dist/{cli-xytwff7f.js → cli-dttph1bw.js} +2 -2
  28. package/dist/cli-ep19k3mb.js +19 -0
  29. package/dist/{cli-m1xa8be7.js → cli-f146mn5s.js} +7 -50
  30. package/dist/{cli-1fj0chdk.js → cli-f7dsy61y.js} +5 -3
  31. package/dist/{cli-f567d5c1.js → cli-ftht3zzj.js} +2782 -2589
  32. package/dist/{cli-a0g1gbt5.js → cli-hbv0kq6w.js} +2 -2
  33. package/dist/{cli-fhsz3twr.js → cli-jsp14b6c.js} +171 -1
  34. package/dist/{cli-7fnkj1hj.js → cli-kpv03zhj.js} +21 -17
  35. package/dist/{cli-m4j6trn4.js → cli-m8yrsg97.js} +2 -2
  36. package/dist/cli-m9sxbkhm.js +779 -0
  37. package/dist/cli-mmemxxdc.js +823 -0
  38. package/dist/{cli-tqtvj5pt.js → cli-n8508nre.js} +1 -1
  39. package/dist/{cli-z96t0thj.js → cli-p1d5b073.js} +37 -2
  40. package/dist/{cli-gh8y8f36.js → cli-rw51bq3j.js} +13 -10
  41. package/dist/{cli-59ej1c07.js → cli-smby5m17.js} +7 -6
  42. package/dist/{cli-65vqzz3v.js → cli-tzvk9haz.js} +94 -372
  43. package/dist/{cli-dc9k2ba9.js → cli-w9kb3bk1.js} +1 -1
  44. package/dist/{cli-kpev657q.js → cli-y6b6fb76.js} +25 -3
  45. package/dist/{cli-123q2get.js → cli-yz3djrm0.js} +9 -9
  46. package/dist/{cli-wvp6bvy4.js → cli-zhjmqxzh.js} +1 -1
  47. package/dist/cli.js +13 -13
  48. package/dist/{codex-adapter-k98vaw2r.js → codex-adapter-zw4dwsx7.js} +2 -2
  49. package/dist/{codex-hook-9pf19cks.js → codex-hook-bw8p5nh4.js} +2 -2
  50. package/dist/{codex-session-runner-tngtphx7.js → codex-session-runner-k981s65v.js} +2 -2
  51. package/dist/{commands-y8nbr3fv.js → commands-sykrxx7e.js} +4 -4
  52. package/dist/{db-30p4h2t1.js → db-sfbsc8wt.js} +6 -4
  53. package/dist/handlers-0aspcejm.js +603 -0
  54. package/dist/{hook-dcw4jfbj.js → hook-4nvqj2sb.js} +7 -5
  55. package/dist/{hook-21jexvfx.js → hook-hxccs7m5.js} +24 -49
  56. package/dist/{http-r4kq5g8d.js → http-y06z517d.js} +230 -94
  57. package/dist/{index-q3qja6ta.js → index-hssww7kj.js} +15 -14
  58. package/dist/{index-gay66b9w.js → index-k8znfx0z.js} +13 -12
  59. package/dist/{index-53zyht4z.js → index-tce1rss4.js} +371 -110
  60. package/dist/{index-nqvf26za.js → index-x5j894zf.js} +14 -13
  61. package/dist/{keepalive-xs8fa55y.js → keepalive-w733ax66.js} +6 -6
  62. package/dist/{lead-d9m2t4s0.js → lead-75bjqa9d.js} +29 -27
  63. package/dist/{maintenance-3zh82g6z.js → maintenance-w4f1zjck.js} +8 -7
  64. package/dist/{oauth-refresh-sweep-03snsye5.js → oauth-refresh-sweep-wdetky26.js} +5 -5
  65. package/dist/{onboard-0cg1925x.js → onboard-bb5net8s.js} +2 -2
  66. package/dist/{otel-impl-0ab95z27.js → otel-impl-0w7c14ap.js} +1 -1
  67. package/dist/{pi-mono-adapter-dtkz4e7j.js → pi-mono-adapter-2has4gnp.js} +12 -2
  68. package/dist/{pricing-refresh-e5jhzz4j.js → pricing-refresh-9nzsrgcq.js} +6 -6
  69. package/dist/{rbac-roles-p1mf40z3.js → rbac-roles-r3hqjp09.js} +5 -5
  70. package/dist/{rbac-roles-2w3cmrkc.js → rbac-roles-z1jrffbt.js} +4 -4
  71. package/dist/{registry-9kdck5ye.js → registry-v5wytg0s.js} +1 -2
  72. package/dist/{render-v2-xhawry3k.js → render-v2-vbr89fnb.js} +6 -5
  73. package/dist/{seed-pricing-t223sw91.js → seed-pricing-g62hy1pk.js} +7 -5
  74. package/dist/{setup-zcs0rce7.js → setup-gnfqnptp.js} +2 -2
  75. package/dist/{worker-qnp2ka62.js → worker-kznxp4qm.js} +29 -27
  76. package/openapi.json +263 -12
  77. package/package.json +1 -1
  78. package/plugin/commands/start-worker.md +4 -9
  79. package/plugin/pi-skills/start-worker/SKILL.md +4 -9
  80. package/src/be/db/agents.ts +556 -0
  81. package/src/be/db/context-versions.ts +117 -0
  82. package/src/be/db/runtime.ts +406 -0
  83. package/src/be/db/tasks/read.ts +1167 -0
  84. package/src/be/db/tasks/write.ts +953 -0
  85. package/src/be/db.ts +584 -3429
  86. package/src/be/migrations/147_scheduled_tasks_parent_task.sql +7 -0
  87. package/src/be/migrations/148_pages_svg.sql +123 -0
  88. package/src/be/migrations/149_session_tokens.sql +24 -0
  89. package/src/be/scripts/typecheck.ts +1 -0
  90. package/src/be/seed-pricing.ts +13 -8
  91. package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
  92. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
  93. package/src/be/seed-scripts/catalog/star-history-refresh.ts +101 -0
  94. package/src/be/seed-scripts/catalog/star-history-renderer.ts +119 -0
  95. package/src/be/seed-scripts/index.ts +17 -0
  96. package/src/be/seed-skills/index.ts +14 -0
  97. package/src/be/swarm-config-guard.ts +5 -2
  98. package/src/be/task-attachment-store.ts +70 -0
  99. package/src/be/users.ts +77 -0
  100. package/src/commands/attachments-section.ts +37 -0
  101. package/src/commands/claude-managed-setup.ts +45 -3
  102. package/src/commands/claude-md-session.ts +408 -0
  103. package/src/commands/context-preamble.ts +15 -3
  104. package/src/commands/profile-sync.ts +205 -5
  105. package/src/commands/provider-credentials.ts +60 -27
  106. package/src/commands/runner.ts +63 -58
  107. package/src/commands/templates.ts +25 -2
  108. package/src/hooks/hook.ts +26 -67
  109. package/src/http/agents.ts +40 -1
  110. package/src/http/auth.ts +13 -1
  111. package/src/http/fs.ts +21 -51
  112. package/src/http/index.ts +2 -0
  113. package/src/http/mcp.ts +32 -0
  114. package/src/http/pages-public.ts +20 -4
  115. package/src/http/pages.ts +11 -3
  116. package/src/http/poll.ts +4 -0
  117. package/src/http/repos.ts +3 -3
  118. package/src/http/sessions.ts +82 -0
  119. package/src/http/tasks.ts +10 -2
  120. package/src/http/utils.ts +79 -1
  121. package/src/prompts/session-templates.ts +12 -8
  122. package/src/providers/acp-adapter.ts +62 -8
  123. package/src/providers/pi-mono-adapter.ts +23 -6
  124. package/src/providers/pricing-sources.md +11 -0
  125. package/src/scheduler/scheduler.test.ts +49 -2
  126. package/src/scheduler/scheduler.ts +4 -0
  127. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  128. package/src/scripts-runtime/types/stdlib.d.ts +8 -0
  129. package/src/scripts-runtime/types/swarm-sdk.d.ts +8 -0
  130. package/src/server.ts +2 -0
  131. package/src/slack/assistant.ts +87 -46
  132. package/src/slack/handlers.test.ts +1 -3
  133. package/src/slack/handlers.ts +102 -87
  134. package/src/slack/inbound-files.ts +466 -0
  135. package/src/slack/templates.ts +14 -0
  136. package/src/tasks/task-terminal-effects.ts +158 -0
  137. package/src/tests/acp-adapter.test.ts +369 -117
  138. package/src/tests/budget-claim-gate.test.ts +1 -0
  139. package/src/tests/build-pi-skills.test.ts +5 -7
  140. package/src/tests/claude-managed-setup.test.ts +52 -4
  141. package/src/tests/claude-md-session.test.ts +603 -0
  142. package/src/tests/codex-skill-resolver.test.ts +19 -0
  143. package/src/tests/context-versioning.test.ts +107 -0
  144. package/src/tests/cors-allowlist.test.ts +309 -0
  145. package/src/tests/credential-check.test.ts +157 -14
  146. package/src/tests/db-agents.test.ts +91 -0
  147. package/src/tests/db-context-versions.test.ts +37 -0
  148. package/src/tests/db-extraction-contract.test.ts +45 -0
  149. package/src/tests/db-runtime.test.ts +70 -0
  150. package/src/tests/db-task-read-contract.test.ts +624 -0
  151. package/src/tests/db-task-write-contract.test.ts +653 -0
  152. package/src/tests/db-tasks-read.test.ts +183 -0
  153. package/src/tests/db-tasks-write.test.ts +230 -0
  154. package/src/tests/defer-task.test.ts +389 -0
  155. package/src/tests/docs-index-footprint.test.js +2 -2
  156. package/src/tests/e2e-visuals-stage.test.ts +151 -0
  157. package/src/tests/file-lock.test.ts +144 -0
  158. package/src/tests/fixtures/file-lock-holder.ts +13 -0
  159. package/src/tests/fixtures/hold-file-lock.ts +25 -0
  160. package/src/tests/heartbeat-expiry.test.ts +188 -0
  161. package/src/tests/http-api-integration.test.ts +78 -1
  162. package/src/tests/pages-public-html.test.ts +43 -0
  163. package/src/tests/pages-svg-migration.test.ts +42 -0
  164. package/src/tests/preload.ts +4 -1
  165. package/src/tests/profile-sync-warning.test.ts +16 -0
  166. package/src/tests/profile-sync.test.ts +126 -0
  167. package/src/tests/prompt-template-session.test.ts +20 -4
  168. package/src/tests/provider-registration.test.ts +93 -0
  169. package/src/tests/rbac-e2e-helpers.test.ts +53 -0
  170. package/src/tests/rbac-e2e-helpers.ts +19 -2
  171. package/src/tests/reload-config.test.ts +26 -0
  172. package/src/tests/routing-affinity-inventory.test.ts +1 -1
  173. package/src/tests/runner-attachments-section.test.ts +22 -2
  174. package/src/tests/runner-context-preamble.test.ts +30 -0
  175. package/src/tests/runner-deployment-resume.test.ts +107 -0
  176. package/src/tests/runner-repo-autostash.test.ts +12 -0
  177. package/src/tests/script-connections.test.ts +4 -0
  178. package/src/tests/seed-scripts.test.ts +2 -2
  179. package/src/tests/session-tokens.test.ts +279 -0
  180. package/src/tests/slack-file-tools-attachments.test.ts +408 -0
  181. package/src/tests/slack-inbound-files.test.ts +847 -0
  182. package/src/tests/star-history-refresh.test.ts +102 -0
  183. package/src/tests/swarm-config-reserved-keys.test.ts +21 -0
  184. package/src/tests/swarm-repos.test.ts +20 -0
  185. package/src/tests/tool-annotations.test.ts +4 -3
  186. package/src/tests/ui-logs-parser.test.ts +176 -0
  187. package/src/tools/create-page.ts +4 -2
  188. package/src/tools/defer-task.ts +239 -0
  189. package/src/tools/repos/update-repo.ts +2 -2
  190. package/src/tools/schedules/create-schedule.ts +1 -0
  191. package/src/tools/schedules/list-schedules.ts +1 -0
  192. package/src/tools/schedules/patch-schedule.ts +1 -0
  193. package/src/tools/schedules/update-schedule.ts +1 -0
  194. package/src/tools/slack-download-file.ts +60 -29
  195. package/src/tools/slack-read.ts +73 -32
  196. package/src/tools/store-progress.ts +16 -146
  197. package/src/tools/tool-config.ts +1 -0
  198. package/src/tools/utils.ts +21 -0
  199. package/src/types.ts +37 -4
  200. package/src/utils/acp-session-token.ts +56 -0
  201. package/src/utils/file-lock.ts +111 -0
  202. package/src/utils/heartbeat-expiry.ts +78 -0
  203. package/src/utils/request-auth-context.ts +2 -0
  204. package/src/utils/task-attachment-links.ts +22 -0
  205. package/templates/skills/code-quality/content.md +8 -5
  206. package/templates/skills/download-task-attachment/content.md +2 -1
  207. package/{plugin/pi-skills → templates/skills}/review-offered-task/SKILL.md +5 -5
  208. package/templates/skills/review-offered-task/config.json +14 -0
  209. package/{plugin/commands/review-offered-task.md → templates/skills/review-offered-task/content.md} +3 -8
  210. package/templates/skills/scheduling/content.md +4 -0
  211. package/templates/skills/slack-interaction/content.md +2 -2
  212. package/templates/skills/work-on-task/SKILL.md +16 -0
  213. package/templates/skills/work-on-task/config.json +14 -0
  214. package/templates/skills/work-on-task/content.md +11 -0
  215. package/dist/assistant-0qt42nzg.js +0 -212
  216. package/dist/cli-fs1kkqgr.js +0 -317
  217. package/dist/cli-jnf5zghe.js +0 -269
  218. package/dist/cli-n1q8q14d.js +0 -177
  219. package/dist/handlers-yfp1qn81.js +0 -592
  220. package/plugin/commands/work-on-task.md +0 -39
  221. package/plugin/pi-skills/work-on-task/SKILL.md +0 -39
@@ -1,15 +1,20 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { mkdtempSync, rmSync } from "node:fs";
3
+ import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
3
4
  import { tmpdir } from "node:os";
4
5
  import { join } from "node:path";
6
+ import type { Usage } from "@agentclientprotocol/sdk";
5
7
  import { normalizeSessionLogs } from "../../apps/ui/src/logs-parser";
6
8
  import {
7
9
  closeDb,
10
+ createAgent,
8
11
  createSessionLogs,
9
12
  createTaskExtended,
10
13
  getSessionLogsByTaskId,
11
14
  initDb,
12
15
  } from "../be/db";
16
+ import { handleSessionData } from "../http/session-data";
17
+ import { getPathSegments, parseQueryParams } from "../http/utils";
13
18
  import { createProviderAdapter } from "../providers";
14
19
  import {
15
20
  ACPAdapter,
@@ -19,6 +24,7 @@ import {
19
24
  } from "../providers/acp-adapter";
20
25
  import { AcpTargetResolutionError, resolveAcpTarget } from "../providers/acp-targets";
21
26
  import type { ProviderEvent, ProviderSessionConfig } from "../providers/types";
27
+ import { listenOnFreePort } from "./test-net";
22
28
 
23
29
  const tmpDirs: string[] = [];
24
30
 
@@ -51,6 +57,32 @@ function baseConfig(overrides: Partial<ProviderSessionConfig> = {}): ProviderSes
51
57
  };
52
58
  }
53
59
 
60
+ /**
61
+ * Start a minimal swarm API stub that handles POST /api/sessions/tokens and
62
+ * DELETE /api/sessions/tokens/:id. Returns the server and its base URL.
63
+ * The caller is responsible for closing the server.
64
+ */
65
+ async function startTokenStubServer(
66
+ tokenId: string,
67
+ plaintext: string,
68
+ ): Promise<{ server: Server; apiUrl: string }> {
69
+ const stub = createServer(async (req: IncomingMessage, res: ServerResponse) => {
70
+ if (req.method === "POST" && req.url === "/api/sessions/tokens") {
71
+ res.writeHead(200, { "Content-Type": "application/json" });
72
+ res.end(JSON.stringify({ tokenId, plaintext }));
73
+ } else if (req.method === "DELETE" && req.url?.startsWith("/api/sessions/tokens/")) {
74
+ res.writeHead(204);
75
+ res.end();
76
+ } else {
77
+ res.writeHead(404);
78
+ res.end();
79
+ }
80
+ });
81
+ await new Promise<void>((resolve) => stub.listen(0, "127.0.0.1", resolve));
82
+ const addr = stub.address() as import("net").AddressInfo;
83
+ return { server: stub, apiUrl: `http://127.0.0.1:${addr.port}` };
84
+ }
85
+
54
86
  describe("ACPAdapter", () => {
55
87
  test("is registered with MCP and local-environment traits", async () => {
56
88
  const adapter = await createProviderAdapter("acp");
@@ -68,7 +100,32 @@ describe("ACPAdapter", () => {
68
100
  );
69
101
  });
70
102
 
71
- test("redacts credential headers from arrays and nested maps before persistence", async () => {
103
+ test.each<{ name: string; usage?: Usage | null }>([
104
+ { name: "absent", usage: undefined },
105
+ { name: "null", usage: null },
106
+ {
107
+ name: "OpenCode observed",
108
+ usage: {
109
+ inputTokens: 84376,
110
+ outputTokens: 65,
111
+ totalTokens: 86233,
112
+ cachedReadTokens: 1792,
113
+ },
114
+ },
115
+ {
116
+ name: "populated",
117
+ usage: {
118
+ totalTokens: 180,
119
+ inputTokens: 100,
120
+ outputTokens: 30,
121
+ thoughtTokens: 10,
122
+ cachedReadTokens: 25,
123
+ cachedWriteTokens: 15,
124
+ },
125
+ },
126
+ ])("persists $name ACP usage with deliberate zero-coalescing of missing session-cost counters", async ({
127
+ usage,
128
+ }) => {
72
129
  const cwd = makeTempDir();
73
130
  const agentPath = join(cwd, "fake-acp-agent.ts");
74
131
  const sdkPath = join(process.cwd(), "node_modules/@agentclientprotocol/sdk/dist/acp.js");
@@ -222,7 +279,15 @@ class FakeAgent {
222
279
  rawOutput: { chunks: Array.from({ length: 20 }, () => "z".repeat(2_000)) },
223
280
  },
224
281
  });
225
- return { stopReason: "end_turn" };
282
+ return ${JSON.stringify({
283
+ stopReason: "end_turn",
284
+ usage,
285
+ _meta: {
286
+ debug: "ghp_abcdefghijklmnopqrstuvwxyz0123456789",
287
+ authorization: "opaque-response-credential",
288
+ note: "response metadata",
289
+ },
290
+ })};
226
291
  }
227
292
 
228
293
  async cancel() {}
@@ -235,133 +300,205 @@ new AgentSideConnection((connection) => new FakeAgent(connection), stream);
235
300
  `,
236
301
  );
237
302
 
238
- const adapter = new ACPAdapter();
239
- const session = await adapter.createSession(
240
- baseConfig({
241
- cwd,
242
- env: {
243
- PATH: process.env.PATH ?? "",
244
- HOME: process.env.HOME ?? "",
245
- ACP_TARGET_COMMAND: "bun",
246
- ACP_TARGET_ARGS: JSON.stringify([agentPath]),
247
- ACP_CONFIG_OPTIONS: JSON.stringify({ thought: "high" }),
248
- },
249
- }),
303
+ const { server: tokenStub, apiUrl } = await startTokenStubServer(
304
+ "stub-token-id",
305
+ "aseph_stubtokenfortest1234567890",
250
306
  );
251
307
 
252
- const events: ProviderEvent[] = [];
253
- session.onEvent((event) => events.push(event));
254
- const result = await session.waitForCompletion();
308
+ try {
309
+ const adapter = new ACPAdapter();
310
+ const session = await adapter.createSession(
311
+ baseConfig({
312
+ cwd,
313
+ apiUrl,
314
+ env: {
315
+ PATH: process.env.PATH ?? "",
316
+ HOME: process.env.HOME ?? "",
317
+ ACP_TARGET_COMMAND: "bun",
318
+ ACP_TARGET_ARGS: JSON.stringify([agentPath]),
319
+ ACP_CONFIG_OPTIONS: JSON.stringify({ thought: "high" }),
320
+ },
321
+ }),
322
+ );
255
323
 
256
- expect(result).toMatchObject({
257
- exitCode: 0,
258
- sessionId: "acp-session-1",
259
- output: "done",
260
- isError: false,
261
- });
262
- expect(events.some((event) => event.type === "session_init")).toBe(true);
263
- expect(events.find((event) => event.type === "session_init")).toMatchObject({
264
- providerMeta: {
265
- target: "custom",
266
- configOptions: [
267
- { id: "model", currentValue: "fake-model" },
268
- { id: "thought", currentValue: "high" },
269
- ],
270
- },
271
- });
272
- expect(events.some((event) => event.type === "message" && event.content === "done")).toBe(true);
273
- expect(events.some((event) => event.type === "tool_start")).toBe(true);
274
- expect(events.some((event) => event.type === "tool_end")).toBe(true);
275
- expect(events.some((event) => event.type === "result")).toBe(true);
276
-
277
- const rawLogs = events
278
- .filter(
279
- (event): event is Extract<ProviderEvent, { type: "raw_log" }> => event.type === "raw_log",
280
- )
281
- .map((event) => event.content);
282
- expect(rawLogs.length).toBeGreaterThan(0);
283
- expect(
284
- rawLogs.some((content) => {
285
- const event = JSON.parse(content) as Record<string, unknown>;
286
- const update = event.update as Record<string, unknown> | undefined;
287
- return update?.sessionUpdate === "agent_message_chunk";
288
- }),
289
- ).toBe(true);
290
- expect(
291
- rawLogs.some((content) => {
292
- const event = JSON.parse(content) as Record<string, unknown>;
293
- const update = event.update as Record<string, unknown> | undefined;
294
- return update?.sessionUpdate === "current_mode_update";
295
- }),
296
- ).toBe(true);
297
- expect(
298
- rawLogs.some((content) => {
299
- const event = JSON.parse(content) as Record<string, unknown>;
300
- return event.type === "message" && event.content === "done";
301
- }),
302
- ).toBe(true);
303
- expect(rawLogs.every((content) => content.length <= 30_000)).toBe(true);
324
+ const events: ProviderEvent[] = [];
325
+ session.onEvent((event) => events.push(event));
326
+ const result = await session.waitForCompletion();
304
327
 
305
- initDb(":memory:");
306
- try {
307
- const task = await createTaskExtended("ACP persistence test");
308
- await createSessionLogs({
309
- taskId: task.id,
310
- sessionId: session.sessionId,
311
- iteration: 1,
312
- cli: "acp",
313
- lines: rawLogs,
328
+ expect(result).toMatchObject({
329
+ exitCode: 0,
330
+ sessionId: "acp-session-1",
331
+ output: "done",
332
+ isError: false,
314
333
  });
315
- const persisted = await getSessionLogsByTaskId(task.id);
316
- expect(persisted).toHaveLength(rawLogs.length);
317
- expect(persisted.map((entry) => entry.content)).toEqual(rawLogs);
318
- expect(persisted.every((entry) => entry.cli === "acp")).toBe(true);
319
- const persistedJson = persisted.map((entry) => entry.content).join("\n");
320
- const credentialHeaderNames = [
321
- "authorization",
322
- "proxy-authorization",
323
- "cookie",
324
- "set-cookie",
325
- "www-authenticate",
326
- "proxy-authenticate",
327
- "x-api-key",
328
- "api-key",
329
- "x-auth-token",
330
- "x-access-token",
331
- "x-session-token",
332
- ];
333
- for (const headerName of credentialHeaderNames) {
334
- expect(persistedJson.toLowerCase()).not.toContain(headerName);
335
- }
336
- const credentialValues = credentialHeaderNames.flatMap((headerName) => [
337
- `opaque-array-${headerName}-credential`,
338
- `opaque-map-${headerName}-credential`,
339
- ]);
340
- for (const credentialValue of credentialValues) {
341
- expect(persistedJson).not.toContain(credentialValue);
342
- }
343
- expect(persistedJson).toContain("X-Debug");
344
- expect(persistedJson).toContain("X-Debug-Map");
345
- expect(persistedJson).not.toContain("ghp_abcdefghijklmnopqrstuvwxyz0123456789");
346
- expect(persistedJson).toContain("[REDACTED:github_token]");
347
- expect(persistedJson).toContain("… [truncated]");
348
- expect(persistedJson).not.toContain("x".repeat(30_001));
349
- const transcript = normalizeSessionLogs(persisted);
350
- expect(transcript.items.some((item) => item.kind === "unknown")).toBe(false);
334
+ expect(events.some((event) => event.type === "session_init")).toBe(true);
335
+ expect(events.find((event) => event.type === "session_init")).toMatchObject({
336
+ providerMeta: {
337
+ target: "custom",
338
+ configOptions: [
339
+ { id: "model", currentValue: "fake-model" },
340
+ { id: "thought", currentValue: "high" },
341
+ ],
342
+ },
343
+ });
344
+ expect(events.some((event) => event.type === "message" && event.content === "done")).toBe(
345
+ true,
346
+ );
347
+ expect(events.some((event) => event.type === "tool_start")).toBe(true);
348
+ expect(events.some((event) => event.type === "tool_end")).toBe(true);
349
+ expect(events.some((event) => event.type === "result")).toBe(true);
350
+
351
+ const rawLogs = events
352
+ .filter(
353
+ (event): event is Extract<ProviderEvent, { type: "raw_log" }> => event.type === "raw_log",
354
+ )
355
+ .map((event) => event.content);
356
+ expect(rawLogs.length).toBeGreaterThan(0);
351
357
  expect(
352
- transcript.items.some(
353
- (item) => item.kind === "text" && item.role === "assistant" && item.text === "done",
354
- ),
358
+ rawLogs.some((content) => {
359
+ const event = JSON.parse(content) as Record<string, unknown>;
360
+ const update = event.update as Record<string, unknown> | undefined;
361
+ return update?.sessionUpdate === "agent_message_chunk";
362
+ }),
355
363
  ).toBe(true);
356
364
  expect(
357
- transcript.items.some((item) => item.kind === "tool_call" && item.tool?.id === "tool-1"),
365
+ rawLogs.some((content) => {
366
+ const event = JSON.parse(content) as Record<string, unknown>;
367
+ const update = event.update as Record<string, unknown> | undefined;
368
+ return update?.sessionUpdate === "current_mode_update";
369
+ }),
358
370
  ).toBe(true);
359
371
  expect(
360
- transcript.items.find((item) => item.kind === "tool_result" && item.result?.id === "tool-1")
361
- ?.result?.isError,
372
+ rawLogs.some((content) => {
373
+ const event = JSON.parse(content) as Record<string, unknown>;
374
+ return event.type === "message" && event.content === "done";
375
+ }),
362
376
  ).toBe(true);
377
+ expect(rawLogs.every((content) => content.length <= 30_000)).toBe(true);
378
+
379
+ initDb(":memory:");
380
+ try {
381
+ const task = await createTaskExtended("ACP persistence test");
382
+ await createSessionLogs({
383
+ taskId: task.id,
384
+ sessionId: session.sessionId,
385
+ iteration: 1,
386
+ cli: "acp",
387
+ lines: rawLogs,
388
+ });
389
+ const persisted = await getSessionLogsByTaskId(task.id);
390
+ expect(persisted).toHaveLength(rawLogs.length);
391
+ expect(persisted.map((entry) => entry.content)).toEqual(rawLogs);
392
+ expect(persisted.every((entry) => entry.cli === "acp")).toBe(true);
393
+ // Match saveCostData's JSON transport: undefined counters disappear on the wire.
394
+ // The existing API deliberately coalesces them to zero; only raw logs retain absence.
395
+ const agent = await createAgent({ name: "ACP cost test", isLead: false, status: "idle" });
396
+ const server = createServer(async (req, res) => {
397
+ const handled = await handleSessionData(
398
+ req,
399
+ res,
400
+ getPathSegments(req.url ?? ""),
401
+ parseQueryParams(req.url ?? ""),
402
+ agent.id,
403
+ );
404
+ if (!handled) {
405
+ res.writeHead(404);
406
+ res.end();
407
+ }
408
+ });
409
+ try {
410
+ const port = await listenOnFreePort(server);
411
+ const endpoint = `http://127.0.0.1:${port}/api/session-costs`;
412
+ const body = JSON.stringify({ ...result.cost, agentId: agent.id, taskId: task.id });
413
+ if (!usage) {
414
+ for (const counter of [
415
+ "inputTokens",
416
+ "outputTokens",
417
+ "cacheReadTokens",
418
+ "cacheWriteTokens",
419
+ ]) {
420
+ expect(JSON.parse(body)).not.toHaveProperty(counter);
421
+ }
422
+ }
423
+ const response = await fetch(endpoint, {
424
+ method: "POST",
425
+ headers: { "Content-Type": "application/json" },
426
+ body,
427
+ });
428
+ expect(response.status).toBe(201);
429
+ const { cost } = await response.json();
430
+ expect(cost).toMatchObject({
431
+ inputTokens: usage?.inputTokens ?? 0,
432
+ outputTokens: usage?.outputTokens ?? 0,
433
+ cacheReadTokens: usage?.cachedReadTokens ?? 0,
434
+ cacheWriteTokens: usage?.cachedWriteTokens ?? 0,
435
+ costSource: "unpriced",
436
+ });
437
+ const readback = await fetch(`${endpoint}?taskId=${task.id}`);
438
+ expect(readback.status).toBe(200);
439
+ const { costs } = await readback.json();
440
+ expect(costs).toHaveLength(1);
441
+ expect(costs[0]).toMatchObject({
442
+ id: cost.id,
443
+ inputTokens: cost.inputTokens,
444
+ outputTokens: cost.outputTokens,
445
+ cacheReadTokens: cost.cacheReadTokens,
446
+ cacheWriteTokens: cost.cacheWriteTokens,
447
+ costSource: cost.costSource,
448
+ });
449
+ } finally {
450
+ await new Promise<void>((resolve) => server.close(() => resolve()));
451
+ }
452
+ const persistedJson = persisted.map((entry) => entry.content).join("\n");
453
+ const credentialHeaderNames = [
454
+ "authorization",
455
+ "proxy-authorization",
456
+ "cookie",
457
+ "set-cookie",
458
+ "www-authenticate",
459
+ "proxy-authenticate",
460
+ "x-api-key",
461
+ "api-key",
462
+ "x-auth-token",
463
+ "x-access-token",
464
+ "x-session-token",
465
+ ];
466
+ for (const headerName of credentialHeaderNames) {
467
+ expect(persistedJson.toLowerCase()).not.toContain(headerName);
468
+ }
469
+ const credentialValues = credentialHeaderNames.flatMap((headerName) => [
470
+ `opaque-array-${headerName}-credential`,
471
+ `opaque-map-${headerName}-credential`,
472
+ ]);
473
+ for (const credentialValue of credentialValues) {
474
+ expect(persistedJson).not.toContain(credentialValue);
475
+ }
476
+ expect(persistedJson).toContain("X-Debug");
477
+ expect(persistedJson).toContain("X-Debug-Map");
478
+ expect(persistedJson).not.toContain("ghp_abcdefghijklmnopqrstuvwxyz0123456789");
479
+ expect(persistedJson).toContain("[REDACTED:github_token]");
480
+ expect(persistedJson).toContain("… [truncated]");
481
+ expect(persistedJson).not.toContain("x".repeat(30_001));
482
+ const transcript = normalizeSessionLogs(persisted);
483
+ expect(transcript.items.some((item) => item.kind === "unknown")).toBe(false);
484
+ expect(
485
+ transcript.items.some(
486
+ (item) => item.kind === "text" && item.role === "assistant" && item.text === "done",
487
+ ),
488
+ ).toBe(true);
489
+ expect(
490
+ transcript.items.some((item) => item.kind === "tool_call" && item.tool?.id === "tool-1"),
491
+ ).toBe(true);
492
+ expect(
493
+ transcript.items.find(
494
+ (item) => item.kind === "tool_result" && item.result?.id === "tool-1",
495
+ )?.result?.isError,
496
+ ).toBe(true);
497
+ } finally {
498
+ closeDb();
499
+ }
363
500
  } finally {
364
- closeDb();
501
+ await new Promise<void>((resolve) => tokenStub.close(() => resolve()));
365
502
  }
366
503
  });
367
504
 
@@ -550,4 +687,119 @@ new AgentSideConnection((connection) => new FakeAgent(connection), stream);
550
687
  expect(toAcpMcpServers({ broken: { foo: "bar" } })).toEqual([]);
551
688
  expect(toAcpMcpServers(null)).toEqual([]);
552
689
  });
690
+
691
+ test("mints an ephemeral token and revokes it when the session ends", async () => {
692
+ const cwd = makeTempDir();
693
+ const agentPath = join(cwd, "fake-acp-ephem-agent.ts");
694
+ const sdkPath = join(process.cwd(), "node_modules/@agentclientprotocol/sdk/dist/acp.js");
695
+
696
+ const captureFile = join(cwd, "captured-auth.txt");
697
+
698
+ await Bun.write(
699
+ agentPath,
700
+ `
701
+ import { Readable, Writable } from "node:stream";
702
+ import { AgentSideConnection, PROTOCOL_VERSION, ndJsonStream } from "${sdkPath}";
703
+ class FakeAgent {
704
+ constructor(connection) { this.connection = connection; }
705
+ async initialize() {
706
+ return { protocolVersion: PROTOCOL_VERSION, agentCapabilities: { loadSession: false } };
707
+ }
708
+ async newSession(params) {
709
+ const swarm = params.mcpServers.find((s) => s.name === "swarm");
710
+ const auth = swarm?.headers?.find((h) => h.name === "Authorization")?.value ?? "";
711
+ // Write bearer to a file so the test can read it back without relying on
712
+ // a custom sessionUpdate type that the ACP SDK schema rejects.
713
+ await Bun.write(${JSON.stringify(captureFile)}, auth);
714
+ // newSession must return { sessionId, configOptions? }, not a prompt response.
715
+ return { sessionId: "ephem-session-1" };
716
+ }
717
+ async prompt(params) {
718
+ await this.connection.sessionUpdate({
719
+ sessionId: params.sessionId,
720
+ update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "done" }, messageId: "m1" },
721
+ });
722
+ return { stopReason: "end_turn" };
723
+ }
724
+ async cancel() {}
725
+ }
726
+ const stream = ndJsonStream(Writable.toWeb(process.stdout), Readable.toWeb(process.stdin));
727
+ new AgentSideConnection((connection) => new FakeAgent(connection), stream);
728
+ `,
729
+ );
730
+
731
+ // Minimal swarm API stub: tracks calls to the session token endpoints.
732
+ const FAKE_TOKEN = "aseph_ephemtesttoken12345678901234";
733
+ const FAKE_TOKEN_ID = "fake-ephem-token-id";
734
+ let mintCalled = false;
735
+ let revokeCalled = false;
736
+ let mintBody: Record<string, unknown> = {};
737
+
738
+ const swarmServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
739
+ if (req.method === "POST" && req.url === "/api/sessions/tokens") {
740
+ mintCalled = true;
741
+ const chunks: Buffer[] = [];
742
+ for await (const chunk of req) chunks.push(chunk as Buffer);
743
+ try {
744
+ mintBody = JSON.parse(Buffer.concat(chunks).toString()) as Record<string, unknown>;
745
+ } catch {
746
+ /* ignore */
747
+ }
748
+ res.writeHead(200, { "Content-Type": "application/json" });
749
+ res.end(JSON.stringify({ tokenId: FAKE_TOKEN_ID, plaintext: FAKE_TOKEN }));
750
+ } else if (req.method === "DELETE" && req.url === `/api/sessions/tokens/${FAKE_TOKEN_ID}`) {
751
+ revokeCalled = true;
752
+ res.writeHead(204);
753
+ res.end();
754
+ } else {
755
+ res.writeHead(404);
756
+ res.end();
757
+ }
758
+ });
759
+ await new Promise<void>((resolve) => swarmServer.listen(0, "127.0.0.1", resolve));
760
+ const swarmPort = (swarmServer.address() as import("net").AddressInfo).port;
761
+
762
+ try {
763
+ const adapter = new ACPAdapter();
764
+ const session = await adapter.createSession(
765
+ baseConfig({
766
+ cwd,
767
+ apiUrl: `http://127.0.0.1:${swarmPort}`,
768
+ apiKey: "real-operator-key",
769
+ env: {
770
+ PATH: process.env.PATH ?? "",
771
+ HOME: process.env.HOME ?? "",
772
+ ACP_TARGET_COMMAND: "bun",
773
+ ACP_TARGET_ARGS: JSON.stringify([agentPath]),
774
+ },
775
+ }),
776
+ );
777
+
778
+ const events: ProviderEvent[] = [];
779
+ session.onEvent((e) => events.push(e));
780
+ const result = await session.waitForCompletion();
781
+ // Give the revoke call (fire-and-forget) a moment to complete.
782
+ await Bun.sleep(100);
783
+
784
+ expect(result.exitCode).toBe(0);
785
+
786
+ // The adapter must have minted an ephemeral token.
787
+ expect(mintCalled).toBe(true);
788
+ expect(mintBody).toMatchObject({
789
+ agentId: "agent-1",
790
+ taskId: "task-1",
791
+ });
792
+
793
+ // The adapter must have revoked the token after the session ended.
794
+ expect(revokeCalled).toBe(true);
795
+
796
+ // Verify the bearer forwarded to the ACP target is the ephemeral token.
797
+ // The fake agent wrote the captured Authorization header value to a file
798
+ // during newSession; read it back here.
799
+ const captured = await Bun.file(captureFile).text();
800
+ expect(captured).toBe(`Bearer ${FAKE_TOKEN}`);
801
+ } finally {
802
+ await new Promise<void>((resolve) => swarmServer.close(() => resolve()));
803
+ }
804
+ });
553
805
  });
@@ -157,6 +157,7 @@ describe("Phase 3 — /api/poll budget admission gate", () => {
157
157
  if ("error" in body) throw new Error("unexpected error response");
158
158
  expect(body.trigger?.type).toBe("task_assigned");
159
159
  expect(body.trigger?.requestedBy).toEqual({
160
+ id: requester.id,
160
161
  name: "Requester One",
161
162
  email: "requester@example.com",
162
163
  role: "engineering manager",
@@ -23,7 +23,7 @@ describe("build-pi-skills", () => {
23
23
  .map((d) => d.name);
24
24
 
25
25
  test("build script runs successfully", () => {
26
- expect(buildOutput).toContain("Converted 13 skills");
26
+ expect(buildOutput).toContain("Converted 11 skills");
27
27
  });
28
28
 
29
29
  test("every command has a corresponding pi-skill", () => {
@@ -75,12 +75,10 @@ describe("build-pi-skills", () => {
75
75
  });
76
76
 
77
77
  describe("slash command syntax", () => {
78
- const skillsWithCrossRefs = [
79
- "work-on-task",
80
- "start-worker",
81
- "start-leader",
82
- "review-offered-task",
83
- ];
78
+ // `work-on-task` / `review-offered-task` are seeded skills, not commands —
79
+ // they have no pi-skill directory. The commands that link to them still
80
+ // must render the reference as `/skill:work-on-task`.
81
+ const skillsWithCrossRefs = ["start-worker", "start-leader"];
84
82
 
85
83
  for (const skill of skillsWithCrossRefs) {
86
84
  test(`${skill} uses /skill: prefix for cross-references`, () => {