@desplega.ai/agent-swarm 1.146.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 (214) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-adapter-jnq4612m.js → acp-adapter-9n319wqc.js} +42 -4
  3. package/dist/{actions-s41xcn3f.js → actions-2vxqvpr9.js} +9 -8
  4. package/dist/{app-j8bfw3f6.js → app-w066xfy0.js} +6 -5
  5. package/dist/assistant-kt31dj2h.js +236 -0
  6. package/dist/{boot-reembed-jxdz46jp.js → boot-reembed-az5rassp.js} +7 -6
  7. package/dist/{boot-reembed-x1476agm.js → boot-reembed-j3mm3rfz.js} +8 -7
  8. package/dist/{boot-scrub-logs-rab9h5bt.js → boot-scrub-logs-b6h54817.js} +4 -4
  9. package/dist/{claude-adapter-ep6qekbm.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-0wp3qrcy.js → claude-sdk-session-f0cc1kec.js} +8 -6
  12. package/dist/{cli-bafd6xnf.js → cli-0b6y7kxm.js} +1 -1
  13. package/dist/{cli-jyy3e193.js → cli-12fz972k.js} +4 -4
  14. package/dist/{cli-pzfc7j7g.js → cli-1th1728g.js} +2 -2
  15. package/dist/{cli-68h2mfrn.js → cli-3tgwnf43.js} +3 -3
  16. package/dist/{cli-q3qr220d.js → cli-4nr988h6.js} +1 -1
  17. package/dist/{cli-m4xsy03s.js → cli-53s590z8.js} +1 -1
  18. package/dist/{cli-c8q7292f.js → cli-54wwve05.js} +5 -4
  19. package/dist/{cli-rymrjneb.js → cli-719k9j2c.js} +2 -2
  20. package/dist/{cli-zn65x0w8.js → cli-89xfd7f9.js} +21 -47
  21. package/dist/{cli-506z6jqa.js → cli-8v0g1yc8.js} +1 -1
  22. package/dist/cli-9d8ntetj.js +42 -0
  23. package/dist/{cli-dhg6j8h3.js → cli-atcve9y8.js} +826 -737
  24. package/dist/{cli-46yck6nj.js → cli-b5bq9crk.js} +14 -31
  25. package/dist/cli-d8brzxjb.js +190 -0
  26. package/dist/{cli-a960nkfb.js → cli-de14znh8.js} +2 -2
  27. package/dist/{cli-s8sbe32s.js → cli-dttph1bw.js} +2 -2
  28. package/dist/cli-ep19k3mb.js +19 -0
  29. package/dist/{cli-d9pnxgth.js → cli-f146mn5s.js} +7 -50
  30. package/dist/{cli-qar1f940.js → cli-f7dsy61y.js} +5 -3
  31. package/dist/{cli-d4xqde2b.js → cli-ftht3zzj.js} +2780 -2587
  32. package/dist/{cli-pmwbdw36.js → cli-hbv0kq6w.js} +2 -2
  33. package/dist/{cli-fhsz3twr.js → cli-jsp14b6c.js} +171 -1
  34. package/dist/{cli-2zvtzevh.js → cli-kpv03zhj.js} +13 -11
  35. package/dist/{cli-a9bn9svs.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-6xwhtc64.js → cli-n8508nre.js} +1 -1
  39. package/dist/{cli-q0qq1sxw.js → cli-p1d5b073.js} +37 -2
  40. package/dist/{cli-yqeb27t1.js → cli-rw51bq3j.js} +13 -10
  41. package/dist/{cli-zww3vcnm.js → cli-smby5m17.js} +7 -6
  42. package/dist/{cli-ah5hjk49.js → cli-tzvk9haz.js} +94 -372
  43. package/dist/{cli-yxwe913z.js → cli-w9kb3bk1.js} +1 -1
  44. package/dist/{cli-skd674mt.js → cli-y6b6fb76.js} +25 -3
  45. package/dist/{cli-3k2g3kg2.js → cli-yz3djrm0.js} +9 -9
  46. package/dist/{cli-4secrq32.js → cli-zhjmqxzh.js} +1 -1
  47. package/dist/cli.js +13 -13
  48. package/dist/{codex-adapter-d6qajxj3.js → codex-adapter-zw4dwsx7.js} +2 -2
  49. package/dist/{codex-hook-n7m5k7cr.js → codex-hook-bw8p5nh4.js} +2 -2
  50. package/dist/{codex-session-runner-v9krqe4x.js → codex-session-runner-k981s65v.js} +2 -2
  51. package/dist/{commands-923wsas7.js → commands-sykrxx7e.js} +4 -4
  52. package/dist/{db-m1hy4zh5.js → db-sfbsc8wt.js} +6 -4
  53. package/dist/handlers-0aspcejm.js +603 -0
  54. package/dist/{hook-ee4hff56.js → hook-4nvqj2sb.js} +7 -5
  55. package/dist/{hook-bxe3bkdy.js → hook-hxccs7m5.js} +24 -49
  56. package/dist/{http-a8rx91me.js → http-y06z517d.js} +223 -92
  57. package/dist/{index-697zfzv1.js → index-hssww7kj.js} +15 -14
  58. package/dist/{index-3x9g3nbg.js → index-k8znfx0z.js} +13 -12
  59. package/dist/{index-1j9va48r.js → index-tce1rss4.js} +371 -110
  60. package/dist/{index-jj4q3x59.js → index-x5j894zf.js} +14 -13
  61. package/dist/{keepalive-sds4e4ce.js → keepalive-w733ax66.js} +6 -6
  62. package/dist/{lead-ggq6szk8.js → lead-75bjqa9d.js} +29 -27
  63. package/dist/{maintenance-093p41d4.js → maintenance-w4f1zjck.js} +8 -7
  64. package/dist/{oauth-refresh-sweep-dvzq4ckj.js → oauth-refresh-sweep-wdetky26.js} +5 -5
  65. package/dist/{onboard-ydr9tzw5.js → onboard-bb5net8s.js} +2 -2
  66. package/dist/{otel-impl-zx3tdf3j.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-7e6pqnzp.js → pricing-refresh-9nzsrgcq.js} +6 -6
  69. package/dist/{rbac-roles-p2rh6d0r.js → rbac-roles-r3hqjp09.js} +5 -5
  70. package/dist/{rbac-roles-0dv1wt0w.js → rbac-roles-z1jrffbt.js} +4 -4
  71. package/dist/{registry-9kdck5ye.js → registry-v5wytg0s.js} +1 -2
  72. package/dist/{render-v2-4k3jt4r2.js → render-v2-vbr89fnb.js} +6 -5
  73. package/dist/{seed-pricing-2cc1qmcd.js → seed-pricing-g62hy1pk.js} +7 -5
  74. package/dist/{setup-c5yss30k.js → setup-gnfqnptp.js} +2 -2
  75. package/dist/{worker-7srgeqcr.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 +583 -3428
  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 +1 -1
  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/mcp.ts +32 -0
  113. package/src/http/pages-public.ts +20 -4
  114. package/src/http/poll.ts +4 -0
  115. package/src/http/repos.ts +3 -3
  116. package/src/http/sessions.ts +82 -0
  117. package/src/http/tasks.ts +10 -2
  118. package/src/prompts/session-templates.ts +4 -2
  119. package/src/providers/acp-adapter.ts +42 -6
  120. package/src/providers/pi-mono-adapter.ts +23 -6
  121. package/src/scheduler/scheduler.test.ts +49 -2
  122. package/src/scheduler/scheduler.ts +4 -0
  123. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  124. package/src/scripts-runtime/types/stdlib.d.ts +8 -0
  125. package/src/scripts-runtime/types/swarm-sdk.d.ts +8 -0
  126. package/src/server.ts +2 -0
  127. package/src/slack/assistant.ts +87 -46
  128. package/src/slack/handlers.test.ts +1 -3
  129. package/src/slack/handlers.ts +102 -87
  130. package/src/slack/inbound-files.ts +466 -0
  131. package/src/slack/templates.ts +14 -0
  132. package/src/tasks/task-terminal-effects.ts +158 -0
  133. package/src/tests/acp-adapter.test.ts +327 -192
  134. package/src/tests/budget-claim-gate.test.ts +1 -0
  135. package/src/tests/build-pi-skills.test.ts +5 -7
  136. package/src/tests/claude-managed-setup.test.ts +52 -4
  137. package/src/tests/claude-md-session.test.ts +603 -0
  138. package/src/tests/codex-skill-resolver.test.ts +19 -0
  139. package/src/tests/context-versioning.test.ts +107 -0
  140. package/src/tests/credential-check.test.ts +157 -14
  141. package/src/tests/db-agents.test.ts +91 -0
  142. package/src/tests/db-context-versions.test.ts +37 -0
  143. package/src/tests/db-extraction-contract.test.ts +45 -0
  144. package/src/tests/db-runtime.test.ts +70 -0
  145. package/src/tests/db-task-read-contract.test.ts +624 -0
  146. package/src/tests/db-task-write-contract.test.ts +653 -0
  147. package/src/tests/db-tasks-read.test.ts +183 -0
  148. package/src/tests/db-tasks-write.test.ts +230 -0
  149. package/src/tests/defer-task.test.ts +389 -0
  150. package/src/tests/docs-index-footprint.test.js +2 -2
  151. package/src/tests/e2e-visuals-stage.test.ts +151 -0
  152. package/src/tests/file-lock.test.ts +144 -0
  153. package/src/tests/fixtures/file-lock-holder.ts +13 -0
  154. package/src/tests/fixtures/hold-file-lock.ts +25 -0
  155. package/src/tests/heartbeat-expiry.test.ts +188 -0
  156. package/src/tests/http-api-integration.test.ts +78 -1
  157. package/src/tests/pages-public-html.test.ts +43 -0
  158. package/src/tests/pages-svg-migration.test.ts +42 -0
  159. package/src/tests/preload.ts +4 -1
  160. package/src/tests/profile-sync-warning.test.ts +16 -0
  161. package/src/tests/profile-sync.test.ts +126 -0
  162. package/src/tests/prompt-template-session.test.ts +20 -4
  163. package/src/tests/provider-registration.test.ts +93 -0
  164. package/src/tests/rbac-e2e-helpers.test.ts +53 -0
  165. package/src/tests/rbac-e2e-helpers.ts +19 -2
  166. package/src/tests/routing-affinity-inventory.test.ts +1 -1
  167. package/src/tests/runner-attachments-section.test.ts +22 -2
  168. package/src/tests/runner-context-preamble.test.ts +30 -0
  169. package/src/tests/runner-deployment-resume.test.ts +107 -0
  170. package/src/tests/runner-repo-autostash.test.ts +12 -0
  171. package/src/tests/script-connections.test.ts +4 -0
  172. package/src/tests/seed-scripts.test.ts +2 -2
  173. package/src/tests/session-tokens.test.ts +279 -0
  174. package/src/tests/slack-file-tools-attachments.test.ts +408 -0
  175. package/src/tests/slack-inbound-files.test.ts +847 -0
  176. package/src/tests/star-history-refresh.test.ts +102 -0
  177. package/src/tests/swarm-repos.test.ts +20 -0
  178. package/src/tests/tool-annotations.test.ts +4 -3
  179. package/src/tests/ui-logs-parser.test.ts +176 -0
  180. package/src/tools/create-page.ts +4 -2
  181. package/src/tools/defer-task.ts +239 -0
  182. package/src/tools/repos/update-repo.ts +2 -2
  183. package/src/tools/schedules/create-schedule.ts +1 -0
  184. package/src/tools/schedules/list-schedules.ts +1 -0
  185. package/src/tools/schedules/patch-schedule.ts +1 -0
  186. package/src/tools/schedules/update-schedule.ts +1 -0
  187. package/src/tools/slack-download-file.ts +60 -29
  188. package/src/tools/slack-read.ts +73 -32
  189. package/src/tools/store-progress.ts +16 -146
  190. package/src/tools/tool-config.ts +1 -0
  191. package/src/tools/utils.ts +21 -0
  192. package/src/types.ts +37 -4
  193. package/src/utils/acp-session-token.ts +56 -0
  194. package/src/utils/file-lock.ts +111 -0
  195. package/src/utils/heartbeat-expiry.ts +78 -0
  196. package/src/utils/request-auth-context.ts +2 -0
  197. package/src/utils/task-attachment-links.ts +22 -0
  198. package/templates/skills/code-quality/content.md +8 -5
  199. package/templates/skills/download-task-attachment/content.md +2 -1
  200. package/{plugin/pi-skills → templates/skills}/review-offered-task/SKILL.md +5 -5
  201. package/templates/skills/review-offered-task/config.json +14 -0
  202. package/{plugin/commands/review-offered-task.md → templates/skills/review-offered-task/content.md} +3 -8
  203. package/templates/skills/scheduling/content.md +4 -0
  204. package/templates/skills/slack-interaction/content.md +2 -2
  205. package/templates/skills/work-on-task/SKILL.md +16 -0
  206. package/templates/skills/work-on-task/config.json +14 -0
  207. package/templates/skills/work-on-task/content.md +11 -0
  208. package/dist/assistant-dx3ew4d8.js +0 -212
  209. package/dist/cli-00kyrp1a.js +0 -317
  210. package/dist/cli-n1q8q14d.js +0 -177
  211. package/dist/cli-ss6vybmt.js +0 -269
  212. package/dist/handlers-e9vp2fr8.js +0 -592
  213. package/plugin/commands/work-on-task.md +0 -39
  214. package/plugin/pi-skills/work-on-task/SKILL.md +0 -39
@@ -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.