@desplega.ai/agent-swarm 1.156.0 → 1.158.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 (263) hide show
  1. package/dist/{acp-adapter-c1msptrw.js → acp-adapter-xaq4n6pc.js} +1 -1
  2. package/dist/{actions-wd41qtdc.js → actions-p9hz5yat.js} +15 -15
  3. package/dist/{app-tz2qrnah.js → app-g26wdcz6.js} +4 -4
  4. package/dist/{artifact-td4q4dch.js → artifact-3v5t9jtg.js} +1 -1
  5. package/dist/{assistant-c3q1zj2t.js → assistant-wydfqkb3.js} +16 -16
  6. package/dist/{boot-reembed-0be1krkm.js → boot-reembed-g5ckg9yb.js} +6 -6
  7. package/dist/{boot-reembed-r00j03f3.js → boot-reembed-se8zmdcp.js} +5 -5
  8. package/dist/{boot-scrub-logs-9j4hd0vw.js → boot-scrub-logs-wfxqan61.js} +3 -3
  9. package/dist/{seed-pricing-ncgxz1r2.js → boot-seeds-s3q01xj3.js} +10 -10
  10. package/dist/{claude-adapter-112w28qc.js → claude-adapter-tv2x4f36.js} +7 -7
  11. package/dist/{claude-managed-adapter-ea2aga6w.js → claude-managed-adapter-41b2s7fd.js} +2 -2
  12. package/dist/{claude-sdk-session-k7pb5kfz.js → claude-sdk-session-t442qz6f.js} +8 -8
  13. package/dist/{cli-e781c6f9.js → cli-0xjkj1j7.js} +1 -0
  14. package/dist/{cli-58rgvkt4.js → cli-1hkrch3k.js} +2 -2
  15. package/dist/{cli-mx3bzbv8.js → cli-20cj37vt.js} +1968 -44
  16. package/dist/{cli-h0wdadsx.js → cli-4198qc9v.js} +7 -3
  17. package/dist/{cli-24w39x6n.js → cli-47m6dkd3.js} +362 -126
  18. package/dist/{cli-90x9ewbz.js → cli-53sz8gkz.js} +1 -1
  19. package/dist/{cli-005pze6s.js → cli-5d10vd2m.js} +1 -1
  20. package/dist/{cli-08n8jxcw.js → cli-65hjmev5.js} +57 -17
  21. package/dist/{cli-scwq51gn.js → cli-68w09sjw.js} +7 -1
  22. package/dist/{cli-7j2098n5.js → cli-7gtvvpbh.js} +4 -4
  23. package/dist/{cli-b05d7szr.js → cli-7s3syk22.js} +18 -11
  24. package/dist/{cli-9w4ex8vh.js → cli-8qmme6vd.js} +2 -2
  25. package/dist/{cli-28wkjpdp.js → cli-987hg0aa.js} +2 -2
  26. package/dist/{cli-7pgcpcrg.js → cli-9httdpyq.js} +1 -1
  27. package/dist/{cli-jk26c1sz.js → cli-aky8tctm.js} +26 -93
  28. package/dist/{cli-865swy5p.js → cli-c07r9a2m.js} +3 -2
  29. package/dist/{cli-nyqza443.js → cli-fgqxwrpv.js} +291 -12
  30. package/dist/{cli-9pyah9c4.js → cli-ftfvggzx.js} +633 -166
  31. package/dist/{cli-ytxs7mn1.js → cli-g8djp6qe.js} +17 -0
  32. package/dist/{cli-avkd6mqv.js → cli-h967nb6r.js} +551 -0
  33. package/dist/{cli-y8bfxjtw.js → cli-j55n64b6.js} +23 -22
  34. package/dist/{cli-7vj52450.js → cli-j57qkrpq.js} +86 -28
  35. package/dist/{cli-tjmghmm5.js → cli-q67y4wzm.js} +6 -3
  36. package/dist/{cli-n6zq3kbd.js → cli-rj19hc34.js} +1 -1
  37. package/dist/{cli-tp0d7vzb.js → cli-rjehvrts.js} +4 -4
  38. package/dist/{cli-s8hpxeg4.js → cli-rqz8qbw7.js} +245 -75
  39. package/dist/{cli-p37sg0a9.js → cli-s8eet254.js} +1 -1
  40. package/dist/cli-t1ze64h3.js +811 -0
  41. package/dist/{cli-hc2fkhdv.js → cli-ws6rqycm.js} +3 -2
  42. package/dist/{cli-t1cd3mhp.js → cli-wscdvcqz.js} +13 -5
  43. package/dist/{cli-xa4j64zh.js → cli-x24pshtq.js} +5 -5
  44. package/dist/{cli-nbp6m6gp.js → cli-xberxa1r.js} +51 -26
  45. package/dist/{cli-szcyv1mm.js → cli-xhaeg2nc.js} +1 -1
  46. package/dist/cli.js +30 -16
  47. package/dist/{codex-adapter-g4tj1f2g.js → codex-adapter-cnm8wk11.js} +5 -5
  48. package/dist/{codex-session-runner-ad8ha7zt.js → codex-session-runner-gfg547zj.js} +5 -5
  49. package/dist/{commands-z3gd8ppc.js → commands-e3bn0p75.js} +3 -3
  50. package/dist/{db-bezm97r9.js → db-03v9kzr2.js} +17 -3
  51. package/dist/{dispatcher-a97hc33y.js → dispatcher-nrmg8yht.js} +14 -14
  52. package/dist/{dsh-adapter-1dwg97zv.js → dsh-adapter-k03nfqa6.js} +2 -2
  53. package/dist/{handlers-wtdy1x02.js → handlers-aqcedbyb.js} +17 -17
  54. package/dist/{hook-7edgmzng.js → hook-7vkskkre.js} +19 -11
  55. package/dist/{hook-fvs58a5b.js → hook-ybvmrmhq.js} +4 -4
  56. package/dist/{http-phnf93hp.js → http-2eqa8t24.js} +432 -195
  57. package/dist/{index-7x131mmf.js → index-1yks3qcr.js} +14 -14
  58. package/dist/{index-1fktvwxc.js → index-a3vn96n8.js} +31 -28
  59. package/dist/{index-yp8rasxk.js → index-c6zsv1bd.js} +5 -5
  60. package/dist/{index-50jxgw0d.js → index-chgdkq36.js} +14 -14
  61. package/dist/{index-7jwk7nsm.js → index-x71bvq0p.js} +15 -15
  62. package/dist/{keepalive-nnz07ebd.js → keepalive-vtkf5n99.js} +5 -5
  63. package/dist/{lead-mg7qcgww.js → lead-h3sp357x.js} +26 -26
  64. package/dist/{maintenance-ywmnqrpx.js → maintenance-jzeqtk3q.js} +6 -6
  65. package/dist/{oauth-refresh-sweep-c63sqfs1.js → oauth-refresh-sweep-qp3dzmdp.js} +4 -4
  66. package/dist/{onboard-2zj8sqkq.js → onboard-tmsp0qbz.js} +278 -81
  67. package/dist/{opencode-adapter-z64xjzec.js → opencode-adapter-x6x2da1z.js} +3 -3
  68. package/dist/{otel-impl-jgdfm26f.js → otel-impl-9mz73q65.js} +416 -301
  69. package/dist/{pi-mono-adapter-2b7mw16r.js → pi-mono-adapter-m6dvv0dz.js} +2 -2
  70. package/dist/{pricing-refresh-3whntb69.js → pricing-refresh-v1cbr9te.js} +5 -5
  71. package/dist/{rbac-roles-3zptcc3p.js → rbac-roles-c9wqz83x.js} +4 -4
  72. package/dist/{render-v2-dbvpyysc.js → render-v2-gryrh9h8.js} +4 -4
  73. package/dist/{setup-qwx0j8z9.js → setup-q9720bt6.js} +2 -2
  74. package/dist/{virtual-modules-8qheft18.js → virtual-modules-3htpgn8x.js} +1 -1
  75. package/dist/{work-objects-sjbcmvep.js → work-objects-b76x7deb.js} +17 -17
  76. package/dist/{worker-xezqdkb7.js → worker-6vrxbv41.js} +26 -26
  77. package/openapi.json +1626 -629
  78. package/package.json +3 -2
  79. package/src/apps/catalog.generated.json +229 -0
  80. package/src/apps/page-validator.ts +71 -16
  81. package/src/apps/row-store.ts +66 -11
  82. package/src/apps/sync.ts +59 -17
  83. package/src/artifact-sdk/browser-sdk.ts +436 -2
  84. package/src/be/approval-sweeps.ts +109 -0
  85. package/src/be/boot-seeds.ts +40 -0
  86. package/src/be/chunking.ts +1 -1
  87. package/src/be/db/agents.ts +20 -0
  88. package/src/be/db/approvals.ts +78 -0
  89. package/src/be/db/tasks/read.ts +197 -39
  90. package/src/be/db/tasks/write.ts +41 -9
  91. package/src/be/db-client.ts +107 -48
  92. package/src/be/db.ts +219 -76
  93. package/src/be/events.ts +33 -1
  94. package/src/be/memory/graph-expansion.ts +10 -3
  95. package/src/be/memory/providers/sqlite-store.ts +55 -13
  96. package/src/be/memory/raters/retrieval.ts +5 -2
  97. package/src/be/memory/reranker.ts +26 -2
  98. package/src/be/memory/types.ts +2 -0
  99. package/src/be/migrations/166_approval_request_auto_cancellation.sql +9 -0
  100. package/src/be/migrations/171_memory_retrieval_relevance.sql +8 -0
  101. package/src/be/migrations/175_workflow_runs_list_index.sql +11 -0
  102. package/src/be/migrations/177_events_event_agent_created_index.sql +11 -0
  103. package/src/be/models-catalog.ts +2 -0
  104. package/src/be/modelsdev-cache.json +1857 -0
  105. package/src/be/modelsdev-cache.ts +14 -1
  106. package/src/be/pricing-normalize.ts +2 -0
  107. package/src/be/scripts/typecheck.ts +1 -0
  108. package/src/be/seed-pricing.ts +3 -1
  109. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  110. package/src/be/steering.ts +53 -3
  111. package/src/be/swarm-config-guard.ts +3 -0
  112. package/src/be/task-citations.ts +29 -0
  113. package/src/cli.tsx +14 -1
  114. package/src/commands/onboard/compose-generator.ts +8 -3
  115. package/src/commands/onboard/container-engine.ts +220 -0
  116. package/src/commands/onboard/manifest.ts +1 -0
  117. package/src/commands/onboard/steps/health-check.tsx +4 -1
  118. package/src/commands/onboard/steps/prereq-check.tsx +50 -54
  119. package/src/commands/onboard/steps/start.tsx +31 -16
  120. package/src/commands/onboard/types.ts +7 -2
  121. package/src/commands/onboard.tsx +39 -4
  122. package/src/commands/profile-sync.ts +79 -26
  123. package/src/commands/runner.ts +8 -1
  124. package/src/extensions/catalog.generated.json +17 -0
  125. package/src/extensions/contract-types.generated.ts +1 -1
  126. package/src/heartbeat/heartbeat.ts +263 -103
  127. package/src/hooks/hook.ts +21 -12
  128. package/src/http/agents.ts +35 -0
  129. package/src/http/approval-requests.ts +136 -64
  130. package/src/http/apps.ts +14 -3
  131. package/src/http/config.ts +15 -1
  132. package/src/http/events.ts +62 -2
  133. package/src/http/index.ts +6 -4
  134. package/src/http/memory.ts +21 -7
  135. package/src/http/schedules.ts +1 -1
  136. package/src/http/session-data.ts +9 -4
  137. package/src/http/sessions.ts +25 -18
  138. package/src/http/stats.ts +19 -7
  139. package/src/http/status.ts +11 -1
  140. package/src/http/tasks.ts +40 -8
  141. package/src/http/webhooks.ts +67 -46
  142. package/src/http/workflows.ts +15 -20
  143. package/src/linear/outbound.ts +37 -15
  144. package/src/otel-impl.ts +151 -1
  145. package/src/otel.ts +14 -3
  146. package/src/prompts/memories.ts +19 -2
  147. package/src/providers/claude-managed-models.ts +8 -0
  148. package/src/providers/codex-agents-md.ts +140 -17
  149. package/src/providers/modelsdev-reasoning.json +551 -0
  150. package/src/rbac/legacy-policy.ts +16 -0
  151. package/src/rbac/permissions.ts +4 -0
  152. package/src/scheduler/deferred-task-waits.ts +28 -0
  153. package/src/scheduler/schedule-task.ts +3 -0
  154. package/src/scheduler/scheduler.ts +14 -0
  155. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  156. package/src/scripts-runtime/swarm-sdk.ts +9 -0
  157. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  158. package/src/scripts-runtime/types/swarm-extension.d.ts +1 -0
  159. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  160. package/src/server.ts +12 -7
  161. package/src/slack/app.ts +35 -4
  162. package/src/slack/connection-state.ts +17 -0
  163. package/src/tasks/context-key.ts +1 -0
  164. package/src/tasks/worker-follow-up.ts +6 -2
  165. package/src/tests/agent-task-activity.test.ts +74 -0
  166. package/src/tests/agents-list-model-display.test.ts +6 -1
  167. package/src/tests/approval-auto-cancellation-migration.test.ts +104 -0
  168. package/src/tests/approval-requests.test.ts +395 -0
  169. package/src/tests/approval-sweeps-config.test.ts +47 -0
  170. package/src/tests/approval-sweeps.test.ts +508 -0
  171. package/src/tests/apps-kibo-blocks.test.ts +77 -0
  172. package/src/tests/apps-spike.test.ts +56 -1
  173. package/src/tests/apps-sync-engine.test.ts +20 -9
  174. package/src/tests/boot-seeds.test.ts +170 -0
  175. package/src/tests/cancel-approval-request-tool.test.ts +115 -0
  176. package/src/tests/claude-managed-adapter.test.ts +6 -0
  177. package/src/tests/codex-adapter.test.ts +126 -1
  178. package/src/tests/config-key-filter.test.ts +80 -0
  179. package/src/tests/context-window.test.ts +1 -0
  180. package/src/tests/db-client-busy-retry.test.ts +96 -0
  181. package/src/tests/db-client.test.ts +60 -1
  182. package/src/tests/db-task-read-contract.test.ts +10 -0
  183. package/src/tests/db-task-write-contract.test.ts +9 -0
  184. package/src/tests/deferred-schedule-reaper.test.ts +193 -0
  185. package/src/tests/docs-index-footprint.test.js +3 -3
  186. package/src/tests/events-http.test.ts +103 -0
  187. package/src/tests/extensions-catalog.test.ts +22 -17
  188. package/src/tests/extensions-pre-task.test.ts +107 -0
  189. package/src/tests/fixtures/kibo-blocks-definition.json.txt +105 -0
  190. package/src/tests/follow-up-config-inheritance.test.ts +260 -0
  191. package/src/tests/heartbeat-orphan-resume.test.ts +84 -0
  192. package/src/tests/heartbeat-reboot-live.test.ts +95 -0
  193. package/src/tests/heartbeat-stall-cas.test.ts +80 -0
  194. package/src/tests/heartbeat.test.ts +99 -3
  195. package/src/tests/hook-mcp-config.test.ts +29 -0
  196. package/src/tests/http-api-integration.test.ts +16 -2
  197. package/src/tests/kv-storage.test.ts +62 -0
  198. package/src/tests/linear-outbound-sync.test.ts +120 -5
  199. package/src/tests/list-endpoint-slimming.test.ts +91 -0
  200. package/src/tests/memory-graph-organic-route.test.ts +155 -0
  201. package/src/tests/memory-http-recall-gating.test.ts +14 -4
  202. package/src/tests/memory-hybrid.test.ts +161 -1
  203. package/src/tests/memory-prompt-recall-crowdout.test.ts +297 -0
  204. package/src/tests/memory.test.ts +22 -0
  205. package/src/tests/onboard-container-engine.test.ts +240 -0
  206. package/src/tests/otel-event-loop-and-logs.test.ts +99 -0
  207. package/src/tests/pricing-routes.test.ts +14 -0
  208. package/src/tests/profile-sync-warning.test.ts +8 -4
  209. package/src/tests/promote-draft-task-route.test.ts +19 -0
  210. package/src/tests/provider-registration.test.ts +296 -42
  211. package/src/tests/rbac-engine.test.ts +30 -0
  212. package/src/tests/reasoning-effort.test.ts +8 -0
  213. package/src/tests/script-connections.test.ts +7 -0
  214. package/src/tests/sdk-allowlist.test.ts +39 -0
  215. package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
  216. package/src/tests/sessions-detail-batch.test.ts +250 -0
  217. package/src/tests/sessions.test.ts +118 -0
  218. package/src/tests/slack-config.test.ts +22 -0
  219. package/src/tests/sonnet-5-5-registry.test.ts +44 -0
  220. package/src/tests/status.test.ts +36 -1
  221. package/src/tests/swarm-diff.test.ts +316 -10
  222. package/src/tests/task-completion-idempotency.test.ts +2 -1
  223. package/src/tests/tasks-requested-by-filter.test.ts +3 -1
  224. package/src/tests/tool-annotations.test.ts +1 -1
  225. package/src/tests/ui-list-slim-projections.test.ts +158 -0
  226. package/src/tests/webhooks-task-blocked.test.ts +127 -0
  227. package/src/tests/workflow-approval-recovery.test.ts +203 -0
  228. package/src/tests/workflow-hitl-routing.test.ts +22 -1
  229. package/src/tests/workflow-hitl-slack-card.test.ts +52 -0
  230. package/src/tests/workflow-http-v2.test.ts +99 -8
  231. package/src/tests/workflow-retry-validation.test.ts +56 -0
  232. package/src/tests/workflow-tla-races.test.ts +389 -0
  233. package/src/tests/workflow-validation-port-routing.test.ts +113 -1
  234. package/src/tools/cancel-approval-request.ts +57 -0
  235. package/src/tools/memory-search.ts +1 -0
  236. package/src/tools/request-human-input.ts +4 -2
  237. package/src/tools/schedules/list-schedules.ts +4 -2
  238. package/src/tools/send-task.ts +4 -0
  239. package/src/tools/templates.ts +19 -0
  240. package/src/tools/tool-config.ts +2 -1
  241. package/src/tools/workflows/list-workflow-runs.ts +1 -0
  242. package/src/types.ts +38 -0
  243. package/src/utils/context-window.ts +1 -0
  244. package/src/workflows/approval-cancel.ts +152 -0
  245. package/src/workflows/approval-notifications.ts +84 -0
  246. package/src/workflows/checkpoint.ts +10 -5
  247. package/src/workflows/definition.ts +21 -0
  248. package/src/workflows/engine.ts +152 -21
  249. package/src/workflows/executors/human-in-the-loop.ts +1 -1
  250. package/src/workflows/recovery.ts +100 -45
  251. package/src/workflows/resume.ts +49 -18
  252. package/src/workflows/retry-poller.ts +71 -21
  253. package/templates/extensions/README.md +1 -0
  254. package/templates/extensions/github-sender-allowlist/README.md +18 -0
  255. package/templates/extensions/github-sender-allowlist/hooks.ts +55 -0
  256. package/templates/extensions/github-sender-allowlist/manifest.json +8 -0
  257. package/templates/skills/apps/content.md +20 -1
  258. package/templates/skills/code-quality/content.md +26 -1
  259. package/templates/skills/pages/content.md +16 -4
  260. package/templates/skills/pages/files/examples/multiplayer-board.html +3 -12
  261. package/templates/skills/scheduling/content.md +1 -1
  262. package/dist/cli-2daksqps.js +0 -378
  263. package/dist/rbac-roles-3zw68x72.js +0 -37
