@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
package/src/types.ts CHANGED
@@ -1145,8 +1145,8 @@ export type AgentLatestModel = z.infer<typeof AgentLatestModelSchema>;
1145
1145
 
1146
1146
  /**
1147
1147
  * Worker-reported Bedrock enumeration block. Only present when the pi harness
1148
- * is in Bedrock SDK mode (`BEDROCK_AUTH_MODE=sdk` or
1149
- * `MODEL_OVERRIDE=amazon-bedrock/*`). Rides inside `cred_status` JSON (no new
1148
+ * is in a Bedrock mode (`BEDROCK_AUTH_MODE=sdk`, `BEDROCK_AUTH_MODE=bearer`,
1149
+ * or `MODEL_OVERRIDE=amazon-bedrock/*`). Rides inside `cred_status` JSON (no new
1150
1150
  * DB column). `models` is the intersection of the models invocable by this
1151
1151
  * account/region (on-demand/ACTIVE foundation models ∪ inference profiles) with
1152
1152
  * the set the pi-ai Converse harness can actually drive — Converse-incompatible
@@ -1287,6 +1287,20 @@ export type VersionMeta = {
1287
1287
  changeReason?: string | null;
1288
1288
  };
1289
1289
 
1290
+ /**
1291
+ * Compare-and-set token for a profile update, per field: the content hash the
1292
+ * writer based its edit on (e.g. what a session materialized from the DB). A
1293
+ * field whose current DB hash differs is dropped: the DB moved since the writer
1294
+ * read it, so its copy is stale, not an edit.
1295
+ */
1296
+ export type ProfileExpectedHashes = Partial<Record<VersionableField, string>>;
1297
+
1298
+ export type ProfileSyncConflict = {
1299
+ field: VersionableField;
1300
+ expectedHash: string;
1301
+ currentHash: string;
1302
+ };
1303
+
1290
1304
  // Channel Types
1291
1305
  export const ChannelTypeSchema = z.enum(["public", "dm"]);
1292
1306
 
