@desplega.ai/agent-swarm 1.171.0 → 1.172.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 (183) hide show
  1. package/dist/{acp-adapter-1z9p3fvx.js → acp-adapter-fpecx1g1.js} +23 -5
  2. package/dist/{actions-rc2yjk8a.js → actions-pw87a1a8.js} +24 -25
  3. package/dist/{amp-adapter-ypyf5s1x.js → amp-adapter-8jem2bc0.js} +1 -1
  4. package/dist/{anthropic-messages-etkbxhhy.js → anthropic-messages-8ee2f22q.js} +55 -22
  5. package/dist/{app-04ab2s0z.js → app-wmf0m843.js} +7 -8
  6. package/dist/{assistant-vhmjd8wf.js → assistant-emp1e108.js} +26 -27
  7. package/dist/{azure-openai-responses-xap57x4b.js → azure-openai-responses-ap1gwxgr.js} +3 -3
  8. package/dist/{boot-reembed-3p0m259x.js → boot-reembed-1de4f4aj.js} +6 -7
  9. package/dist/{boot-reembed-wew3gwk8.js → boot-reembed-4ymn9atn.js} +6 -7
  10. package/dist/{boot-scrub-sweep-5ytwtq24.js → boot-scrub-sweep-adhk1s8c.js} +7 -8
  11. package/dist/{boot-seeds-s8p2xry8.js → boot-seeds-8q637aew.js} +9 -10
  12. package/dist/{claude-adapter-akrzg6jh.js → claude-adapter-t26cv8a4.js} +16 -17
  13. package/dist/{claude-managed-adapter-kqqqhp2q.js → claude-managed-adapter-xgxhqrt2.js} +2 -2
  14. package/dist/{claude-sdk-session-exq1jadx.js → claude-sdk-session-gsfsh662.js} +16 -17
  15. package/dist/{cli-n8ybnv1k.js → cli-04emfe2q.js} +1 -1
  16. package/dist/{cli-w0bwte75.js → cli-104zsxba.js} +2 -1
  17. package/dist/{cli-x6z90nq9.js → cli-19njxg5g.js} +2 -4
  18. package/dist/{cli-8mr695sb.js → cli-1hgat6pa.js} +5 -5
  19. package/dist/{cli-1qtgd1h1.js → cli-1m2xxxe2.js} +2 -2
  20. package/dist/{cli-231av4y6.js → cli-1x7da0de.js} +2 -4
  21. package/dist/{cli-x3fy2tgp.js → cli-388egyf6.js} +1 -1
  22. package/dist/{cli-j0w59p15.js → cli-4fnb6884.js} +4 -4
  23. package/dist/{cli-y1j1ggbf.js → cli-4nz8p1p5.js} +1372 -588
  24. package/dist/{cli-s88my3br.js → cli-5rv1j9ga.js} +6 -6
  25. package/dist/{cli-mn8qkd8v.js → cli-61625k3x.js} +1 -1
  26. package/dist/{cli-mzj3aqac.js → cli-6jb2rf77.js} +3 -3
  27. package/dist/{cli-ec7hrgqp.js → cli-8hzfb6xz.js} +8 -8
  28. package/dist/{cli-n805bb98.js → cli-8vs9trd4.js} +1 -1
  29. package/dist/{cli-xf713zz0.js → cli-9fxpr83v.js} +1 -1
  30. package/dist/{cli-q1vnz8wv.js → cli-9s17naqf.js} +1 -1
  31. package/dist/{cli-nkjwe4z7.js → cli-9xsmdpdt.js} +59 -54
  32. package/dist/{cli-6xxxb8qt.js → cli-aspcaef3.js} +5 -5
  33. package/dist/cli-bf7485pm.js +514 -0
  34. package/dist/{cli-s9y81s7j.js → cli-bt8k5h3s.js} +625 -150
  35. package/dist/{cli-jxsgrnmq.js → cli-c2pq2rjb.js} +2 -2
  36. package/dist/{cli-13xwcqvj.js → cli-c5km5znr.js} +2 -2
  37. package/dist/{cli-5y67yrt6.js → cli-cceskcvt.js} +112 -176
  38. package/dist/{cli-ph3145b7.js → cli-cf69s3ew.js} +4 -4
  39. package/dist/{cli-k2v5x13t.js → cli-d603qf86.js} +1 -1
  40. package/dist/{cli-r455yd58.js → cli-e0a9sk00.js} +1 -1
  41. package/dist/{cli-cgj4hc12.js → cli-eh1ytktb.js} +1872 -190
  42. package/dist/{cli-erbp6b4g.js → cli-esnpehf5.js} +7 -8
  43. package/dist/cli-ev91t6xp.js +148 -0
  44. package/dist/{cli-e2prcx5m.js → cli-fdvbjtbw.js} +235 -5
  45. package/dist/{cli-62bwwc2q.js → cli-g4f5pj26.js} +3 -3
  46. package/dist/{cli-tg8q0b5y.js → cli-g6t9texm.js} +5 -5
  47. package/dist/{cli-p2pjfq1r.js → cli-gj81e47t.js} +1 -1
  48. package/dist/{cli-srmej5tp.js → cli-gp1rtyvk.js} +2 -2
  49. package/dist/{cli-rb9m6rpe.js → cli-h6vtvt5c.js} +139 -1
  50. package/dist/cli-hd27d2md.js +1057 -0
  51. package/dist/{cli-hsk5gd15.js → cli-hwwvy36w.js} +3 -3
  52. package/dist/{cli-2rbt7at8.js → cli-hz99wfxp.js} +1 -1
  53. package/dist/cli-j1pmc07z.js +706 -0
  54. package/dist/{cli-01qndrvw.js → cli-j6kpj9az.js} +14 -8
  55. package/dist/{cli-2qbs2tvf.js → cli-k46r9hmk.js} +6 -6
  56. package/dist/{cli-wfvedq5w.js → cli-kb4ehzx5.js} +5 -5
  57. package/dist/{cli-agamgm0p.js → cli-mvzyxwg3.js} +1 -1
  58. package/dist/{cli-280gnz4v.js → cli-nv4zd0b9.js} +1 -1
  59. package/dist/{cli-rrbctenq.js → cli-nyhb1r6x.js} +3 -5
  60. package/dist/{cli-g7k7zbcz.js → cli-p7z6q2m6.js} +2 -1
  61. package/dist/{cli-yn0a417a.js → cli-phre6gz0.js} +22 -4
  62. package/dist/{cli-m4csbs67.js → cli-pj6x9qce.js} +1 -1
  63. package/dist/{cli-w0zx9ya1.js → cli-prrt9fa0.js} +4 -4
  64. package/dist/{cli-ctret352.js → cli-pstq9888.js} +33 -33
  65. package/dist/{cli-y195hv1a.js → cli-pxeeyc53.js} +1 -1
  66. package/dist/{cli-4bm25xtw.js → cli-qsztpfry.js} +1 -1
  67. package/dist/{cli-5t5my0h8.js → cli-qybkr9n3.js} +278 -1267
  68. package/dist/{cli-xezn9nkh.js → cli-r44t9wqc.js} +29 -472
  69. package/dist/{cli-x207g4w1.js → cli-r8smj8b8.js} +1 -1
  70. package/dist/{cli-bxqez1p0.js → cli-rcp5aqdw.js} +9 -39
  71. package/dist/{cli-vr5avem2.js → cli-smcrrmhy.js} +14 -18
  72. package/dist/{cli-0d2rqhvq.js → cli-tgpfnm6r.js} +15 -17
  73. package/dist/cli-vb8g5x02.js +549 -0
  74. package/dist/{cli-gt4hxb8p.js → cli-vv0qtfpx.js} +1 -2
  75. package/dist/{cli-95zs84qw.js → cli-wd0hz6ah.js} +671 -388
  76. package/dist/{cli-nkqjj42q.js → cli-x4m6feqt.js} +1 -1
  77. package/dist/{cli-x61srfpp.js → cli-xczpdp9e.js} +2 -2
  78. package/dist/cli-z1kd834t.js +36 -0
  79. package/dist/{cli-p8f7dqgt.js → cli-ztrv9y21.js} +48 -33
  80. package/dist/cli.js +24 -25
  81. package/dist/{cloudflare-workers-ai-system-one-sta45jf9.js → cloudflare-workers-ai-system-one-vnpyfr7e.js} +3 -3
  82. package/dist/{codex-adapter-9ag75whm.js → codex-adapter-m4tgdysy.js} +12 -12
  83. package/dist/{codex-hook-7ba15hjd.js → codex-hook-xch6z5wt.js} +2 -3
  84. package/dist/{codex-session-runner-9fp7y4cf.js → codex-session-runner-ymwzm6ea.js} +12 -12
  85. package/dist/{commands-d7eah0y9.js → commands-n0hshcvs.js} +6 -7
  86. package/dist/{controllers-connect-8t0k65x2.js → controllers-connect-y07cqcvy.js} +24 -25
  87. package/dist/{cursor-adapter-t4pr1m3c.js → cursor-adapter-q8pgv5qa.js} +2 -3
  88. package/dist/{db-sp39qv72.js → db-4w7tmt1g.js} +6 -7
  89. package/dist/{db-2rmd23wq.js → db-hkqvrdcb.js} +7 -8
  90. package/dist/{dispatcher-g7fsybsd.js → dispatcher-5sd69y14.js} +23 -24
  91. package/dist/{easter-egg-3d-66zxervv.js → easter-egg-3d-m8rvz40s.js} +4 -3
  92. package/dist/execute-107xtsp1.js +3745 -0
  93. package/dist/{google-generative-ai-pvhm0dq1.js → google-generative-ai-7pavrbv7.js} +3 -3
  94. package/dist/{google-vertex-tek9sqe2.js → google-vertex-3f1jk41f.js} +3 -3
  95. package/dist/{grok-adapter-1err1me7.js → grok-adapter-8m9yqzbq.js} +24 -6
  96. package/dist/{handlers-c7yggm37.js → handlers-s20xr10r.js} +26 -27
  97. package/dist/{heartbeat-dkm924fk.js → heartbeat-3k31q4g2.js} +25 -26
  98. package/dist/{hook-wgx4exw6.js → hook-gdn6a2fn.js} +13 -14
  99. package/dist/{hook-g8e7sfnf.js → hook-ygwerrdh.js} +14 -15
  100. package/dist/{http-1d23k928.js → http-jdteeg81.js} +53 -55
  101. package/dist/{index-y5vvczrs.js → index-6ksewd5y.js} +20 -18
  102. package/dist/{index-kkeftv5h.js → index-82rk11v7.js} +4 -4
  103. package/dist/{index-gm10tmye.js → index-e0snym4t.js} +23 -24
  104. package/dist/{index-vertaxs1.js → index-ffqzqgwb.js} +6 -7
  105. package/dist/{index-q7cj9bn2.js → index-tsszt7g9.js} +25 -26
  106. package/dist/{index-w097eg0x.js → index-vccv018g.js} +1 -1
  107. package/dist/{index-va4zbvzp.js → index-vm3bm9t7.js} +24 -25
  108. package/dist/{keepalive-1jc5e7ct.js → keepalive-jekc896t.js} +8 -9
  109. package/dist/{lead-yxw2f1sg.js → lead-4k9s9szx.js} +40 -41
  110. package/dist/{maintenance-h7w2m10y.js → maintenance-shazch4n.js} +6 -7
  111. package/dist/{mistral-conversations-rjxa9s1j.js → mistral-conversations-vjc1ggpd.js} +29 -15
  112. package/dist/{oauth-refresh-sweep-wh9ypd1b.js → oauth-refresh-sweep-5gb36kn8.js} +7 -8
  113. package/dist/{onboard-dfr9j762.js → onboard-ydrbnvww.js} +2 -2
  114. package/dist/{openai-codex-responses-pgvtddvv.js → openai-codex-responses-33qhkkdy.js} +7 -39
  115. package/dist/{openai-completions-44gb0zsk.js → openai-completions-s88xm3ns.js} +2 -2
  116. package/dist/{openai-decisions-vj9nkpf8.js → openai-decisions-fctnj8xd.js} +2 -2
  117. package/dist/{openai-responses-9dyzwzkr.js → openai-responses-7xd2f2e2.js} +3 -3
  118. package/dist/opencode-adapter-5zh0he20.js +55 -0
  119. package/dist/{otel-impl-and31m5s.js → otel-impl-6pf5cv86.js} +1 -1
  120. package/dist/{pi-mono-adapter-fx6k5wwh.js → pi-mono-adapter-ef9wwx9m.js} +25 -500
  121. package/dist/{pricing-refresh-23zr6j3w.js → pricing-refresh-s52nsa5q.js} +8 -9
  122. package/dist/{rbac-roles-wnx35x4x.js → rbac-roles-aq3msh4e.js} +6 -7
  123. package/dist/{rbac-roles-nc7k9d0t.js → rbac-roles-q5gb5e8v.js} +7 -8
  124. package/dist/{render-v2-81cg1y52.js → render-v2-pd1rg54q.js} +7 -8
  125. package/dist/{resume-d4b6j2n2.js → resume-gw4a2c6v.js} +14 -15
  126. package/dist/{runtime-h9ha1mq6.js → runtime-qfry88v2.js} +4 -7
  127. package/dist/{setup-g68r1w28.js → setup-jda1ksdh.js} +2 -2
  128. package/dist/{typesafe-system-one-e988vty7.js → typesafe-system-one-zzzcx4s3.js} +3 -3
  129. package/dist/{virtual-modules-rdjmcg7q.js → virtual-modules-b3ga2m65.js} +13 -12
  130. package/dist/{work-objects-0zzdnbt9.js → work-objects-tb7ycye8.js} +26 -27
  131. package/dist/{worker-p1g8qm6h.js → worker-njjccz5d.js} +40 -41
  132. package/openapi.json +1 -1
  133. package/package.json +7 -8
  134. package/src/be/scripts/extract-schema.ts +4 -0
  135. package/src/github/handlers.ts +61 -18
  136. package/src/github/types.ts +2 -1
  137. package/src/prompts/goal-reminder.ts +30 -0
  138. package/src/prompts/session-templates.ts +22 -0
  139. package/src/providers/acp-targets.ts +3 -4
  140. package/src/providers/amp-adapter.ts +1 -1
  141. package/src/providers/harness-version.ts +0 -1
  142. package/src/providers/opencode-adapter.ts +442 -723
  143. package/src/providers/opencode-swarm-plugin.ts +88 -0
  144. package/src/providers/pi-mono-extension.ts +58 -37
  145. package/src/scripts-runtime/executors/native.ts +6 -5
  146. package/src/scripts-runtime/executors/quickjs-bundle.ts +18 -11
  147. package/src/scripts-runtime/extract-args-schema.ts +22 -4
  148. package/src/scripts-runtime/import-allowlist.ts +1 -1
  149. package/src/scripts-runtime/zod-mini-bundle.ts +5 -0
  150. package/src/slack/render-v2.ts +2 -2
  151. package/src/tests/entrypoint-acp-target-binary.test.ts +19 -1
  152. package/src/tests/github-handlers-inline-comments.test.ts +131 -2
  153. package/src/tests/github-handlers-reactions.test.ts +9 -0
  154. package/src/tests/harness-version.test.ts +0 -20
  155. package/src/tests/opencode-adapter.test.ts +536 -1389
  156. package/src/tests/opencode-mcp-admission-sdk.test.ts +106 -0
  157. package/src/tests/opencode-swarm-plugin.test.ts +130 -0
  158. package/src/tests/pi-mono-extension.test.ts +59 -0
  159. package/src/tests/prompt-template-session.test.ts +4 -3
  160. package/src/tests/provider-registration.test.ts +3 -0
  161. package/src/tests/script-executor-quickjs.test.ts +17 -12
  162. package/src/tests/scripts-http.test.ts +24 -16
  163. package/src/tests/scripts-import-allowlist.test.ts +1 -1
  164. package/src/tests/scripts-runtime.test.ts +78 -40
  165. package/src/tests/seed-scripts.test.ts +13 -0
  166. package/src/tests/slack-render-v2.test.ts +4 -2
  167. package/src/tests/ui-logs-parser.test.ts +28 -0
  168. package/src/tools/script-upsert.ts +1 -1
  169. package/src/utils/memory-raters.ts +7 -3
  170. package/src/utils/openrouter-base-url.ts +0 -2
  171. package/templates/schedules/weekly-harness-upgrade-check/content.md +4 -6
  172. package/templates/skills/heartbeat-runbook/content.md +2 -0
  173. package/templates/skills/swarm-scripts/SKILL.md +1 -0
  174. package/templates/skills/swarm-scripts/content.md +1 -0
  175. package/dist/cli-5bfdm1ah.js +0 -462
  176. package/dist/cli-5c2j5yvx.js +0 -222
  177. package/dist/cli-rhy7z1rp.js +0 -532
  178. package/dist/cli-ywh2a87k.js +0 -2359
  179. package/dist/execute-h4jgevyw.js +0 -1387
  180. package/dist/opencode-adapter-f1zby94p.js +0 -34
  181. package/plugin/opencode-plugins/lib/memory-raters.ts +0 -7
  182. package/src/tests/opencode-plugin.test.ts +0 -580
  183. package/src/tests/providers/opencode-cost.test.ts +0 -74
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Agent Swarm plugin for OpenCode 2.
3
+ *
4
+ * OpenCode 2 runs in-process (`@opencode/sdk`), so this plugin is a plain
5
+ * object passed to `OpenCode.create({ plugins })`. It closes over the task's
6
+ * swarm config and reuses the pi extension's swarm-hook helpers; no env vars,
7
+ * no plugin file on disk, no vendored copies.
8
+ *
9
+ * Session-end work (identity sync, session summary, session close) runs from
10
+ * the adapter's event loop, not from here.
11
+ */
12
+
13
+ import type { Plugin } from "@opencode/plugin";
14
+ import { renderGoalReminder } from "../prompts/goal-reminder";
15
+ import {
16
+ apiHeaders,
17
+ checkShouldBlockPolling,
18
+ fetchConcurrentContext,
19
+ fetchTaskDetails,
20
+ fireAndForget,
21
+ isTaskCancelled,
22
+ type SwarmHooksConfig,
23
+ } from "./pi-mono-extension";
24
+
25
+ /** The host's registered tools (MCP tools included), keyed by effective name. */
26
+ export type OpencodeToolLister = () => Promise<ReadonlyArray<{ readonly id: string }>>;
27
+
28
+ export interface OpencodeSwarmPluginOptions {
29
+ /** Receives the tool lister once OpenCode sets the plugin up. */
30
+ onToolRegistry?: (listTools: OpencodeToolLister) => void;
31
+ }
32
+
33
+ export function createOpencodeSwarmPlugin(
34
+ config: SwarmHooksConfig,
35
+ options: OpencodeSwarmPluginOptions = {},
36
+ ): Plugin.Plugin {
37
+ return {
38
+ id: "agent-swarm",
39
+ async setup(ctx) {
40
+ options.onToolRegistry?.(() => ctx.tool.list());
41
+
42
+ // A throw blocks the tool; OpenCode hands the message back to the model.
43
+ await ctx.tool.hook("execute.before", async (event) => {
44
+ if (!config.isLead && config.taskId) {
45
+ const { cancelled, reason } = await isTaskCancelled(config);
46
+ if (cancelled) {
47
+ const cancelReason = reason || "Task cancelled by lead or creator";
48
+ throw new Error(
49
+ `🛑 TASK CANCELLED: Your current task (${config.taskId.slice(0, 8)}) has been cancelled. Reason: "${cancelReason}". ` +
50
+ "Stop working on this task immediately. Do NOT continue making tool calls. " +
51
+ "Use store-progress to acknowledge the cancellation and mark the task as failed, then wait for new tasks.",
52
+ );
53
+ }
54
+ }
55
+ if (event.tool.includes("poll-task") && (await checkShouldBlockPolling(config))) {
56
+ throw new Error(
57
+ "🛑 POLLING LIMIT REACHED: You have exceeded the maximum empty poll attempts. " +
58
+ "EXIT NOW - do not make any more tool calls.",
59
+ );
60
+ }
61
+ });
62
+
63
+ await ctx.tool.hook("execute.after", () => {
64
+ if (config.isLead || !config.taskId) return;
65
+ fireAndForget(`${config.apiUrl}/api/agents/${config.agentId}/activity`, {
66
+ method: "PUT",
67
+ headers: apiHeaders(config),
68
+ });
69
+ });
70
+
71
+ // Lead only: tell the model what other sessions are already handling.
72
+ await ctx.session.hook("context", async (event) => {
73
+ if (!config.isLead) return;
74
+ const concurrent = await fetchConcurrentContext(config);
75
+ if (concurrent) event.system.push({ type: "text", text: concurrent });
76
+ });
77
+
78
+ // PreCompact parity: keep the task goal in the checkpoint summary.
79
+ await ctx.session.hook("compaction", async (event) => {
80
+ if (!config.taskId) return;
81
+ const task = await fetchTaskDetails(config);
82
+ if (!task) return;
83
+ const reminder = await renderGoalReminder(task);
84
+ if (reminder) event.system.push({ type: "text", text: reminder });
85
+ });
86
+ },
87
+ };
88
+ }
@@ -9,6 +9,7 @@
9
9
  import type { ExtensionFactory, McpServerConfig } from "@earendil-works/pi-coding-agent";