@@ -20,6 +20,7 @@ import {
20
20
  import * as dbClient from "../be/db-client";
21
21
  import type { InstallExtensionArgs as ExtensionInstallBody } from "../be/extensions/db";
22
22
  import { installExtension, listExtensionRuns } from "../be/extensions/db";
23
+ import { getCatalogEntry } from "../extensions/catalog";
23
24
  import { disableExtension, enableExtension, stopExtensionRuntime } from "../extensions/lifecycle";
24
25
  import { handleTasks } from "../http/tasks";
25
26
  import { dispatchScheduleTarget } from "../scheduler/scheduler";
@@ -168,6 +169,112 @@ describe("extension task boundaries", () => {
168
169
  expect(retried.status).toBe(201);
169
170
  });
170
171
 
172
+ test("GitHub sender allowlist separates internal and external senders", async () => {
173
+ const template = getCatalogEntry("github-sender-allowlist");
174
+ if (!template) throw new Error("github-sender-allowlist is missing from the catalog");
175
+ const installed = await installExtension({
176
+ manifest: template.manifest,
177
+ files: template.files,
178
+ config: {
179
+ internal: ["tarasyarema", "harlequinetcie"],
180
+ external: ["fuvidani", "capchase-bot"],
181
+ reviewBots: ["review-bot"],
182
+ },
183
+ });
184
+ await enableExtension(installed.extension.id);
185
+
186
+ const createFromGitHub = (
187
+ description: string,
188
+ vcsAuthor: string,
189
+ taskType: string,
190
+ vcsUrl: string,
191
+ source: "github" | "api" = "github",
192
+ ) =>
193
+ createTaskWithSiblingAwareness(
194
+ description,
195
+ { source, vcsAuthor, taskType, vcsUrl },
196
+ { origin: "webhook" },
197
+ );
198
+
199
+ await createFromGitHub(
200
+ "internal sender issue",
201
+ "HARLEQUINETCIE",
202
+ "github-issue",
203
+ "https://github.com/acme/project/issues/1",
204
+ );
205
+ await createFromGitHub(
206
+ "internal sender review",
207
+ "tarasyarema",
208
+ "github-review",
209
+ "https://github.com/acme/project/pull/2",
210
+ );
211
+ await createFromGitHub(
212
+ "external sender PR comment",
213
+ "FuViDaNi",
214
+ "github-comment",
215
+ "https://github.com/acme/project/pull/2",
216
+ );
217
+ await createFromGitHub(
218
+ "external sender PR assignment",
219
+ "capchase-bot",
220
+ "github-pr",
221
+ "https://github.com/acme/project/pull/3",
222
+ );
223
+ await createFromGitHub(
224
+ "external sender PR review",
225
+ "fuvidani",
226
+ "github-review",
227
+ "https://github.com/acme/project/pull/4",
228
+ );
229
+
230
+ await expect(
231
+ createFromGitHub(
232
+ "external sender issue comment",
233
+ "fuvidani",
234
+ "github-comment",
235
+ "https://github.com/acme/project/issues/5",
236
+ ),
237
+ ).rejects.toThrow('GitHub sender "fuvidani" is blocked: external senders are allowed only');
238
+ await expect(
239
+ createFromGitHub(
240
+ "unknown sender issue",
241
+ "unknown-user",
242
+ "github-issue",
243
+ "https://github.com/acme/project/issues/6",
244
+ ),
245
+ ).rejects.toThrow('GitHub sender "unknown-user" is blocked: sender is not on the internal');
246
+
247
+ await expect(
248
+ createFromGitHub(
249
+ "unknown sender review",
250
+ "unknown-user",
251
+ "github-review",
252
+ "https://github.com/acme/project/pull/7",
253
+ ),
254
+ ).rejects.toThrow('GitHub sender "unknown-user" is blocked: sender is not on the internal');
255
+ await createFromGitHub(
256
+ "review bot review",
257
+ "REVIEW-BOT",
258
+ "github-review",
259
+ "https://github.com/acme/project/pull/8",
260
+ );
261
+ await expect(
262
+ createFromGitHub(
263
+ "review bot PR comment",
264
+ "review-bot",
265
+ "github-comment",
266
+ "https://github.com/acme/project/pull/9",
267
+ ),
268
+ ).rejects.toThrow('GitHub sender "review-bot" is blocked: review bots are allowed only');
269
+ await createFromGitHub(
270
+ "non-GitHub source",
271
+ "unknown-user",
272
+ "github-issue",
273
+ "https://github.com/acme/project/issues/10",
274
+ "api",
275
+ );
276
+ });
277
+
171
278
  test("scheduler and workflow executor provide their task creation origins", async () => {
172
279
  const extension = await installSource(
173
280
  "record-task-origin",
@@ -0,0 +1,105 @@
1
+ {
2
+ "models": {
3
+ "deal": {
4
+ "columns": {
5
+ "title": { "kind": "string", "required": true },
6
+ "stage": { "kind": "enum", "enum": ["lead", "qualified", "won", "lost"], "default": "lead" },
7
+ "owner": { "kind": "string" },
8
+ "value": { "kind": "number" },
9
+ "closeDate": { "kind": "date" }
10
+ }
11
+ }
12
+ },
13
+ "queries": {
14
+ "deals": { "model": "deal", "sort": { "column": "createdAt", "dir": "desc" } },
15
+ "dealById": { "model": "deal", "filter": { "id": { "$param": "deal" } } }
16
+ },
17
+ "pages": {
18
+ "main": {
19
+ "params": { "deal": { "kind": "string" } },
20
+ "root": "root",
21
+ "elements": {
22
+ "root": {
23
+ "type": "Stack",
24
+ "props": { "gap": "lg" },
25
+ "children": ["board", "calendars", "activity", "dealDrawer"]
26
+ },
27
+ "board": {
28
+ "type": "Kanban",
29
+ "props": {
30
+ "data": { "$state": "/queries/deals/data" },
31
+ "loading": { "$state": "/queries/deals/loading" },
32
+ "columnField": "stage",
33
+ "columns": ["lead", "qualified", { "id": "won", "label": "Won" }, { "id": "lost", "label": "Lost" }],
34
+ "titleField": "title",
35
+ "cardFields": [
36
+ { "key": "owner", "label": "Owner" },
37
+ { "key": "value", "label": "Value", "kind": "number" }
38
+ ],
39
+ "onMove": [
40
+ {
41
+ "action": "app.mutate",
42
+ "params": {
43
+ "model": "deal",
44
+ "op": "update",
45
+ "rowId": { "$row": "id" },
46
+ "values": { "stage": { "$row": "stage" } }
47
+ }
48
+ }
49
+ ],
50
+ "onCardClick": [
51
+ { "action": "app.navigate", "params": { "page": "main", "params": { "deal": { "$row": "id" } } } }
52
+ ]
53
+ }
54
+ },
55
+ "calendars": { "type": "Grid", "props": { "columns": 2 }, "children": ["closeCalendar", "createdCalendar"] },
56
+ "closeCalendar": {
57
+ "type": "Calendar",
58
+ "props": {
59
+ "data": { "$state": "/queries/deals/data" },
60
+ "startField": "closeDate",
61
+ "titleField": "title",
62
+ "onSelect": [
63
+ { "action": "app.navigate", "params": { "page": "main", "params": { "deal": { "$row": "id" } } } }
64
+ ]
65
+ }
66
+ },
67
+ "createdCalendar": {
68
+ "type": "Calendar",
69
+ "props": {
70
+ "data": { "$state": "/queries/deals/data" },
71
+ "startField": "createdAt",
72
+ "endField": "closeDate",
73
+ "titleField": "title"
74
+ }
75
+ },
76
+ "activity": {
77
+ "type": "ContributionGraph",
78
+ "props": {
79
+ "data": { "$state": "/queries/deals/data" },
80
+ "dateField": "createdAt",
81
+ "countField": "value",
82
+ "days": 180
83
+ }
84
+ },
85
+ "dealDrawer": {
86
+ "type": "Drawer",
87
+ "props": { "param": "deal", "title": "Deal" },
88
+ "children": ["dealDetail"]
89
+ },
90
+ "dealDetail": {
91
+ "type": "DetailList",
92
+ "props": {
93
+ "data": { "$state": "/queries/dealById/data/0" },
94
+ "fields": [
95
+ { "key": "title" },
96
+ { "key": "stage", "kind": "badge" },
97
+ { "key": "closeDate", "kind": "date" }
98
+ ]
99
+ }
100
+ }
101
+ }
102
+ }
103
+ },
104
+ "defaultPage": "main"
105
+ }
@@ -0,0 +1,260 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
4
+ import {
5
+ closeDb,
6
+ completeTask,
7
+ createAgent,
8
+ createTaskExtended,
9
+ getDbClient,
10
+ getScheduledTaskById,
11
+ getTaskById,
12
+ initDb,
13
+ startTask,
14
+ } from "../be/db";
15
+ import { getRebootAffectedTasks, runRebootSweep } from "../heartbeat/heartbeat";
16
+ import { createStandaloneScheduleTask } from "../scheduler/schedule-task";
17
+ import { createResumeFollowUp, createWorkerTaskFollowUp } from "../tasks/worker-follow-up";
18
+ import { registerDeferTaskTool } from "../tools/defer-task";
19
+ import { registerSendTaskTool } from "../tools/send-task";
20
+ import type { FollowUpConfig } from "../types";
21
+
22
+ // followUpConfig follows one piece of work, not the thread: continuations of
23
+ // the same work inherit it, new work created with a parentTaskId does not.
24
+
25
+ type Tool = { handler: (args: unknown, extra: unknown) => Promise<CallToolResult> };
26
+
27
+ const ON_COMPLETED: FollowUpConfig = { onCompleted: "Switch the cron and DM the requester." };
28
+
29
+ let leadId: string;
30
+ let workerId: string;
31
+ let sendTask: Tool;
32
+ let deferTask: Tool;
33
+
34
+ function callTool(tool: Tool, args: Record<string, unknown>, agentId: string) {
35
+ return tool.handler(args, {
36
+ sessionId: crypto.randomUUID(),
37
+ requestInfo: { headers: { "x-agent-id": agentId } },
38
+ });
39
+ }
40
+
41
+ function sentTaskId(result: CallToolResult): string {
42
+ const structured = result.structuredContent as { success: boolean; task?: { id: string } };
43
+ expect(structured.success).toBe(true);
44
+ return structured.task!.id;
45
+ }
46
+
47
+ async function followUpConfigOf(taskId: string) {
48
+ return (await getTaskById(taskId))?.followUpConfig;
49
+ }
50
+
51
+ async function workerTaskWithOnCompleted(description: string) {
52
+ const task = await createTaskExtended(description, {
53
+ agentId: workerId,
54
+ followUpConfig: ON_COMPLETED,
55
+ });
56
+ await startTask(task.id);
57
+ return task;
58
+ }
59
+
60
+ beforeAll(async () => {
61
+ initDb(":memory:");
62
+ const server = new McpServer({ name: "follow-up-config-inheritance", version: "1.0.0" });
63
+ registerSendTaskTool(server);
64
+ registerDeferTaskTool(server);
65
+ const tools = (server as unknown as { _registeredTools: Record<string, Tool> })._registeredTools;
66
+ sendTask = tools["send-task"]!;
67
+ deferTask = tools["defer-task"]!;
68
+ });
69
+
70
+ beforeEach(async () => {
71
+ const db = getDbClient();
72
+ await db.run("DELETE FROM agent_tasks");
73
+ await db.run("DELETE FROM agents");
74
+ await db.run("DELETE FROM active_sessions");
75
+ leadId = (
76
+ await createAgent({
77
+ name: "Lead",
78
+ isLead: true,
79
+ status: "idle",
80
+ capabilities: [],
81
+ maxTasks: 50,
82
+ })
83
+ ).id;
84
+ workerId = (
85
+ await createAgent({
86
+ name: "Worker",
87
+ isLead: false,
88
+ status: "idle",
89
+ capabilities: [],
90
+ maxTasks: 50,
91
+ })
92
+ ).id;
93
+ });
94
+
95
+ afterAll(() => closeDb());
96
+
97
+ describe("createTaskExtended followUpConfig inheritance", () => {
98
+ test("a plain child with parentTaskId does not inherit", async () => {
99
+ const parent = await workerTaskWithOnCompleted("parent work");
100
+ const child = await createTaskExtended("unrelated child", { parentTaskId: parent.id });
101
+ expect(child.followUpConfig).toBeUndefined();
102
+ });
103
+
104
+ test("an explicit followUpConfig wins over the parent's", async () => {
105
+ const parent = await workerTaskWithOnCompleted("parent work");
106
+ const own: FollowUpConfig = { onFailed: "Retry once." };
107
+ const child = await createTaskExtended("continuation", {
108
+ parentTaskId: parent.id,
109
+ inheritParentFollowUpConfig: true,
110
+ followUpConfig: own,
111
+ });
112
+ expect(child.followUpConfig).toEqual(own);
113
+ });
114
+ });
115
+
116
+ describe("kept: continuations of the same work inherit followUpConfig", () => {
117
+ test("interrupted-task resume (createResumeFollowUp)", async () => {
118
+ const parent = await workerTaskWithOnCompleted("interrupted work");
119
+ const resume = await createResumeFollowUp({ parentId: parent.id, reason: "crash_recovery" });
120
+ expect(resume.kind).toBe("created");
121
+ if (resume.kind !== "created") return;
122
+ expect(resume.task.followUpConfig).toEqual(ON_COMPLETED);
123
+ });
124
+
125
+ test("reboot-sweep retry", async () => {
126
+ const parent = await workerTaskWithOnCompleted("work cut by a reboot");
127
+ await getDbClient().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
128
+ new Date(Date.now() - 1000).toISOString(),
129
+ parent.id,
130
+ ]);
131
+
132
+ await runRebootSweep();
133
+
134
+ const retryTaskId = getRebootAffectedTasks().find(
135
+ (a) => a.original.id === parent.id,
136
+ )?.retryTaskId;
137
+ expect(retryTaskId).toBeTruthy();
138
+ expect(await followUpConfigOf(retryTaskId!)).toEqual(ON_COMPLETED);
139
+ });
140
+
141
+ test("defer-task wake-up continuation", async () => {
142
+ const parent = await workerTaskWithOnCompleted("work that waits");
143
+ const result = await callTool(
144
+ deferTask,
145
+ { taskId: parent.id, delayMs: 3_600_000, summary: "Waiting", note: "Check back" },
146
+ workerId,
147
+ );
148
+ const { success, scheduleId } = result.structuredContent as {
149
+ success: boolean;
150
+ scheduleId?: string;
151
+ };
152
+ expect(success).toBe(true);
153
+ const schedule = await getScheduledTaskById(scheduleId!);
154
+
155
+ const wake = await createStandaloneScheduleTask(schedule!);
156
+ expect(wake.parentTaskId).toBe(parent.id);
157
+ expect(wake.followUpConfig).toEqual(ON_COMPLETED);
158
+ });
159
+
160
+ test("send-task resume re-delegation (reroute-decision template)", async () => {
161
+ const parent = await workerTaskWithOnCompleted("crashed work");
162
+ const childId = sentTaskId(
163
+ await callTool(
164
+ sendTask,
165
+ {
166
+ task: "resume the crashed work",
167
+ agentId: workerId,
168
+ routingReason: "reroute_fault",
169
+ routingNote: "same specialization as the crashed worker",
170
+ taskType: "resume",
171
+ parentTaskId: parent.id,
172
+ },
173
+ leadId,
174
+ ),
175
+ );
176
+ expect(await followUpConfigOf(childId)).toEqual(ON_COMPLETED);
177
+ });
178
+ });
179
+
180
+ describe("stopped: new work does not inherit followUpConfig", () => {
181
+ test("Lead follow-up from createWorkerTaskFollowUp", async () => {
182
+ const task = await workerTaskWithOnCompleted("worker build task");
183
+ const completed = await completeTask(task.id, "done");
184
+ const followUp = await createWorkerTaskFollowUp({
185
+ task: completed!,
186
+ status: "completed",
187
+ output: "done",
188
+ });
189
+ expect(followUp).not.toBeNull();
190
+ // The instruction is delivered in the follow-up's text, once.
191
+ expect(followUp!.task).toContain(ON_COMPLETED.onCompleted!);
192
+ expect(followUp!.followUpConfig).toBeUndefined();
193
+ });
194
+
195
+ test("send-task child with parentTaskId", async () => {
196
+ const parent = await workerTaskWithOnCompleted("original request");
197
+ const childId = sentTaskId(
198
+ await callTool(
199
+ sendTask,
200
+ {
201
+ task: "a new delegation in the same thread",
202
+ agentId: workerId,
203
+ routingReason: "skill",
204
+ routingNote: "worker owns this area of the codebase",
205
+ parentTaskId: parent.id,
206
+ },
207
+ leadId,
208
+ ),
209
+ );
210
+ expect(await followUpConfigOf(childId)).toBeUndefined();
211
+ });
212
+
213
+ test("send-task child keeps an explicit followUpConfig", async () => {
214
+ const parent = await workerTaskWithOnCompleted("original request 2");
215
+ const own: FollowUpConfig = { disabled: true };
216
+ const childId = sentTaskId(
217
+ await callTool(
218
+ sendTask,
219
+ {
220
+ task: "delegation that is awaited inline",
221
+ agentId: workerId,
222
+ routingReason: "skill",
223
+ routingNote: "worker owns this area of the codebase",
224
+ parentTaskId: parent.id,
225
+ followUpConfig: own,
226
+ },
227
+ leadId,
228
+ ),
229
+ );
230
+ expect(await followUpConfigOf(childId)).toEqual(own);
231
+ });
232
+
233
+ test("chain regression: worker task -> Lead follow-up -> send-task child", async () => {
234
+ const task = await workerTaskWithOnCompleted("tla-spec-sync build");
235
+ const completed = await completeTask(task.id, "built");
236
+ const followUp = await createWorkerTaskFollowUp({
237
+ task: completed!,
238
+ status: "completed",
239
+ output: "built",
240
+ });
241
+ expect(followUp).not.toBeNull();
242
+
243
+ const childId = sentTaskId(
244
+ await callTool(
245
+ sendTask,
246
+ {
247
+ task: "render the launch video",
248
+ agentId: workerId,
249
+ routingReason: "skill",
250
+ routingNote: "worker owns the video pipeline",
251
+ parentTaskId: followUp!.id,
252
+ },
253
+ leadId,
254
+ ),
255
+ );
256
+
257
+ expect(await followUpConfigOf(followUp!.id)).toBeUndefined();
258
+ expect(await followUpConfigOf(childId)).toBeUndefined();
259
+ });
260
+ });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Counterexamples from the TLA+ heartbeat model (specs/tla/heartbeat/).
3
+ *
4
+ * Each test replays one TLC trace against a real temp SQLite DB, calling the
5
+ * functions ACTIONS.md maps each trace step to. A test that passes on main
6
+ * means the trace was model drift, not a bug.
7
+ */
8
+
9
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
10
+ import { unlink } from "node:fs/promises";
11
+ import {
12
+ closeDb,
13
+ createAgent,
14
+ createTaskExtended,
15
+ getChildTasks,
16
+ getDbClient,
17
+ getTaskById,
18
+ initDb,
19
+ startTask,
20
+ supersedeTask,
21
+ } from "../be/db";
22
+ import {
23
+ codeLevelTriage,
24
+ runRebootSweep,
25
+ setBeforeHeartbeatSupersedeForTests,
26
+ } from "../heartbeat/heartbeat";
27
+
28
+ const TEST_DB_PATH = "./test-heartbeat-orphan-resume.sqlite";
29
+
30
+ describe("Heartbeat orphaned supersede repair (TLA+ counterexample)", () => {
31
+ beforeAll(async () => {
32
+ try {
33
+ await unlink(TEST_DB_PATH);
34
+ } catch {
35
+ // File doesn't exist
36
+ }
37
+ closeDb();
38
+ initDb(TEST_DB_PATH);
39
+ });
40
+
41
+ afterAll(async () => {
42
+ closeDb();
43
+ for (const path of [TEST_DB_PATH, `${TEST_DB_PATH}-wal`, `${TEST_DB_PATH}-shm`]) {
44
+ try {
45
+ await unlink(path);
46
+ } catch {
47
+ // Files may not exist
48
+ }
49
+ }
50
+ });
51
+
52
+ beforeEach(async () => {
53
+ setBeforeHeartbeatSupersedeForTests(null);
54
+ const db = getDbClient();
55
+ await db.run("DELETE FROM agent_tasks");
56
+ await db.run("DELETE FROM agents");
57
+ await db.run("DELETE FROM active_sessions");
58
+ });
59
+
60
+ // Trace: PollStart -> Age -> HbRead -> HbWrite -> ApiCrash -> ApiBoot (SupersededGetsResume).
61
+ test("a task superseded without a resume (crash before resume creation) gets a resume on the next sweep", async () => {
62
+ const agent = await createAgent({ name: "crashed-worker", isLead: false, status: "idle" });
63
+ const task = await createTaskExtended("Work interrupted mid-recovery", { agentId: agent.id });
64
+ await startTask(task.id);
65
+
66
+ // State the API process leaves behind if it dies between supersedeTask
67
+ // and createResumeFollowUp in remediateCrashedWorkerTask.
68
+ await supersedeTask(task.id, {
69
+ reason: "Auto-superseded by heartbeat: worker session not found (no active session for task)",
70
+ resumeTaskId: null,
71
+ });
72
+ await getDbClient().run("UPDATE agent_tasks SET finishedAt = ? WHERE id = ?", [
73
+ new Date(Date.now() - 10 * 60 * 1000).toISOString(),
74
+ task.id,
75
+ ]);
76
+
77
+ // Restarted API: boot sweep, then a regular sweep.
78
+ await runRebootSweep();
79
+ await codeLevelTriage();
80
+
81
+ expect((await getTaskById(task.id))?.status).toBe("superseded");
82
+ expect(await getChildTasks(task.id)).toHaveLength(1);
83
+ });
84
+ });
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Counterexamples from the TLA+ heartbeat model (specs/tla/heartbeat/).
3
+ *
4
+ * Each test replays one TLC trace against a real temp SQLite DB, calling the
5
+ * functions ACTIONS.md maps each trace step to. A test that passes on main
6
+ * means the trace was model drift, not a bug.
7
+ */
8
+
9
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
10
+ import { unlink } from "node:fs/promises";
11
+ import {
12
+ closeDb,
13
+ createAgent,
14
+ createTaskExtended,
15
+ getActiveSessionForTask,
16
+ getDbClient,
17
+ getTaskById,
18
+ initDb,
19
+ insertActiveSession,
20
+ startTask,
21
+ } from "../be/db";
22
+ import { runRebootSweep, setBeforeHeartbeatSupersedeForTests } from "../heartbeat/heartbeat";
23
+
24
+ const TEST_DB_PATH = "./test-heartbeat-reboot-live.sqlite";
25
+
26
+ async function backdateTask(taskId: string, isoTime: string): Promise<void> {
27
+ await getDbClient().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
28
+ isoTime,
29
+ taskId,
30
+ ]);
31
+ }
32
+
33
+ describe("Reboot sweep live sessions (TLA+ counterexample)", () => {
34
+ beforeAll(async () => {
35
+ try {
36
+ await unlink(TEST_DB_PATH);
37
+ } catch {
38
+ // File doesn't exist
39
+ }
40
+ closeDb();
41
+ initDb(TEST_DB_PATH);
42
+ });
43
+
44
+ afterAll(async () => {
45
+ closeDb();
46
+ for (const path of [TEST_DB_PATH, `${TEST_DB_PATH}-wal`, `${TEST_DB_PATH}-shm`]) {
47
+ try {
48
+ await unlink(path);
49
+ } catch {
50
+ // Files may not exist
51
+ }
52
+ }
53
+ });
54
+
55
+ beforeEach(async () => {
56
+ setBeforeHeartbeatSupersedeForTests(null);
57
+ const db = getDbClient();
58
+ await db.run("DELETE FROM agent_tasks");
59
+ await db.run("DELETE FROM agents");
60
+ await db.run("DELETE FROM active_sessions");
61
+ });
62
+
63
+ // Trace: PollStart -> RegisterSession -> ApiCrash -> ApiBoot -> RebootFail (NoLiveKill).
64
+ test("reboot sweep does not fail a live worker whose last tool call was shortly before the API restart", async () => {
65
+ const gs = globalThis as typeof globalThis & { __runId?: string };
66
+ const original = gs.__runId;
67
+ const bootTime = Date.now();
68
+ gs.__runId = `run_${bootTime}`;
69
+ try {
70
+ const agent = await createAgent({ name: "live-worker", isLead: false, status: "busy" });
71
+ const task = await createTaskExtended("Long model call", { agentId: agent.id });
72
+ await startTask(task.id);
73
+ await backdateTask(task.id, new Date(bootTime - 60_000).toISOString());
74
+
75
+ // Worker container kept running through the API deploy. Its last
76
+ // PostToolUse heartbeat landed 30s before the new API process booted.
77
+ await insertActiveSession({
78
+ agentId: agent.id,
79
+ taskId: task.id,
80
+ triggerType: "task_assigned",
81
+ });
82
+ await getDbClient().run("UPDATE active_sessions SET lastHeartbeatAt = ? WHERE taskId = ?", [
83
+ new Date(bootTime - 30_000).toISOString(),
84
+ task.id,
85
+ ]);
86
+
87
+ await runRebootSweep();
88
+
89
+ expect((await getTaskById(task.id))?.status).toBe("in_progress");
90
+ expect(await getActiveSessionForTask(task.id)).not.toBeNull();
91
+ } finally {
92
+ gs.__runId = original;
93
+ }
94
+ });
95
+ });