@@ -1534,6 +1548,7 @@ export const EventNameSchema = z.enum([
1534
1548
  "system.error",
1535
1549
  "system.profile_sync_rejected",
1536
1550
  "system.profile_sync_reconciled",
1551
+ "system.profile_sync_conflict",
1537
1552
  // Script catalog events
1538
1553
  "script.global_upsert",
1539
1554
  // Schedule events
@@ -1599,6 +1614,9 @@ export const ScheduledTaskSchema = z
1599
1614
  lastRunAt: z.iso.datetime().optional(),
1600
1615
  nextRunAt: z.iso.datetime().optional(),
1601
1616
  createdByAgentId: z.string().optional(),
1617
+ // Set only by `defer-task`: the task this schedule wakes up to continue.
1618
+ // Passed through as the created task's `parentTaskId`.
1619
+ parentTaskId: z.string().optional(),
1602
1620
  timezone: z.string().default("UTC"),
1603
1621
  consecutiveErrors: z.number().int().min(0).default(0),
1604
1622
  lastErrorAt: z.iso.datetime().optional(),
@@ -1692,6 +1710,21 @@ export const RepoGuidelinesSchema = z
1692
1710
 
1693
1711
  export type RepoGuidelines = z.infer<typeof RepoGuidelinesSchema>;
1694
1712
 
1713
+ /** Upper bound per guideline list. The repository prompt section renders every
1714
+ * entry outside the bootstrap budget, so the cap lives at the write boundary. */
1715
+ export const REPO_GUIDELINES_MAX_ENTRIES = 50;
1716
+
1717
+ /** Write-side guidelines schema: same shape, bounded lists. Reads keep the
1718
+ * unbounded `RepoGuidelinesSchema` so an older row never fails a response. */
1719
+ export const RepoGuidelinesInputSchema = z
1720
+ .object({
1721
+ prChecks: z.array(z.string()).max(REPO_GUIDELINES_MAX_ENTRIES),
1722
+ mergeChecks: z.array(z.string()).max(REPO_GUIDELINES_MAX_ENTRIES),
1723
+ allowMerge: z.boolean().optional().default(false),
1724
+ review: z.array(z.string()).max(REPO_GUIDELINES_MAX_ENTRIES),
1725
+ })
1726
+ .openapi("RepoGuidelinesInput");
1727
+
1695
1728
  export const RepoHooksSchema = z
1696
1729
  .object({
1697
1730
  enabled: z.boolean().default(false),
@@ -2212,10 +2245,10 @@ export type WorkflowVersion = z.infer<typeof WorkflowVersionSchema>;
2212
2245
  // Pages — DB-backed lightweight artifacts (HTML or JSON spec) stored in
2213
2246
  // SQLite and served at /p/:id. See plan: thoughts/taras/plans/2026-05-12-db-backed-pages/.
2214
2247
  // PageContentTypeSchema + PageAuthModeSchema MUST stay in sync with the SQL
2215
- // CHECK constraints in src/be/migrations/059_pages.sql.
2248
+ // CHECK constraints in src/be/migrations/148_pages_svg.sql.
2216
2249
  // ---------------------------------------------------------------------------
2217
2250
 
2218
- export const PageContentTypeSchema = z.enum(["text/html", "application/json"]);
2251
+ export const PageContentTypeSchema = z.enum(["text/html", "application/json", "image/svg+xml"]);
2219
2252
  export type PageContentType = z.infer<typeof PageContentTypeSchema>;
2220
2253
 
2221
2254
  export const PageAuthModeSchema = z.enum(["public", "authed", "password"]);
@@ -0,0 +1,56 @@
1
+ /**
2
+ * HTTP helpers used by the ACP adapter to mint and revoke ephemeral session
3
+ * tokens via the swarm API. This keeps the provider layer free of direct
4
+ * DB imports while still allowing the adapter to exchange the full operator
5
+ * key for a short-lived aseph_ bearer before handing credentials to the
6
+ * ACP target process.
7
+ */
8
+
9
+ /** Wall-clock TTL for a session token: 24 hours. Long enough for any realistic
10
+ * ACP session; the token is actively revoked when the session finishes anyway. */
11
+ export const ACP_SESSION_TOKEN_TTL_MS = 24 * 60 * 60 * 1000;
12
+
13
+ /**
14
+ * Ask the swarm API to mint a short-lived `aseph_` bearer for the given
15
+ * agent + task. Throws if the request fails so callers fail closed rather
16
+ * than falling back to the full operator key.
17
+ */
18
+ export async function mintAcpSessionToken(
19
+ apiUrl: string,
20
+ apiKey: string,
21
+ agentId: string,
22
+ taskId: string,
23
+ ): Promise<{ tokenId: string; plaintext: string }> {
24
+ const res = await fetch(`${apiUrl.replace(/\/+$/, "")}/api/sessions/tokens`, {
25
+ method: "POST",
26
+ headers: {
27
+ Authorization: `Bearer ${apiKey}`,
28
+ "Content-Type": "application/json",
29
+ },
30
+ body: JSON.stringify({ agentId, taskId, ttlMs: ACP_SESSION_TOKEN_TTL_MS }),
31
+ });
32
+ if (!res.ok) {
33
+ throw new Error(`\x1b[33m[acp]\x1b[0m Session token mint failed (${res.status})`);
34
+ }
35
+ const data = (await res.json()) as { tokenId: string; plaintext: string };
36
+ return { tokenId: data.tokenId, plaintext: data.plaintext };
37
+ }
38
+
39
+ /**
40
+ * Ask the swarm API to revoke a previously-minted `aseph_` token. Best-effort:
41
+ * errors are logged but never propagated to the caller.
42
+ */
43
+ export async function revokeAcpSessionToken(
44
+ apiUrl: string,
45
+ apiKey: string,
46
+ tokenId: string,
47
+ ): Promise<void> {
48
+ try {
49
+ await fetch(`${apiUrl.replace(/\/+$/, "")}/api/sessions/tokens/${tokenId}`, {
50
+ method: "DELETE",
51
+ headers: { Authorization: `Bearer ${apiKey}` },
52
+ });
53
+ } catch {
54
+ // best-effort — token will expire on its own
55
+ }
56
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Advisory cross-process lock held by the kernel: flock(2) on a lock file, via
3
+ * `bun:ffi` (Bun and Node expose no flock).
4
+ *
5
+ * Makes a multi-file state transition exclusive across processes of the same
6
+ * user. First use: the Claude hook rewrites `~/.claude/CLAUDE.md` together with
7
+ * its lineage record, and the hook and the runner read that pair — a reader
8
+ * landing between the two writes can misjudge the file (see
9
+ * `src/commands/claude-md-session.ts`).
10
+ *
11
+ * Why the kernel and not a lock file with a staleness threshold: a threshold
12
+ * cannot tell a slow holder (a stalled filesystem, a suspended process) from a
13
+ * dead one, so it eventually lets a second process into a transition that is
14
+ * still running. flock is released only when the holder closes its descriptor or
15
+ * dies, so it can never be taken from a live holder and needs no breaking.
16
+ *
17
+ * The lock file is never deleted: unlinking a locked path would let a newcomer
18
+ * create and lock a different inode while the old one is still held.
19
+ */
20
+
21
+ import { dlopen, FFIType } from "bun:ffi";
22
+ // A file handle whose descriptor stays open while the lock is held has no Bun
23
+ // equivalent; neither does directory creation.
24
+ import { mkdir, open } from "node:fs/promises";
25
+ import { dirname } from "node:path";
26
+
27
+ const LOCK_EX = 2;
28
+ const LOCK_NB = 4;
29
+
30
+ export type Flock = (fd: number, operation: number) => number;
31
+
32
+ const LIBC_CANDIDATES =
33
+ process.platform === "darwin"
34
+ ? ["libc.dylib", "/usr/lib/libSystem.B.dylib"]
35
+ : ["libc.so.6", "libc.so", "libc.musl-x86_64.so.1", "libc.musl-aarch64.so.1"];
36
+
37
+ let flockImpl: Flock | null | undefined;
38
+
39
+ function loadFlock(): Flock | null {
40
+ if (flockImpl !== undefined) return flockImpl;
41
+ flockImpl = null;
42
+ for (const name of LIBC_CANDIDATES) {
43
+ try {
44
+ const { symbols } = dlopen(name, {
45
+ flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 },
46
+ });
47
+ flockImpl = (fd, operation) => symbols.flock(fd, operation);
48
+ break;
49
+ } catch {
50
+ // try the next libc name
51
+ }
52
+ }
53
+ return flockImpl;
54
+ }
55
+
56
+ const DEFAULTS = { waitMs: 15_000, pollMs: 25 };
57
+
58
+ export interface FileLockOptions {
59
+ /** How long to wait for a held lock before giving up (default 15 s). */
60
+ waitMs?: number;
61
+ /** Poll interval while waiting (default 25 ms). */
62
+ pollMs?: number;
63
+ }
64
+
65
+ export type FileLockResult<T> =
66
+ | { acquired: true; value: T }
67
+ /**
68
+ * `busy`: another live process holds it (or flock keeps refusing).
69
+ * `error`: the lock file could not be created or opened.
70
+ * `unsupported`: no flock on this platform.
71
+ */
72
+ | { acquired: false; reason: "busy" | "error" | "unsupported"; error?: unknown };
73
+
74
+ /** Test seam: replace the flock binding (`null` = unsupported platform, `undefined` = real). */
75
+ export function setFlockForTests(impl: Flock | null | undefined): void {
76
+ flockImpl = impl;
77
+ }
78
+
79
+ /**
80
+ * Run `fn` while holding the lock. Never runs `fn` without it: callers decide
81
+ * what a `busy`, `error` or `unsupported` result means for them. Errors thrown
82
+ * by `fn` propagate (after the lock is released).
83
+ */
84
+ export async function withFileLock<T>(
85
+ lockPath: string,
86
+ fn: () => Promise<T>,
87
+ options: FileLockOptions = {},
88
+ ): Promise<FileLockResult<T>> {
89
+ const flock = loadFlock();
90
+ if (!flock) return { acquired: false, reason: "unsupported" };
91
+ const waitMs = options.waitMs ?? DEFAULTS.waitMs;
92
+ const pollMs = options.pollMs ?? DEFAULTS.pollMs;
93
+
94
+ let handle: Awaited<ReturnType<typeof open>>;
95
+ try {
96
+ await mkdir(dirname(lockPath), { recursive: true });
97
+ handle = await open(lockPath, "a");
98
+ } catch (error) {
99
+ return { acquired: false, reason: "error", error };
100
+ }
101
+ try {
102
+ const deadline = Date.now() + waitMs;
103
+ while (flock(handle.fd, LOCK_EX | LOCK_NB) !== 0) {
104
+ if (Date.now() >= deadline) return { acquired: false, reason: "busy" };
105
+ await Bun.sleep(pollMs);
106
+ }
107
+ return { acquired: true, value: await fn() };
108
+ } finally {
109
+ await handle.close(); // closing the descriptor releases the lock
110
+ }
111
+ }
@@ -0,0 +1,78 @@
1
+ import { scrubSecrets } from "./secret-scrubber";
2
+
3
+ const EXPIRY = /delete\s+(on\s+)?\d{2}-\d{2}/i;
4
+
5
+ function trackedLines(markdown: string): string[] {
6
+ const marker = "## Tracked Items";
7
+ const start = markdown.lastIndexOf(marker);
8
+ if (start < 0) return [];
9
+ const lineEnd = markdown.indexOf("\n", start);
10
+ if (lineEnd < 0) return [];
11
+ // Stop at the next peer/parent section; subheadings may group tracked items.
12
+ const section = markdown.slice(lineEnd + 1).split(/^#{1,2}\s/m)[0] ?? "";
13
+ return section
14
+ .split(/\r?\n/)
15
+ .map((line) => line.trim().replace(/^(?:[-*+]\s+|\d+[.)]\s+)(?:\[[ xX]\]\s*)?/, ""))
16
+ .filter((line) => line && !line.startsWith("#") && !/^[-*_]{3,}$/.test(line));
17
+ }
18
+
19
+ function title(line: string): string | undefined {
20
+ return /^\*\*([^*]+)\*\*/.exec(line)?.[1];
21
+ }
22
+
23
+ // Deleting text anywhere in a legacy line is cleanup, not a new item.
24
+ function isShortening(old: string, next: string): boolean {
25
+ let cursor = 0;
26
+ for (const character of next) {
27
+ const index = old.indexOf(character, cursor);
28
+ if (index < 0) return false;
29
+ cursor = index + character.length;
30
+ }
31
+ return true;
32
+ }
33
+
34
+ export class HeartbeatExpiryError extends Error {
35
+ constructor(line: string) {
36
+ super(
37
+ scrubSecrets(
38
+ `Update rejected for heartbeatMd: new tracked item requires DELETE <MM-DD> (or DELETE on <MM-DD>): ${line}`,
39
+ ),
40
+ );
41
+ this.name = "HeartbeatExpiryError";
42
+ }
43
+ }
44
+
45
+ /** Grandfather existing lines, but never let one old item cover two new ones. */
46
+ export function validateHeartbeatExpiry(currentValue: string, nextValue: string): void {
47
+ const previous = trackedLines(currentValue);
48
+ const incoming = trackedLines(nextValue);
49
+ // Consume exact matches first so an unchanged item cannot sponsor a new item.
50
+ const changed = incoming.filter((line) => {
51
+ const index = previous.indexOf(line);
52
+ if (index < 0) return true;
53
+ previous.splice(index, 1);
54
+ return false;
55
+ });
56
+ // Dated/pinned replacements also consume their old item before undated edits.
57
+ // Otherwise dating one old item could accidentally authorize an undated copy.
58
+ for (const line of changed) {
59
+ if (!line.startsWith("📌") && !EXPIRY.test(line)) continue;
60
+ const itemTitle = title(line.replace(/^📌\s*/, ""));
61
+ const index = previous.findIndex(
62
+ (old) => line.includes(old) || (itemTitle !== undefined && itemTitle === title(old)),
63
+ );
64
+ if (index >= 0) previous.splice(index, 1);
65
+ }
66
+ for (const line of changed) {
67
+ if (line.startsWith("📌") || EXPIRY.test(line)) continue;
68
+ const itemTitle = title(line);
69
+ const index = previous.findIndex(
70
+ (old) =>
71
+ !old.startsWith("📌") &&
72
+ !EXPIRY.test(old) &&
73
+ (isShortening(old, line) || (itemTitle !== undefined && itemTitle === title(old))),
74
+ );
75
+ if (index < 0) throw new HeartbeatExpiryError(line);
76
+ previous.splice(index, 1);
77
+ }
78
+ }
@@ -5,6 +5,8 @@ import type { User } from "../types";
5
5
  export type HttpRequestAuth = (
6
6
  | { kind: "operator"; fingerprint: string }
7
7
  | { kind: "user"; userId: string; user: User }
8
+ /** Ephemeral session token minted by the ACP adapter per session (aseph_ prefix). */
9
+ | { kind: "agent"; agentId: string; taskId: string }
8
10
  ) & {
9
11
  /** Verified page context for agent-scoped operations. This does not identify the viewer. */
10
12
  page?: { id: string; executionAgentId: string };
@@ -1,6 +1,28 @@
1
1
  import type { TaskAttachment } from "../types";
2
2
  import { buildAgentFsLiveUrl, getAppUrl } from "./constants";
3
3
 
4
+ function shellQuote(value: string): string {
5
+ return `'${value.replace(/'/g, `'\\''`)}'`;
6
+ }
7
+
8
+ /**
9
+ * A ready-to-run command that downloads a task attachment's bytes from inside a
10
+ * worker container through the provider-agnostic raw route — MCP_BASE_URL, the
11
+ * API key and AGENT_ID are already in every worker's env. Each attachment gets
12
+ * its own directory, so two attachments with the same name (every pasted
13
+ * screenshot is "image.png") never overwrite each other; the path is quoted
14
+ * because attachment names come from users.
15
+ */
16
+ export function taskAttachmentFetchCommand(
17
+ taskId: string,
18
+ attachmentId: string,
19
+ name: string,
20
+ ): string {
21
+ const url = `$MCP_BASE_URL/api/fs/tasks/${taskId}/files/${attachmentId}/raw`;
22
+ const outPath = shellQuote(`/tmp/attachments/${attachmentId}/${name.replace(/\//g, "_")}`);
23
+ return `curl -s -H "Authorization: Bearer \${AGENT_SWARM_API_KEY:-$API_KEY}" -H "X-Agent-ID: $AGENT_ID" "${url}" --create-dirs -o ${outPath}`;
24
+ }
25
+
4
26
  export function taskAttachmentDisplayUrl(attachment: TaskAttachment): string {
5
27
  if (attachment.kind === "url") return attachment.url ?? "";
6
28
  if (attachment.kind === "page") {
@@ -38,11 +38,14 @@ The Repository Guidelines carry `allowMerge` and `mergeChecks`.
38
38
 
39
39
  ## Review a PR
40
40
 
41
- 1. CI status first. Failing CI is a REQUEST_CHANGES. Name the failing checks in the review.
42
- 2. Tests second. A code change without new or updated tests is a REQUEST_CHANGES. Name the tests you expect. Exceptions: documentation-only, configuration-only, and dependency-bump PRs.
43
- 3. Apply the "Review Guidance" entries from the Repository Guidelines.
44
- 4. Read the diff for security (injection, secrets in code), logic (null handling, off-by-one, edge cases), performance (N+1, leaks), and code shape (naming, duplication, error handling). Run the test suite and the type check locally when you can.
45
- 5. Post the review with the verdict first. One finding per comment, with file and line, and what to change.
41
+ 1. CI status first, including the fork checks below before treating CI as evidence. Failing CI is a REQUEST_CHANGES. Name the failing checks in the review.
42
+ 2. Detect a GitHub fork PR with `gh pr view <number> --json isCrossRepository --jq .isCrossRepository`.
43
+ 3. For a GitHub fork PR, inspect held runs with `SHA=$(gh pr view <number> --json headRefOid --jq .headRefOid); gh api --paginate "repos/<owner>/<repo>/actions/runs?head_sha=$SHA" --jq '.workflow_runs[] | "\(.conclusion // .status)|\(.name)"'`; `gh pr checks` and `statusCheckRollup` can omit them.
44
+ 4. Treat any `action_required` run or no substantive run executed on that SHA as a CI blocker. Keep run handling read-only: REQUEST_CHANGES naming the held or unexecuted run, and report the blocker so an explicitly authorized maintainer or the application-controlled GitHub integration can approve the run after validating the workflow and its trust boundary. Do not APPROVE on omitted runs.
45
+ 5. Tests second. A code change without new or updated tests is a REQUEST_CHANGES. Name the tests you expect. Exceptions: documentation-only, configuration-only, and dependency-bump PRs.
46
+ 6. Apply the "Review Guidance" entries from the Repository Guidelines.
47
+ 7. Read the diff for security (injection, secrets in code), logic (null handling, off-by-one, edge cases), performance (N+1, leaks), and code shape (naming, duplication, error handling). Run the test suite and the type check locally when you can.
48
+ 8. Post the review with the verdict first. One finding per comment, with file and line, and what to change.
46
49
 
47
50
  ## GitHub review-reply provenance
48
51
 
@@ -12,11 +12,12 @@ discovery.
12
12
  curl "$MCP_BASE_URL/api/fs/tasks/$AGENT_SWARM_TASK_ID/files/<attachmentId>/raw" \
13
13
  -H "X-Agent-ID: $AGENT_ID" \
14
14
  -H "Authorization: Bearer ${AGENT_SWARM_API_KEY:-$API_KEY}" \
15
- -o /tmp/<name>
15
+ --create-dirs -o "/tmp/attachments/<attachmentId>/<name>"
16
16
  ```
17
17
 
18
18
  - `$MCP_BASE_URL`, `$AGENT_ID`, `$AGENT_SWARM_TASK_ID` (or `$AGENT_SWARM_AGENT_ID`/`TASK_FILE`-derived taskId), and `$AGENT_SWARM_API_KEY`/`$API_KEY` are already present in every worker container's env — no new plumbing needed.
19
19
  - `<attachmentId>` and `<name>`/`mimeType` come from the task's attachment list (see below).
20
+ - One directory per attachment: two attachments can share a name (every pasted screenshot is `image.png`), and `/tmp/<name>` would let one overwrite the other.
20
21
  - The route resolves the active file-storage provider (`local-fs` in dev, `agent-fs` in prod) server-side and streams the raw bytes back with the correct `Content-Type` — you get one call no matter which provider is behind it.
21
22
 
22
23
  ## Finding the attachment ID
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: review-offered-task
3
- description: Review a task that has been offered to you and decide whether to accept or reject it
3
+ description: "Turn prompt for an offered swarm task: read the offer, accept with task-action and start work-on-task, or reject with a reason."
4
4
  ---
5
5
 
6
6
  # Review Offered Task
@@ -17,7 +17,7 @@ You have been offered a task. Your job is to review it and decide whether to acc
17
17
  - Is the task description clear enough to proceed?
18
18
 
19
19
  3. **Make a decision**:
20
- - **Accept**: If you can complete this task, call `task-action` with `action: "accept"` and `taskId: "<taskId>"`. Then immediately use `/skill:work-on-task <taskId>` to start working on it.
20
+ - **Accept**: If you can complete this task, call `task-action` with `action: "accept"` and `taskId: "<taskId>"`. Then immediately use the `work-on-task` skill with the taskId to start working on it.
21
21
  - **Reject**: If you cannot complete this task, call `task-action` with `action: "reject"`, `taskId: "<taskId>"`, and provide a `reason` explaining why you're rejecting it (e.g., "Task requires Python expertise which I don't have", "Task description is too vague").
22
22
 
23
23
  ## Example Accept Flow
@@ -26,7 +26,7 @@ You have been offered a task. Your job is to review it and decide whether to acc
26
26
  1. get-task-details taskId="abc-123"
27
27
  2. [Review the task details]
28
28
  3. task-action action="accept" taskId="abc-123"
29
- 4. /skill:work-on-task abc-123
29
+ 4. /work-on-task abc-123
30
30
  ```
31
31
 
32
32
  ## Example Reject Flow
@@ -35,11 +35,11 @@ You have been offered a task. Your job is to review it and decide whether to acc
35
35
  1. get-task-details taskId="abc-123"
36
36
  2. [Review the task details]
37
37
  3. task-action action="reject" taskId="abc-123" reason="Task requires access to production database which I don't have"
38
- 4. Reply "DONE" to end the session
38
+ 4. Stop
39
39
  ```
40
40
 
41
41
  ## Important Notes
42
42
 
43
43
  - Always provide a clear reason when rejecting a task - this helps the lead agent reassign it appropriately
44
- - If you accept, you must immediately start working on the task using `/skill:work-on-task`
44
+ - If you accept, you must immediately start working on the task using the `work-on-task` skill
45
45
  - If you reject, the task returns to the unassigned pool for reassignment
@@ -0,0 +1,14 @@
1
+ {
2
+ "kind": "skill",
3
+ "name": "review-offered-task",
4
+ "displayName": "Review Offered Task",
5
+ "slug": "review-offered-task",
6
+ "title": "Review Offered Task",
7
+ "description": "Turn prompt for an offered swarm task: read the offer, accept with task-action and start work-on-task, or reject with a reason.",
8
+ "version": "1.0.0",
9
+ "category": "skills",
10
+ "placeholders": [],
11
+ "runAllSeedersCandidate": true,
12
+ "systemDefault": true,
13
+ "tags": ["tasks", "turn-prompt", "worker"]
14
+ }
@@ -1,8 +1,3 @@
1
- ---
2
- description: Review a task that has been offered to you and decide whether to accept or reject it
3
- argument-hint: [taskId]
4
- ---
5
-
6
1
  # Review Offered Task
7
2
 
8
3
  You have been offered a task. Your job is to review it and decide whether to accept or reject it based on your capabilities and current workload.
@@ -17,7 +12,7 @@ You have been offered a task. Your job is to review it and decide whether to acc
17
12
  - Is the task description clear enough to proceed?
18
13
 
19
14
  3. **Make a decision**:
20
- - **Accept**: If you can complete this task, call `task-action` with `action: "accept"` and `taskId: "<taskId>"`. Then immediately use `/work-on-task <taskId>` to start working on it.
15
+ - **Accept**: If you can complete this task, call `task-action` with `action: "accept"` and `taskId: "<taskId>"`. Then immediately use the `work-on-task` skill with the taskId to start working on it.
21
16
  - **Reject**: If you cannot complete this task, call `task-action` with `action: "reject"`, `taskId: "<taskId>"`, and provide a `reason` explaining why you're rejecting it (e.g., "Task requires Python expertise which I don't have", "Task description is too vague").
22
17
 
23
18
  ## Example Accept Flow
@@ -35,11 +30,11 @@ You have been offered a task. Your job is to review it and decide whether to acc
35
30
  1. get-task-details taskId="abc-123"
36
31
  2. [Review the task details]
37
32
  3. task-action action="reject" taskId="abc-123" reason="Task requires access to production database which I don't have"
38
- 4. Reply "DONE" to end the session
33
+ 4. Stop
39
34
  ```
40
35
 
41
36
  ## Important Notes
42
37
 
43
38
  - Always provide a clear reason when rejecting a task - this helps the lead agent reassign it appropriately
44
- - If you accept, you must immediately start working on the task using `/work-on-task`
39
+ - If you accept, you must immediately start working on the task using the `work-on-task` skill
45
40
  - If you reject, the task returns to the unassigned pool for reassignment
@@ -41,6 +41,10 @@ The template is the whole task the agent receives. It must state:
41
41
 
42
42
  Tasks created by a schedule are automatic tasks. Their completed output is not stored as memory unless the agent calls `store-progress` with `persistMemory: true`.
43
43
 
44
+ ## Check back later
45
+
46
+ A task whose answer needs time (a build, a deploy, a reply) calls `defer-task` with `delayMs` or `runAt`, a `summary` of what you did so far, and a `note` that says what is pending. The tool completes the task and creates the one-off schedule for you. The task reaches its final state (`completed`) on this call, and the `summary` becomes its output. The wake-up task carries the deferred task as its parent, so you receive that task's context. Add `checks` to list what to verify on wake-up. Do not hand-roll this with `create-schedule`.
47
+
44
48
  ## Secrets
45
49
 
46
50
  A `taskTemplate` and `scriptArgs` are stored as plain text and replayed on every run. They must not contain a token, password, or key. Call the external API from a script through a registered connection or a credential binding instead. See the `swarm-scripts` skill, section Secrets.
@@ -30,12 +30,12 @@ The task carries `slackChannelId` and `slackThreadTs` in its metadata. A follow-
30
30
  | `slack-read` | read a thread by `taskId` or `inboxMessageId`, or a channel by `channelId` (lead) |
31
31
  | `slack-list-channels` | channels the bot is a member of |
32
32
  | `slack-upload-file` | upload a file to a thread or channel, up to 1 GB |
33
- | `slack-download-file` | download a Slack file by ID or URL |
33
+ | `slack-download-file` | attach a Slack file (by ID or URL) to your task; run the `fetchCommand` it returns to get the bytes |
34
34
  | `slack-update` | edit your own message |
35
35
  | `slack-delete` | delete your own message (lead) |
36
36
  | `slack-create-channel`, `slack-invite-to-channel`, `slack-archive-channel` | channel lifecycle (lead) |
37
37
 
38
- Files attached to your task already come with download commands in the task message. Use those before `slack-download-file`.
38
+ Files attached to your task already come with download commands in the task message. Use those before `slack-download-file`. `slack-read` on your task also attaches the files in the thread and gives each one a `fetchCommand`. Don't look for Slack files under `/app/shared` or `/workspace/shared/downloads`: those paths are the API server's disk.
39
39
 
40
40
  ## Unknown user
41
41
 
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: work-on-task
3
+ description: "Turn prompt for an assigned swarm task: find the task, gather context when the message lacks it, use the skill the task names, finish with one of the four endings."
4
+ ---
5
+
6
+ # Working on a task
7
+
8
+ The taskId follows the command. Without one, call `get-tasks` with `mineOnly: true` and take your `pending` or `in_progress` task. If there is none, say so and stop.
9
+
10
+ This message carries the task text, its attachments, its output format, and memories from past sessions. If it does not (you invoked this command yourself, or the context was compacted), run the `task-context-gathering` script with the taskId.
11
+
12
+ When the task names a skill (`researching`, `planning`, `implementing`), use it. Otherwise work directly.
13
+
14
+ Finish the task with one of the four endings in your operating contract: `completed`, `defer-task`, `request-human-input`, or `failed`. Then stop.
15
+
16
+ If the user interrupts, follow their instructions. To resume, call `/work-on-task <taskId>` again.
@@ -0,0 +1,14 @@
1
+ {
2
+ "kind": "skill",
3
+ "name": "work-on-task",
4
+ "displayName": "Work On Task",
5
+ "slug": "work-on-task",
6
+ "title": "Work On Task",
7
+ "description": "Turn prompt for an assigned swarm task: find the task, gather context when the message lacks it, use the skill the task names, finish with one of the four endings.",
8
+ "version": "1.0.0",
9
+ "category": "skills",
10
+ "placeholders": [],
11
+ "runAllSeedersCandidate": true,
12
+ "systemDefault": true,
13
+ "tags": ["tasks", "turn-prompt", "worker"]
14
+ }
@@ -0,0 +1,11 @@
1
+ # Working on a task
2
+
3
+ The taskId follows the command. Without one, call `get-tasks` with `mineOnly: true` and take your `pending` or `in_progress` task. If there is none, say so and stop.
4
+
5
+ This message carries the task text, its attachments, its output format, and memories from past sessions. If it does not (you invoked this command yourself, or the context was compacted), run the `task-context-gathering` script with the taskId.
6
+
7
+ When the task names a skill (`researching`, `planning`, `implementing`), use it. Otherwise work directly.
8
+
9
+ Finish the task with one of the four endings in your operating contract: `completed`, `defer-task`, `request-human-input`, or `failed`. Then stop.
10
+
11
+ If the user interrupts, follow their instructions. To resume, call `/work-on-task <taskId>` again.