10
10
  import { buildRatingsFromLlm, fetchRetrievalsForTask, postRatings } from "../be/memory/raters/llm";
11
11
  import { checkToolLoop, clearToolHistory } from "../hooks/tool-loop-detection";
12
+ import { renderGoalReminder } from "../prompts/goal-reminder";
12
13
  import { summarizeSession as runSummarize } from "../utils/internal-ai";
13
14
  import { getMemoryRaterNames } from "../utils/memory-raters";
14
15
  import { getOpenRouterAttributionHeaders } from "../utils/openrouter-base-url";
@@ -34,7 +35,7 @@ export interface SwarmHooksConfig {
34
35
  }
35
36
 
36
37
  /** Standard headers for swarm API requests */
37
- function apiHeaders(config: SwarmHooksConfig): Record<string, string> {
38
+ export function apiHeaders(config: SwarmHooksConfig): Record<string, string> {
38
39
  return {
39
40
  "Content-Type": "application/json",
40
41
  ...(config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}),
@@ -43,12 +44,12 @@ function apiHeaders(config: SwarmHooksConfig): Record<string, string> {
43
44
  }
44
45
 
45
46
  /** Fire-and-forget fetch — logs nothing, swallows errors */
46
- function fireAndForget(url: string, init: RequestInit): void {
47
+ export function fireAndForget(url: string, init: RequestInit): void {
47
48
  void fetch(url, init).catch(() => {});
48
49
  }
49
50
 
50
51
  /** Check if a task has been cancelled */
51
- async function isTaskCancelled(
52
+ export async function isTaskCancelled(
52
53
  config: SwarmHooksConfig,
53
54
  ): Promise<{ cancelled: boolean; reason?: string }> {
54
55
  try {
@@ -68,7 +69,7 @@ async function isTaskCancelled(
68
69
  }
69
70
 
70
71
  /** Check if agent should stop polling */
71
- async function checkShouldBlockPolling(config: SwarmHooksConfig): Promise<boolean> {
72
+ export async function checkShouldBlockPolling(config: SwarmHooksConfig): Promise<boolean> {
72
73
  try {
73
74
  const resp = await fetch(`${config.apiUrl}/me`, {
74
75
  method: "GET",
@@ -83,7 +84,7 @@ async function checkShouldBlockPolling(config: SwarmHooksConfig): Promise<boolea
83
84
  }
84
85
 
85
86
  /** Fetch task details for goal reminder */
86
- async function fetchTaskDetails(
87
+ export async function fetchTaskDetails(
87
88
  config: SwarmHooksConfig,
88
89
  ): Promise<{ id: string; task: string; progress?: string } | null> {
89
90
  try {
@@ -99,9 +100,10 @@ async function fetchTaskDetails(
99
100
  }
100
101
 
101
102
  /** Sync identity files (SOUL.md, IDENTITY.md, TOOLS.md) to server */
102
- async function syncIdentityFilesToServer(
103
+ export async function syncIdentityFilesToServer(
103
104
  config: SwarmHooksConfig,
104
105
  changeSource: "self_edit" | "session_sync" = "session_sync",
106
+ signal?: AbortSignal,
105
107
  ): Promise<void> {
106
108
  const updates: Record<string, string> = {};
107
109
  const paths: Record<string, string> = {
@@ -131,6 +133,7 @@ async function syncIdentityFilesToServer(
131
133
  method: "PUT",
132
134
  headers: apiHeaders(config),
133
135
  body: JSON.stringify({ ...updates, changeSource }),
136
+ signal,
134
137
  });
135
138
  } catch {
136
139
  /* silently fail */
@@ -174,7 +177,10 @@ async function syncSetupScriptToServer(
174
177
  }
175
178
 
176
179
  /** Auto-index a file written to memory directory */
177
- async function autoIndexMemoryFile(config: SwarmHooksConfig, editedPath: string): Promise<void> {
180
+ export async function autoIndexMemoryFile(
181
+ config: SwarmHooksConfig,
182
+ editedPath: string,
183
+ ): Promise<void> {
178
184
  try {
179
185
  const fileContent = await Bun.file(editedPath).text();
180
186
  const isShared = editedPath.startsWith("/workspace/shared/");
@@ -198,7 +204,7 @@ async function autoIndexMemoryFile(config: SwarmHooksConfig, editedPath: string)
198
204
  }
199
205
 
200
206
  /** Fetch concurrent context for lead agents */
201
- async function fetchConcurrentContext(config: SwarmHooksConfig): Promise<string | null> {
207
+ export async function fetchConcurrentContext(config: SwarmHooksConfig): Promise<string | null> {
202
208
  try {
203
209
  const resp = await fetch(`${config.apiUrl}/api/concurrent-context`, {
204
210
  method: "GET",
@@ -313,26 +319,38 @@ export async function summarizeSessionForPi(
313
319
  return;
314
320
  }
315
321
 
322
+ let transcript = "";
323
+ try {
324
+ const fullTranscript = await Bun.file(sessionFile).text();
325
+ transcript = fullTranscript.length > 20000 ? fullTranscript.slice(-20000) : fullTranscript;
326
+ } catch (err) {
327
+ console.warn(
328
+ scrubSecrets(`session_summary skipped (pi): cannot read ${sessionFile}: ${String(err)}`),
329
+ );
330
+ return;
331
+ }
332
+ await summarizeTranscript(config, "pi", transcript, deps);
333
+ }
334
+
335
+ /**
336
+ * Summarize an already-loaded session transcript and index the result. Shared
337
+ * by pi (transcript file) and opencode (transcript read from its session API).
338
+ */
339
+ export async function summarizeTranscript(
340
+ config: SwarmHooksConfig,
341
+ harness: "pi" | "opencode",
342
+ transcript: string,
343
+ deps: SummarizeSessionForPiDeps = {},
344
+ ): Promise<void> {
316
345
  const _runSummarize = deps.runSummarize ?? runSummarize;
317
346
  const _fetchRetrievals = deps.fetchRetrievalsForTask ?? fetchRetrievalsForTask;
318
347
  const _postRatings = deps.postRatings ?? postRatings;
319
348
  const _buildRatings = deps.buildRatingsFromLlm ?? buildRatingsFromLlm;
320
349
 
321
350
  try {
322
- let transcript = "";
323
- try {
324
- const fullTranscript = await Bun.file(sessionFile).text();
325
- transcript = fullTranscript.length > 20000 ? fullTranscript.slice(-20000) : fullTranscript;
326
- } catch (err) {
327
- console.warn(
328
- scrubSecrets(`session_summary skipped (pi): cannot read ${sessionFile}: ${String(err)}`),
329
- );
330
- return;
331
- }
332
-
333
351
  if (transcript.length <= 100) {
334
352
  console.warn(
335
- `session_summary skipped (pi): transcript too short (${transcript.length} chars)`,
353
+ `session_summary skipped (${harness}): transcript too short (${transcript.length} chars)`,
336
354
  );
337
355
  return;
338
356
  }
@@ -341,7 +359,7 @@ export async function summarizeSessionForPi(
341
359
  const agentId = config.agentId;
342
360
  if (!sourceTaskId || !agentId) {
343
361
  console.warn(
344
- `session_summary skipped (pi): ${!sourceTaskId ? "task id unavailable" : "agent id unavailable"}`,
362
+ `session_summary skipped (${harness}): ${!sourceTaskId ? "task id unavailable" : "agent id unavailable"}`,
345
363
  );
346
364
  return;
347
365
  }
@@ -359,7 +377,7 @@ export async function summarizeSessionForPi(
359
377
  : [];
360
378
 
361
379
  const result = await _runSummarize({
362
- harness: "pi",
380
+ harness,
363
381
  transcript,
364
382
  retrievals,
365
383
  taskContext: {
@@ -375,14 +393,14 @@ export async function summarizeSessionForPi(
375
393
  });
376
394
  // null = no auth resolved or wrapper exhausted retries (already logged inside)
377
395
  if (!result) {
378
- console.warn("session_summary skipped (pi): summarizer returned no result");
396
+ console.warn(`session_summary skipped (${harness}): summarizer returned no result`);
379
397
  return;
380
398
  }
381
399
 
382
400
  const summary = result.summary.trim();
383
401
  if (summary.length <= 20 || summary.toLowerCase().includes("no significant learnings")) {
384
402
  console.debug(
385
- `session_summary skipped (pi): summary failed quality gate (${summary.length} chars)`,
403
+ `session_summary skipped (${harness}): summary failed quality gate (${summary.length} chars)`,
386
404
  );
387
405
  return;
388
406
  }
@@ -401,7 +419,11 @@ export async function summarizeSessionForPi(
401
419
  });
402
420
  if (!indexResp.ok) {
403
421
  const text = await indexResp.text().catch(() => "");
404
- console.error("session_summary: /api/memory/index POST failed (pi):", indexResp.status, text);
422
+ console.error(
423
+ `session_summary: /api/memory/index POST failed (${harness}):`,
424
+ indexResp.status,
425
+ scrubSecrets(text),
426
+ );
405
427
  return;
406
428
  }
407
429
 
@@ -414,14 +436,21 @@ export async function summarizeSessionForPi(
414
436
  agentId,
415
437
  taskId: sourceTaskId,
416
438
  events: ratingEvents,
417
- }).catch((err) => console.error("session_summary: postRatings failed (pi):", err));
439
+ }).catch((err) =>
440
+ console.error(`session_summary: postRatings failed (${harness}):`, scrubError(err)),
441
+ );
418
442
  }
419
443
  }
420
444
  } catch (err) {
421
- console.error("session_summary failed (pi):", err);
445
+ console.error(`session_summary failed (${harness}):`, scrubError(err));
422
446
  }
423
447
  }
424
448
 
449
+ /** An error as scrubbed text: the session-summary diagnostics go to stderr. */
450
+ function scrubError(err: unknown): string {
451
+ return scrubSecrets(err instanceof Error ? (err.stack ?? err.message) : String(err));
452
+ }
453
+
425
454
  /** `customType` of the advisory messages this extension sends to the model. */
426
455
  export const SWARM_NUDGE_CUSTOM_TYPE = "swarm-nudge";
427
456
 
@@ -629,16 +658,8 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
629
658
 
630
659
  const taskDetails = await fetchTaskDetails(config);
631
660
  if (taskDetails) {
632
- const reminder = [
633
- "=== GOAL REMINDER (injected before context compaction) ===",
634
- `Task ID: ${taskDetails.id}`,
635
- `Task: ${taskDetails.task}`,
636
- ];
637
- if (taskDetails.progress) {
638
- reminder.push(`Current Progress: ${taskDetails.progress}`);
639
- }
640
- reminder.push("=== Continue working on this task after compaction ===");
641
- sendNudge(pi, reminder.join("\n"));
661
+ const reminder = await renderGoalReminder(taskDetails);
662
+ if (reminder) sendNudge(pi, reminder);
642
663
  }
643
664
  });
644
665
 
@@ -1,4 +1,4 @@
1
- import { fileURLToPath } from "node:url";
1
+ import { fileURLToPath, pathToFileURL } from "node:url";
2
2
  import {
3
3
  detachedProcessGroup,
4
4
  registerProcessGroup,
@@ -112,6 +112,7 @@ async function writeBareImportShims(tmpdir: string): Promise<void> {
112
112
  ["stdlib", `${runtimeDir}/stdlib.bundle.js`],
113
113
  ["swarm-sdk", `${runtimeDir}/swarm-sdk.bundle.js`],
114
114
  ["zod", `${runtimeDir}/zod.bundle.js`],
115
+ ["zod/mini", `${runtimeDir}/zod-mini.bundle.js`],
115
116
  ];
116
117
  for (const [name, bundlePath] of shims) {
117
118
  const dir = `${tmpdir}/node_modules/${name}`;
@@ -119,16 +120,16 @@ async function writeBareImportShims(tmpdir: string): Promise<void> {
119
120
  await Bun.write(`${dir}/package.json`, JSON.stringify({ type: "module", main: "index.js" }));
120
121
  await Bun.write(
121
122
  `${dir}/index.js`,
122
- `export * from ${JSON.stringify(`file://${bundlePath}`)};\n`,
123
+ `export * from ${JSON.stringify(pathToFileURL(bundlePath).href)};\n`,
123
124
  );
124
125
  }
125
126
  return;
126
127
  }
127
128
  await writeBareImportShim(tmpdir, "stdlib", new URL("../stdlib/index.ts", import.meta.url));
128
129
  await writeBareImportShim(tmpdir, "swarm-sdk", new URL("../swarm-sdk.ts", import.meta.url));
129
- // Allow `import { z } from "zod"` in user scripts (for argsSchema definitions).
130
- const zodEntry = Bun.resolveSync("zod", import.meta.dir);
131
- await writeBareImportShim(tmpdir, "zod", new URL(`file://${zodEntry}`));
130
+ for (const name of ["zod", "zod/mini"]) {
131
+ await writeBareImportShim(tmpdir, name, pathToFileURL(Bun.resolveSync(name, import.meta.dir)));
132
+ }
132
133
  }
133
134
 
134
135
  function harnessCommand(
@@ -37,33 +37,38 @@ export type QuickJSBundle =
37
37
 
38
38
  type BuildConfigWithFiles = Parameters<typeof Bun.build>[0] & { files: Record<string, string> };
39
39
 
40
- let zodEntryCache: string | null | undefined;
40
+ const zodEntryCache = new Map<string, string | null>();
41
41
 
42
42
  /**
43
43
  * Prefer zod's real ESM sources, which tree-shake well. The prebuilt
44
44
  * `zod.bundle.js` of the API image barely tree-shakes (~340 KB against ~87 KB
45
45
  * for z.object + z.string().email()), so it is only the last fallback.
46
46
  */
47
- export async function resolveZodEntry(): Promise<string | null> {
48
- if (zodEntryCache !== undefined) return zodEntryCache;
47
+ export async function resolveZodEntry(
48
+ moduleName: "zod" | "zod/mini" = "zod",
49
+ ): Promise<string | null> {
49
50
  const runtimeDir = process.env.SCRIPT_RUNTIME_DIR;
51
+ const cacheKey = `${runtimeDir ?? ""}:${moduleName}`;
52
+ if (zodEntryCache.has(cacheKey)) return zodEntryCache.get(cacheKey)!;
53
+ const esmPath = moduleName === "zod" ? "index.js" : "mini/index.js";
54
+ const bundleName = moduleName === "zod" ? "zod" : "zod-mini";
50
55
  const candidates: Array<() => string | undefined> = [
51
- () => (runtimeDir ? `${runtimeDir}/zod-esm/index.js` : undefined),
52
- () => Bun.resolveSync("zod", import.meta.dir),
53
- () => (runtimeDir ? `${runtimeDir}/zod.bundle.js` : undefined),
56
+ () => (runtimeDir ? `${runtimeDir}/zod-esm/${esmPath}` : undefined),
57
+ () => Bun.resolveSync(moduleName, import.meta.dir),
58
+ () => (runtimeDir ? `${runtimeDir}/${bundleName}.bundle.js` : undefined),
54
59
  ];
55
60
  for (const candidate of candidates) {
56
61
  try {
57
62
  const path = candidate();
58
63
  if (path && (await Bun.file(path).exists())) {
59
- zodEntryCache = path;
64
+ zodEntryCache.set(cacheKey, path);
60
65
  return path;
61
66
  }
62
67
  } catch {
63
68
  // Try the next candidate. Bun.resolveSync throws inside the compiled binary.
64
69
  }
65
70
  }
66
- zodEntryCache = null;
71
+ zodEntryCache.set(cacheKey, null);
67
72
  return null;
68
73
  }
69
74
 
@@ -89,7 +94,6 @@ export async function bundleForQuickJS(source: string): Promise<QuickJSBundle> {
89
94
  return cached;
90
95
  }
91
96
 
92
- const zodEntry = await resolveZodEntry();
93
97
  let result: Awaited<ReturnType<typeof Bun.build>>;
94
98
  try {
95
99
  result = await Bun.build({
@@ -117,9 +121,12 @@ export async function bundleForQuickJS(source: string): Promise<QuickJSBundle> {
117
121
  contents: args.path === "stdlib" ? STDLIB_MODULE : SWARM_SDK_MODULE,
118
122
  loader: "js",
119
123
  }));
120
- build.onResolve({ filter: /^zod$/ }, () => {
124
+ build.onResolve({ filter: /^zod(?:\/mini)?$/ }, async (args) => {
125
+ const zodEntry = await resolveZodEntry(args.path as "zod" | "zod/mini");
121
126
  if (!zodEntry) {
122
- throw new Error("zod is not available to the quickjs executor in this deployment");
127
+ throw new Error(
128
+ `${args.path} is not available to the quickjs executor in this deployment`,
129
+ );
123
130
  }
124
131
  return { path: zodEntry };
125
132
  });
@@ -7,14 +7,32 @@
7
7
  * SWARM_SCHEMA_RESULT_FILE path where JSON Schema (or "null") is written
8
8
  * SWARM_SCHEMA_TMPDIR tmpdir used for shims + the user module
9
9
  */
10
+ import { pathToFileURL } from "node:url";
10
11
  import { toJSONSchema } from "zod";
11
12
 
12
13
  async function createShims(tmpdir: string): Promise<void> {
13
- const zodEntry = Bun.resolveSync("zod", import.meta.dir);
14
+ const runtimeDir = process.env.SCRIPT_RUNTIME_DIR;
14
15
  const shims: [string, URL][] = [
15
- ["stdlib", new URL("./stdlib/index.ts", import.meta.url)],
16
- ["swarm-sdk", new URL("./swarm-sdk.ts", import.meta.url)],
17
- ["zod", new URL(`file://${zodEntry}`)],
16
+ [
17
+ "stdlib",
18
+ runtimeDir
19
+ ? pathToFileURL(`${runtimeDir}/stdlib.bundle.js`)
20
+ : new URL("./stdlib/index.ts", import.meta.url),
21
+ ],
22
+ [
23
+ "swarm-sdk",
24
+ runtimeDir
25
+ ? pathToFileURL(`${runtimeDir}/swarm-sdk.bundle.js`)
26
+ : new URL("./swarm-sdk.ts", import.meta.url),
27
+ ],
28
+ ...["zod", "zod/mini"].map((name): [string, URL] => [
29
+ name,
30
+ pathToFileURL(
31
+ runtimeDir
32
+ ? `${runtimeDir}/${name === "zod" ? "zod" : "zod-mini"}.bundle.js`
33
+ : Bun.resolveSync(name, import.meta.dir),
34
+ ),
35
+ ]),
18
36
  ];
19
37
  for (const [name, url] of shims) {
20
38
  const dir = `${tmpdir}/node_modules/${name}`;
@@ -1,6 +1,6 @@
1
1
  import ts from "typescript";
2
2
 
3
- const SCRIPT_ALLOWED_BARE_IMPORTS = ["swarm-sdk", "stdlib", "zod"] as const;
3
+ const SCRIPT_ALLOWED_BARE_IMPORTS = ["swarm-sdk", "stdlib", "zod", "zod/mini"] as const;
4
4
  const FORBIDDEN_HINTS = new Set(["node:", "bun:", "fs", "child_process", "crypto", "bun:sqlite"]);
5
5
 
6
6
  export type ImportAllowlistOptions = {
@@ -0,0 +1,5 @@
1
+ // Keep Mini's namespace export live in Bun's standalone bundle.
2
+ import * as mini from "zod/mini";
3
+
4
+ export * from "zod/mini";
5
+ export const z = mini.z;
@@ -1016,7 +1016,7 @@ async function conclusionResultsLines(closure: AgentTask[]): Promise<string[]> {
1016
1016
  const agentName = await agentDisplayNameFor(member);
1017
1017
  const card = await getSlackOutcomeMessage(member.id);
1018
1018
  if (card?.permalink) {
1019
- lines.push(`↳ ${glyph} ${agentName} — ${card.permalink}`);
1019
+ lines.push(`↳ ${glyph} ${agentName} — [result](${card.permalink})`);
1020
1020
  continue;
1021
1021
  }
1022
1022
  const raw = outcomeText(
@@ -1452,7 +1452,7 @@ async function resolvedDeferralContent(
1452
1452
  return { text: presentation, blocks: [{ type: "markdown", text: presentation }, ...captions] };
1453
1453
  }
1454
1454
  const card = await getSlackOutcomeMessage(wake.id);
1455
- const pointer = card?.permalink ? ` — ${card.permalink}` : ".";
1455
+ const pointer = card?.permalink ? ` — <${card.permalink}|next update>` : ".";
1456
1456
  return { text: `↪️ Resumed by ${await agentDisplayNameFor(wake)} and deferred again${pointer}` };
1457
1457
  }
1458
1458
 
@@ -393,7 +393,6 @@ describe("docker-entrypoint.sh: provider binary chain contract", () => {
393
393
  test.each([
394
394
  ["claude", "Claude CLI:"],
395
395
  ["codex", "Codex CLI:"],
396
- ["opencode", "opencode CLI:"],
397
396
  ["dsh", "dsh CLI:"],
398
397
  ["claude-managed", "no local CLI required"],
399
398
  ["devin", "cloud API"],
@@ -408,6 +407,25 @@ describe("docker-entrypoint.sh: provider binary chain contract", () => {
408
407
  CHILD_PROCESS_TEST_BUDGET_MS,
409
408
  );
410
409
 
410
+ test(
411
+ "checks the opencode branch for the in-process SDK, not a CLI",
412
+ async () => {
413
+ const sdkPath = join(getStubDir(), "opencode-sdk-index.js");
414
+ writeFileSync(sdkPath, "");
415
+ const found = await runChain({ HARNESS_PROVIDER: "opencode", OPENCODE_SDK_PATH: sdkPath });
416
+ expect(found.exitCode).toBe(0);
417
+ expect(found.stdout).toContain(`OpenCode SDK: ${sdkPath}`);
418
+
419
+ const missing = await runChain({
420
+ HARNESS_PROVIDER: "opencode",
421
+ OPENCODE_SDK_PATH: join(getStubDir(), "no-such-sdk.js"),
422
+ });
423
+ expect(missing.exitCode).not.toBe(0);
424
+ expect(missing.stdout).toContain("FATAL: @opencode/sdk not found");
425
+ },
426
+ CHILD_PROCESS_TEST_BUDGET_MS,
427
+ );
428
+
411
429
  test(
412
430
  "leaves the pi provider unchecked",
413
431
  async () => {