@desplega.ai/agent-swarm 2.4.1 → 2.4.3

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 (153) hide show
  1. package/dist/{acp-adapter-zvzr538d.js → acp-adapter-1885dpr7.js} +4 -4
  2. package/dist/{actions-jhtqdw69.js → actions-pn76n32q.js} +18 -18
  3. package/dist/{amp-adapter-bd84rhs2.js → amp-adapter-af84rx1d.js} +3 -3
  4. package/dist/{app-kreq1gwg.js → app-6sv8mhzm.js} +3 -3
  5. package/dist/{assistant-3k22sbsq.js → assistant-sc2tf825.js} +20 -20
  6. package/dist/{boot-reembed-f6z923ny.js → boot-reembed-gvxjs2py.js} +1 -1
  7. package/dist/{boot-reembed-e53x6b9v.js → boot-reembed-y3m0ydzj.js} +1 -1
  8. package/dist/{boot-scrub-sweep-0538vsb7.js → boot-scrub-sweep-1j9mk05f.js} +3 -3
  9. package/dist/{boot-seeds-6wxqkfk2.js → boot-seeds-37sp626b.js} +4 -4
  10. package/dist/{claude-adapter-gfts5044.js → claude-adapter-rp0jtnwr.js} +4 -4
  11. package/dist/{claude-managed-setup-h00hsgtz.js → claude-managed-setup-rdt248k7.js} +1 -1
  12. package/dist/{claude-sdk-session-56besmfz.js → claude-sdk-session-53rcw5we.js} +9822 -9749
  13. package/dist/{cli-3tnpj2bh.js → cli-038fga5k.js} +2 -2
  14. package/dist/{cli-kmqgmcnb.js → cli-0dxxwfkt.js} +8 -8
  15. package/dist/{cli-471qk6s2.js → cli-1012xb7j.js} +28 -28
  16. package/dist/{cli-sv5gb0yb.js → cli-1eszr343.js} +1 -1
  17. package/dist/{cli-7225gwj7.js → cli-1m17dc9d.js} +1 -1
  18. package/dist/{cli-xftrvxag.js → cli-1ma335fz.js} +3 -3
  19. package/dist/{cli-s48yvzb8.js → cli-1n9gdg56.js} +4 -4
  20. package/dist/{cli-ash6xxk7.js → cli-1xsraagq.js} +3 -3
  21. package/dist/{cli-xbzvet96.js → cli-36ckw0wx.js} +5 -4
  22. package/dist/{cli-mhywgrrz.js → cli-4sydckyj.js} +1 -1
  23. package/dist/{cli-3r448dqn.js → cli-4yef8ghv.js} +3 -3
  24. package/dist/{cli-g0hgtz4p.js → cli-58vwn100.js} +3 -3
  25. package/dist/{cli-pw7v69vp.js → cli-625tb7aq.js} +1 -1
  26. package/dist/{cli-m2qgh5sv.js → cli-6aen3ezb.js} +1 -1
  27. package/dist/{cli-z071fb95.js → cli-6eyncg0y.js} +6 -6
  28. package/dist/{cli-0bh477zn.js → cli-73ezfhha.js} +120 -12
  29. package/dist/{cli-x5mq1fyq.js → cli-74c1wpm0.js} +3 -3
  30. package/dist/{cli-b1dfr1m4.js → cli-7a6p4e65.js} +1 -1
  31. package/dist/{cli-fpmsqq59.js → cli-836ehxmt.js} +1 -1
  32. package/dist/{cli-1pa3rz69.js → cli-8vverzf7.js} +1 -1
  33. package/dist/{cli-rdq7tzwm.js → cli-8y99kfey.js} +11 -11
  34. package/dist/{cli-z1ew8vbq.js → cli-aa3tb29r.js} +10 -5
  35. package/dist/{cli-6ncjvx55.js → cli-aypbg9r2.js} +1 -1
  36. package/dist/{cli-3kh4kpqy.js → cli-bxrqvpg5.js} +6 -6
  37. package/dist/{cli-0hyhdkdk.js → cli-cebvbc9t.js} +4 -1
  38. package/dist/{cli-p6argxh9.js → cli-dm8p2dpf.js} +7 -7
  39. package/dist/{cli-jh0qcrga.js → cli-e4d78xsv.js} +1 -1
  40. package/dist/{cli-7a91w22y.js → cli-e8xyjvfc.js} +1 -1
  41. package/dist/{cli-axh29tyr.js → cli-f0p0f6ja.js} +10 -10
  42. package/dist/{cli-w9g4sz81.js → cli-gf2850q7.js} +1 -1
  43. package/dist/{cli-bn5kevnf.js → cli-gx0j2khs.js} +4 -4
  44. package/dist/{cli-gf0kfg81.js → cli-h0mgxann.js} +5 -5
  45. package/dist/{cli-kr2pmfqx.js → cli-kgnkyabk.js} +85 -71
  46. package/dist/{cli-hqhzg1qq.js → cli-mzkwkbdy.js} +1 -1
  47. package/dist/{cli-k9z4zd4p.js → cli-npfe9t1s.js} +2 -2
  48. package/dist/{cli-syhq378v.js → cli-p0qerd2r.js} +1 -1
  49. package/dist/{cli-sjyetj21.js → cli-pygdsgh8.js} +1 -1
  50. package/dist/{cli-2wag8973.js → cli-qeaq3j6t.js} +1 -1
  51. package/dist/{cli-nx462e4v.js → cli-qp2m4btv.js} +3 -3
  52. package/dist/{cli-tbhwrxpt.js → cli-s9rr4gr2.js} +5 -5
  53. package/dist/{cli-fz9kewbh.js → cli-sds5zzs8.js} +1 -1
  54. package/dist/{cli-vy0fay6k.js → cli-tkf4x6ha.js} +15 -15
  55. package/dist/{cli-mfe7f7b5.js → cli-v87dr6e3.js} +2 -2
  56. package/dist/{cli-rqqsfz30.js → cli-vcgscaan.js} +91 -21
  57. package/dist/{cli-zfg9enh9.js → cli-vfha21c9.js} +6 -1
  58. package/dist/{cli-3p8fqcnh.js → cli-vq9gqarq.js} +1 -1
  59. package/dist/{cli-a4gvwttf.js → cli-wk28hzqn.js} +2 -2
  60. package/dist/{cli-p0rakbrk.js → cli-zvbb4n0e.js} +6 -5
  61. package/dist/cli.js +17 -17
  62. package/dist/{codex-adapter-p6rmm7vv.js → codex-adapter-2zrmab9b.js} +2 -2
  63. package/dist/{codex-hook-x7nejyrr.js → codex-hook-e2he7aca.js} +2 -2
  64. package/dist/{codex-session-runner-8vd5t9t0.js → codex-session-runner-kbkz62vc.js} +2 -2
  65. package/dist/{commands-7ems2p13.js → commands-hd574th6.js} +3 -3
  66. package/dist/{controller-m263fg43.js → controller-pz5mcfg4.js} +3 -3
  67. package/dist/{controllers-connect-bg33ae53.js → controllers-connect-5rgq2hfk.js} +19 -19
  68. package/dist/{cursor-adapter-cz6nm6mj.js → cursor-adapter-mgaprq0j.js} +2 -2
  69. package/dist/{db-w505dwa8.js → db-6nf1207q.js} +6 -2
  70. package/dist/{db-ys2rbdft.js → db-h06dxp05.js} +3 -3
  71. package/dist/{dispatcher-0s0mckqr.js → dispatcher-zthpy4m1.js} +17 -17
  72. package/dist/{dsh-adapter-3n3net33.js → dsh-adapter-64dhw0j7.js} +2 -2
  73. package/dist/{e2b-thdcxtjd.js → e2b-16d6ch39.js} +1 -1
  74. package/dist/{e2b-7am6tmq6.js → e2b-zn0b4w5w.js} +1 -1
  75. package/dist/{external-channel-5b23fv8z.js → external-channel-x01t7ke6.js} +2 -2
  76. package/dist/{grok-adapter-q9fghe5q.js → grok-adapter-h21jv594.js} +5 -5
  77. package/dist/handlers-2qjw434w.js +39 -0
  78. package/dist/{heartbeat-tww6yx1w.js → heartbeat-zzfksckz.js} +20 -20
  79. package/dist/{hook-pvmh9b6m.js → hook-3xfqhh2s.js} +2 -2
  80. package/dist/{hook-dx0pkvar.js → hook-z4zhzj1f.js} +10 -5
  81. package/dist/{http-k2ejbt05.js → http-q813tmya.js} +121 -75
  82. package/dist/{index-2z710v4x.js → index-220tk9bw.js} +9 -9
  83. package/dist/index-d0g18bax.js +41 -0
  84. package/dist/{index-dgz61nff.js → index-m0a4ydqj.js} +17 -17
  85. package/dist/{index-h2ek08d6.js → index-wbbs14c5.js} +17 -17
  86. package/dist/{index-x5aktmp8.js → index-x8xbyn1t.js} +1 -1
  87. package/dist/{keepalive-nhvpw10z.js → keepalive-y5t1my7m.js} +3 -3
  88. package/dist/{lead-jb6sf69z.js → lead-2xwtq66b.js} +30 -30
  89. package/dist/{maintenance-agakc519.js → maintenance-ybfsaedv.js} +1 -1
  90. package/dist/{oauth-refresh-sweep-4dcbhtn0.js → oauth-refresh-sweep-909jmn12.js} +2 -2
  91. package/dist/{onboard-4n2kwa4v.js → onboard-xn6fkkys.js} +3 -3
  92. package/dist/{opencode-adapter-3s7ax02e.js → opencode-adapter-x4tqzhkh.js} +3 -3
  93. package/dist/{otel-impl-tfa12080.js → otel-impl-e6mn0z13.js} +1 -1
  94. package/dist/{pi-mono-adapter-ytp7gz8b.js → pi-mono-adapter-mcfkn9vf.js} +3 -3
  95. package/dist/{pricing-refresh-3cqx3f3x.js → pricing-refresh-ygd362ep.js} +3 -3
  96. package/dist/{rbac-roles-9fae5rnv.js → rbac-roles-c9axvfnz.js} +3 -3
  97. package/dist/{rbac-roles-y6zbfzve.js → rbac-roles-mb470p4e.js} +2 -2
  98. package/dist/{render-v2-emawjvc4.js → render-v2-g0306nh1.js} +3 -3
  99. package/dist/{resume-zhgts7h7.js → resume-v315hnq6.js} +5 -5
  100. package/dist/{session-access-cm823e06.js → session-access-2zg285a1.js} +4 -4
  101. package/dist/{setup-tkp99w1e.js → setup-a4mtf02k.js} +1 -1
  102. package/dist/{work-objects-f722h263.js → work-objects-9fr0fgtn.js} +21 -21
  103. package/dist/{worker-p7f336rq.js → worker-qae22jeb.js} +30 -30
  104. package/openapi.json +393 -1
  105. package/package.json +3 -3
  106. package/src/apps/catalog.generated.json +3 -2
  107. package/src/apps/definition.ts +152 -4
  108. package/src/apps/row-store.ts +19 -7
  109. package/src/apps/schema-migrate.ts +14 -6
  110. package/src/apps/script-types.ts +1 -0
  111. package/src/apps/sync.ts +20 -19
  112. package/src/be/db/tasks/read.ts +112 -0
  113. package/src/be/db.ts +9 -4
  114. package/src/be/migrations/231_slack_tasks_last_updated_index.sql +5 -0
  115. package/src/be/swarm-config-guard.ts +15 -0
  116. package/src/github/handlers.ts +6 -5
  117. package/src/hooks/hook.ts +2 -1
  118. package/src/http/apps.ts +15 -2
  119. package/src/http/status.ts +49 -43
  120. package/src/http/tasks.ts +47 -0
  121. package/src/providers/acp-targets.ts +1 -1
  122. package/src/providers/amp-adapter.ts +1 -1
  123. package/src/providers/codex-adapter.ts +4 -4
  124. package/src/providers/opencode-adapter.ts +2 -2
  125. package/src/providers/pi-mono-extension.ts +2 -1
  126. package/src/telemetry.ts +15 -3
  127. package/src/tests/app-row-tools.test.ts +25 -0
  128. package/src/tests/apps-json-column.test.ts +611 -0
  129. package/src/tests/apps-script-types.test.ts +2 -0
  130. package/src/tests/apps-sync-engine.test.ts +42 -0
  131. package/src/tests/claude-stop-hook.test.ts +47 -0
  132. package/src/tests/codex-adapter.test.ts +42 -0
  133. package/src/tests/db-task-read-contract.test.ts +4 -0
  134. package/src/tests/db-task-write-contract.test.ts +2 -0
  135. package/src/tests/docs-index-footprint.test.js +2 -2
  136. package/src/tests/env-flag.test.ts +15 -1
  137. package/src/tests/github-handlers-cancel-config.test.ts +29 -8
  138. package/src/tests/github-handlers-inline-comments.test.ts +9 -2
  139. package/src/tests/runner-harness-telemetry-env.test.ts +6 -0
  140. package/src/tests/script-connections.test.ts +2 -0
  141. package/src/tests/skill-script-refs.test.ts +63 -0
  142. package/src/tests/slack-render-v2-query-plans.test.ts +147 -0
  143. package/src/tests/status.test.ts +158 -9
  144. package/src/tests/task-lineage.test.ts +180 -0
  145. package/src/tests/telemetry-init.test.ts +62 -5
  146. package/src/tests/user-token-rest-auth.test.ts +38 -0
  147. package/src/tools/app-rows.ts +5 -0
  148. package/src/utils/env-flag.ts +9 -0
  149. package/templates/skills/apps/SKILL.md +4 -2
  150. package/templates/skills/apps/content.md +4 -2
  151. package/templates/skills/code-quality/content.md +9 -14
  152. package/dist/handlers-mqrym347.js +0 -39
  153. package/dist/index-pqfwh9nn.js +0 -41
@@ -371,6 +371,53 @@ describe("runStopHookSessionSummary", () => {
371
371
  expect(consoleErrors.length).toBe(0);
372
372
  });
373
373
 
374
+ test.each(["true", "TRUE"])("SKIP_SESSION_SUMMARY=%s → no runSummarize call", async (value) => {
375
+ const transcriptPath = await writeTempTranscript(longTranscript());
376
+
377
+ let runSummarizeCalled = false;
378
+ const deps: RunStopHookSessionSummaryDeps = {
379
+ runSummarize: async () => {
380
+ runSummarizeCalled = true;
381
+ throw new Error("should not be called");
382
+ },
383
+ };
384
+
385
+ await runStopHookSessionSummary(
386
+ {
387
+ agentId: "agent-claude-1",
388
+ transcriptPath,
389
+ env: makeEnv({ SKIP_SESSION_SUMMARY: value }),
390
+ },
391
+ deps,
392
+ );
393
+
394
+ expect(runSummarizeCalled).toBe(false);
395
+ expect(fetchCalls.length).toBe(0);
396
+ });
397
+
398
+ test.each(["0", "false"])("SKIP_SESSION_SUMMARY=%s → summary still runs", async (value) => {
399
+ const transcriptPath = await writeTempTranscript(longTranscript());
400
+
401
+ let runSummarizeCalled = false;
402
+ const deps: RunStopHookSessionSummaryDeps = {
403
+ runSummarize: async () => {
404
+ runSummarizeCalled = true;
405
+ return null;
406
+ },
407
+ };
408
+
409
+ await runStopHookSessionSummary(
410
+ {
411
+ agentId: "agent-claude-1",
412
+ transcriptPath,
413
+ env: makeEnv({ SKIP_SESSION_SUMMARY: value }),
414
+ },
415
+ deps,
416
+ );
417
+
418
+ expect(runSummarizeCalled).toBe(true);
419
+ });
420
+
374
421
  test("short transcript (≤100 chars) → no runSummarize call, no POST", async () => {
375
422
  const transcriptPath = await writeTempTranscript("tiny");
376
423
 
@@ -2294,6 +2294,48 @@ describe("CodexSession session-end summarization", () => {
2294
2294
  expect(indexCalls.length).toBe(0);
2295
2295
  });
2296
2296
 
2297
+ test.each(["true", "TRUE"])("SKIP_SESSION_SUMMARY=%s → no runSummarize call", async (value) => {
2298
+ process.env.SKIP_SESSION_SUMMARY = value;
2299
+ const events = buildSummaryEvents();
2300
+
2301
+ let runSummarizeCalls = 0;
2302
+ const deps: SummarizeSessionForCodexDeps = {
2303
+ runSummarize: async () => {
2304
+ runSummarizeCalls += 1;
2305
+ return null;
2306
+ },
2307
+ };
2308
+
2309
+ const config = testConfig({
2310
+ logFile: join(tmpLogDir, `skip-${value}.log`),
2311
+ cwd: "",
2312
+ });
2313
+ await runSessionWithFakeThreadAndDeps(events, config, deps);
2314
+
2315
+ expect(runSummarizeCalls).toBe(0);
2316
+ });
2317
+
2318
+ test.each(["0", "false"])("SKIP_SESSION_SUMMARY=%s → summary still runs", async (value) => {
2319
+ process.env.SKIP_SESSION_SUMMARY = value;
2320
+ const events = buildSummaryEvents();
2321
+
2322
+ let runSummarizeCalls = 0;
2323
+ const deps: SummarizeSessionForCodexDeps = {
2324
+ runSummarize: async () => {
2325
+ runSummarizeCalls += 1;
2326
+ return null;
2327
+ },
2328
+ };
2329
+
2330
+ const config = testConfig({
2331
+ logFile: join(tmpLogDir, `no-skip-${value}.log`),
2332
+ cwd: "",
2333
+ });
2334
+ await runSessionWithFakeThreadAndDeps(events, config, deps);
2335
+
2336
+ expect(runSummarizeCalls).toBe(1);
2337
+ });
2338
+
2297
2339
  test("missing taskId → no runSummarize call, no POST", async () => {
2298
2340
  const events = buildSummaryEvents();
2299
2341
 
@@ -111,6 +111,7 @@ value:ReservedAgentRoleError
111
111
  value:SESSION_ACTIVITY_REFRESH_MIN_INTERVAL_MS
112
112
  value:SKILL_FILE_LIMITS
113
113
  value:SUPERSEDED_RESUME_REPAIR_WINDOW_MS
114
+ value:TASK_LINEAGE_LIMITS
114
115
  value:UNATTRIBUTED_USER_ID
115
116
  value:__resetSqliteVecExtensionPathCacheForTests
116
117
  value:abandonSlackOutcomeDelivery
@@ -412,6 +413,7 @@ value:getSystemDefaultSkills
412
413
  value:getTaskAttachments
413
414
  value:getTaskById
414
415
  value:getTaskByWorkflowRunStepId
416
+ value:getTaskLineage
415
417
  value:getTaskStats
416
418
  value:getTasksByAgentId
417
419
  value:getTasksByStatus
@@ -612,6 +614,7 @@ type:ExistingTrackerContextWorkReason
612
614
  type:PendingChatRelay
613
615
  type:TaskFilters
614
616
  value:SUPERSEDED_RESUME_REPAIR_WINDOW_MS
617
+ value:TASK_LINEAGE_LIMITS
615
618
  value:findCompletedTaskInThread
616
619
  value:findExistingLinearTrackerContextWork
617
620
  value:findRecentCancelledTaskInThread
@@ -639,6 +642,7 @@ value:getSlackTasksMissingTree
639
642
  value:getSupersededTasksWithUnsettledDependents
640
643
  value:getSupersededTasksWithoutResume
641
644
  value:getTaskById
645
+ value:getTaskLineage
642
646
  value:getTaskStats
643
647
  value:getTasksByAgentId
644
648
  value:getTasksByStatus
@@ -134,6 +134,7 @@ value:ReservedAgentRoleError
134
134
  value:SESSION_ACTIVITY_REFRESH_MIN_INTERVAL_MS
135
135
  value:SKILL_FILE_LIMITS
136
136
  value:SUPERSEDED_RESUME_REPAIR_WINDOW_MS
137
+ value:TASK_LINEAGE_LIMITS
137
138
  value:UNATTRIBUTED_USER_ID
138
139
  value:__resetSqliteVecExtensionPathCacheForTests
139
140
  value:abandonSlackOutcomeDelivery
@@ -435,6 +436,7 @@ value:getSystemDefaultSkills
435
436
  value:getTaskAttachments
436
437
  value:getTaskById
437
438
  value:getTaskByWorkflowRunStepId
439
+ value:getTaskLineage
438
440
  value:getTaskStats
439
441
  value:getTasksByAgentId
440
442
  value:getTasksByStatus
@@ -103,8 +103,8 @@ describe("docs API index footprint", () => {
103
103
  expect(spec.paths["/api/agents/{id}/runtime"]?.get).toBeDefined();
104
104
 
105
105
  const generatedKeys = await generatedOperationKeys();
106
- expect(operationKeys).toHaveLength(436);
107
- expect(generatedKeys).toHaveLength(436);
106
+ expect(operationKeys).toHaveLength(437);
107
+ expect(generatedKeys).toHaveLength(437);
108
108
  expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
109
109
  });
110
110
 
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { validateConfigValue } from "../be/swarm-config-guard";
3
- import { isEnvFlagEnabled, parseEnvFlag } from "../utils/env-flag";
3
+ import { isEnvFlagEnabled, isSessionSummarySkipped, parseEnvFlag } from "../utils/env-flag";
4
4
 
5
5
  describe("parseEnvFlag", () => {
6
6
  test("accepts both truthy serializations", () => {
@@ -199,3 +199,17 @@ describe("swarm-config-guard: Configuration-page value validation", () => {
199
199
  expect(validateConfigValue("SOME_RANDOM_KEY", "whatever")).toBeNull();
200
200
  });
201
201
  });
202
+
203
+ describe("isSessionSummarySkipped", () => {
204
+ test.each(["1", "true", "TRUE", " True "])("SKIP_SESSION_SUMMARY=%p skips", (value) => {
205
+ expect(isSessionSummarySkipped({ SKIP_SESSION_SUMMARY: value })).toBe(true);
206
+ });
207
+
208
+ test.each(["0", "false", "FALSE", ""])("SKIP_SESSION_SUMMARY=%p keeps the summary", (value) => {
209
+ expect(isSessionSummarySkipped({ SKIP_SESSION_SUMMARY: value })).toBe(false);
210
+ });
211
+
212
+ test("unset keeps the summary", () => {
213
+ expect(isSessionSummarySkipped({})).toBe(false);
214
+ });
215
+ });
@@ -6,8 +6,8 @@
6
6
  * github.cancelOnUnassign — PR unassigned + issue unassigned
7
7
  * github.cancelOnReviewRequestRemoved — PR review_request_removed
8
8
  *
9
- * Absent / any value ≠ "false" → cancel (current behavior, default).
10
- * Value "false" → leave task untouched, return { created: false }.
9
+ * Absent / empty / unrecognized values → cancel (default).
10
+ * Values "false" / "0" (case-insensitive, trimmed) → leave task untouched.
11
11
  */
12
12
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
13
13
  import { unlink } from "node:fs/promises";
@@ -122,8 +122,15 @@ describe("PR unassigned — github.cancelOnUnassign", () => {
122
122
  expect(await getTaskStatus(taskId)).toBe("failed");
123
123
  });
124
124
 
125
- test("config = 'false': unassign leaves task untouched", async () => {
126
- await setConfigFlag("github.cancelOnUnassign", "false");
125
+ test.each([
126
+ "false",
127
+ "FALSE",
128
+ "False",
129
+ "0",
130
+ " false ",
131
+ " 0 ",
132
+ ])("config = %j: unassign leaves task untouched", async (value) => {
133
+ await setConfigFlag("github.cancelOnUnassign", value);
127
134
  const taskId = await seedTask(BASE_PR.number, "pr");
128
135
  expect(await getTaskStatus(taskId)).toBe("pending");
129
136
 
@@ -162,8 +169,15 @@ describe("issue unassigned — github.cancelOnUnassign", () => {
162
169
  expect(await getTaskStatus(taskId)).toBe("failed");
163
170
  });
164
171
 
165
- test("config = 'false': unassign leaves task untouched", async () => {
166
- await setConfigFlag("github.cancelOnUnassign", "false");
172
+ test.each([
173
+ "false",
174
+ "FALSE",
175
+ "False",
176
+ "0",
177
+ " false ",
178
+ " 0 ",
179
+ ])("config = %j: unassign leaves task untouched", async (value) => {
180
+ await setConfigFlag("github.cancelOnUnassign", value);
167
181
  const taskId = await seedTask(BASE_ISSUE.number, "issue");
168
182
  expect(await getTaskStatus(taskId)).toBe("pending");
169
183
 
@@ -201,8 +215,15 @@ describe("PR review_request_removed — github.cancelOnReviewRequestRemoved", ()
201
215
  expect(await getTaskStatus(taskId)).toBe("failed");
202
216
  });
203
217
 
204
- test("config = 'false': review removal leaves task untouched", async () => {
205
- await setConfigFlag("github.cancelOnReviewRequestRemoved", "false");
218
+ test.each([
219
+ "false",
220
+ "FALSE",
221
+ "False",
222
+ "0",
223
+ " false ",
224
+ " 0 ",
225
+ ])("config = %j: review removal leaves task untouched", async (value) => {
226
+ await setConfigFlag("github.cancelOnReviewRequestRemoved", value);
206
227
  const taskId = await seedTask(BASE_PR.number, "pr");
207
228
  expect(await getTaskStatus(taskId)).toBe("pending");
208
229
 
@@ -573,9 +573,16 @@ describe("no-op approved reviews", () => {
573
573
  }
574
574
  });
575
575
 
576
- test("GITHUB_SKIP_NOOP_APPROVALS=false: empty-body approval still creates a task", async () => {
576
+ test.each([
577
+ "false",
578
+ "FALSE",
579
+ "False",
580
+ "0",
581
+ " false ",
582
+ " 0 ",
583
+ ])("GITHUB_SKIP_NOOP_APPROVALS=%j: empty-body approval still creates a task", async (value) => {
577
584
  const previous = process.env.GITHUB_SKIP_NOOP_APPROVALS;
578
- process.env.GITHUB_SKIP_NOOP_APPROVALS = "false";
585
+ process.env.GITHUB_SKIP_NOOP_APPROVALS = value;
579
586
  const fetchSpy = mockFetchForApproval({ reviews: [APPROVED_BY_REVIEWER] });
580
587
 
581
588
  try {
@@ -11,6 +11,12 @@ const TELEMETRY_PATH = join(import.meta.dir, "..", "telemetry.ts");
11
11
  // leaves the process), and print the environment the event carries.
12
12
  const BOOT_SCRIPT = `
13
13
  const t = await import(${JSON.stringify(TELEMETRY_PATH)});
14
+ const { mkdtempSync, rmSync } = await import("node:fs");
15
+ const { tmpdir } = await import("node:os");
16
+ const { pathToFileURL } = await import("node:url");
17
+ const root = mkdtempSync(tmpdir() + "/telemetry-package-");
18
+ t.getTelemetryEnvironment(pathToFileURL(root + "/"));
19
+ rmSync(root, { recursive: true, force: true });
14
20
  let body;
15
21
  globalThis.fetch = async (_url, init) => {
16
22
  body = JSON.parse(init.body);
@@ -634,6 +634,8 @@ describe("script connections", () => {
634
634
  markMigrationApplied(database, "220_model_profiles.sql");
635
635
  // 227 alters inbox_item_state, which this migration-112-only fixture does not create.
636
636
  markMigrationApplied(database, "227_inbox_item_state_last_read_at.sql");
637
+ // 231 indexes agent_tasks columns that this fixture's id-only agent_tasks lacks.
638
+ markMigrationApplied(database, "231_slack_tasks_last_updated_index.sql");
637
639
 
638
640
  const id = crypto.randomUUID();
639
641
  const now = new Date().toISOString();
@@ -0,0 +1,63 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { join, relative } from "node:path";
3
+ import { SEED_SCRIPTS } from "../be/seed-scripts";
4
+
5
+ // A shipped skill that tells agents to `script-run` a named script must name a
6
+ // script this repo ships in SEED_SCRIPTS. Otherwise the skill works only on a
7
+ // swarm that happens to hold that script in its catalog, and every other
8
+ // deployment hits "Script not found" at that step.
9
+
10
+ const REPO_ROOT = join(import.meta.dir, "../..");
11
+ // Seeded skills, baked plugin commands/agents/skills, and public operator skills.
12
+ const SKILL_DIRS = ["templates/skills", "plugin", "skills"].map((d) => join(REPO_ROOT, d));
13
+
14
+ // `script-run name="x"` and the prose forms "`script-run` with name `x`" / "`script-run` on `x`".
15
+ const REF_PATTERNS = [
16
+ /script-run\s+name=["']([a-z0-9][a-z0-9-]*)["']/g,
17
+ /`script-run`\s+(?:with\s+name|on)\s+`([a-z0-9][a-z0-9-]*)`/g,
18
+ ];
19
+
20
+ function findScriptRefs(text: string): string[] {
21
+ const names = new Set<string>();
22
+ for (const re of REF_PATTERNS) {
23
+ for (const m of text.matchAll(re)) if (m[1]) names.add(m[1]);
24
+ }
25
+ return [...names];
26
+ }
27
+
28
+ async function collectRefs(): Promise<{ file: string; name: string }[]> {
29
+ const refs: { file: string; name: string }[] = [];
30
+ for (const dir of SKILL_DIRS) {
31
+ for await (const path of new Bun.Glob("**/*.md").scan({ cwd: dir, absolute: true })) {
32
+ const text = await Bun.file(path).text();
33
+ for (const name of findScriptRefs(text)) refs.push({ file: relative(REPO_ROOT, path), name });
34
+ }
35
+ }
36
+ return refs;
37
+ }
38
+
39
+ describe("shipped skills only script-run scripts the repo ships", () => {
40
+ test("the reference scanner matches each call form and ignores prose", () => {
41
+ expect(
42
+ findScriptRefs(
43
+ [
44
+ 'Run `script-run name="internal-only-gate" scope="global" args=<draft>`.',
45
+ "Run `script-run` with name `boot-triage`.",
46
+ "Preview with `script-run` on `heartbeat-jev-gate`.",
47
+ "Seed scripts (`script-run` with `name` and `args`):",
48
+ "Inline `script-run` for a one-off.",
49
+ ].join("\n"),
50
+ ).sort(),
51
+ ).toEqual(["boot-triage", "heartbeat-jev-gate", "internal-only-gate"]);
52
+ });
53
+
54
+ test("every script-run name in a shipped skill is in SEED_SCRIPTS", async () => {
55
+ const refs = await collectRefs();
56
+ // Positive control: the scan must see heartbeat-runbook's boot-triage reference, or it is blind.
57
+ expect(refs.some((r) => r.name === "boot-triage")).toBe(true);
58
+
59
+ const shipped = new Set(SEED_SCRIPTS.map((s) => s.name));
60
+ const missing = refs.filter((r) => !shipped.has(r.name)).map((r) => `${r.file}: ${r.name}`);
61
+ expect(missing).toEqual([]);
62
+ });
63
+ });
@@ -0,0 +1,147 @@
1
+ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
2
+ import {
3
+ closeDb,
4
+ createAgent,
5
+ createTaskExtended,
6
+ getDb,
7
+ getDbClient,
8
+ getResolvableDeferralOutcomes,
9
+ initDb,
10
+ recordSlackMessage,
11
+ } from "../be/db";
12
+ import { getSlackTasksMissingTree } from "../be/db/tasks/read";
13
+ import { completeTask, startTask } from "../be/db/tasks/write";
14
+
15
+ // Both queries run on every Slack watcher tick. The DB has no sqlite_stat1, so
16
+ // the planner works from heuristics; these pin the plans that keep each tick
17
+ // proportional to Slack rows instead of every task.
18
+
19
+ async function capturedSql(run: () => Promise<unknown>): Promise<string> {
20
+ const client = getDbClient();
21
+ const seen: string[] = [];
22
+ const original = client.query.bind(client);
23
+ const spy = spyOn(client, "query").mockImplementation(((sql: string, params?: unknown[]) => {
24
+ seen.push(sql);
25
+ return original(sql, params);
26
+ }) as typeof client.query);
27
+ try {
28
+ await run();
29
+ } finally {
30
+ spy.mockRestore();
31
+ }
32
+ expect(seen).toHaveLength(1);
33
+ return seen[0]!;
34
+ }
35
+
36
+ function plan(sql: string, params: unknown[] = []): string {
37
+ return getDb()
38
+ .query<{ detail: string }, any[]>(`EXPLAIN QUERY PLAN ${sql}`)
39
+ .all(...params)
40
+ .map((row) => row.detail)
41
+ .join("\n");
42
+ }
43
+
44
+ async function deferredAsk(label: string, leadId: string) {
45
+ const ask = await createTaskExtended(`ask ${label}`, {
46
+ agentId: leadId,
47
+ source: "slack",
48
+ slackChannelId: `C_${label}`,
49
+ slackThreadTs: `${label}.0`,
50
+ });
51
+ await startTask(ask.id);
52
+ await completeTask(ask.id, "Checking back later", {
53
+ addTags: ["deferred"],
54
+ deferredAt: new Date().toISOString(),
55
+ });
56
+ const card = await recordSlackMessage({
57
+ contextKey: `slack:C_${label}:${label}.0`,
58
+ channelId: `C_${label}`,
59
+ threadTs: `${label}.0`,
60
+ ts: `${label}.1`,
61
+ kind: "outcome",
62
+ taskId: ask.id,
63
+ finalized: true,
64
+ });
65
+ return { ask, card };
66
+ }
67
+
68
+ async function wake(parentTaskId: string, leadId: string, opts: { deferAgain?: boolean } = {}) {
69
+ await Bun.sleep(2);
70
+ const task = await createTaskExtended("wake", {
71
+ agentId: leadId,
72
+ parentTaskId,
73
+ taskType: "deferred",
74
+ });
75
+ await startTask(task.id);
76
+ await completeTask(
77
+ task.id,
78
+ "done",
79
+ opts.deferAgain ? { deferredAt: new Date().toISOString() } : {},
80
+ );
81
+ return task;
82
+ }
83
+
84
+ describe("slack render-v2 watcher queries", () => {
85
+ beforeEach(() => {
86
+ initDb(":memory:");
87
+ getDb().run(
88
+ "INSERT OR IGNORE INTO slack_render_v2_state (id, activated_at) VALUES (1, '2000-01-01T00:00:00.000Z')",
89
+ );
90
+ });
91
+
92
+ afterEach(() => {
93
+ closeDb();
94
+ });
95
+
96
+ test("deferral resolution drives from the chain, not from every task", async () => {
97
+ const sql = await capturedSql(() => getResolvableDeferralOutcomes());
98
+ const detail = plan(sql, [50]);
99
+ expect(detail).toContain(
100
+ "SEARCH wake USING INDEX idx_agent_tasks_parentTaskId (parentTaskId=?)",
101
+ );
102
+ expect(detail).not.toContain("idx_agent_tasks_taskType");
103
+ expect(detail).not.toContain("idx_agent_tasks_status_last_updated");
104
+ });
105
+
106
+ test("missing-tree scan reads only Slack tasks", async () => {
107
+ const sql = await capturedSql(() => getSlackTasksMissingTree());
108
+ const detail = plan(sql);
109
+ expect(detail).toContain("SCAN task USING INDEX idx_agent_tasks_slack_last_updated");
110
+ expect(detail).not.toContain("idx_agent_tasks_last_updated");
111
+ });
112
+
113
+ test("deferral resolution returns the latest terminal wake of each open chain", async () => {
114
+ const lead = await createAgent({ name: "Plan Lead", isLead: true, status: "idle" });
115
+
116
+ const single = await deferredAsk("SINGLE", lead.id);
117
+ const singleWake = await wake(single.ask.id, lead.id);
118
+
119
+ const chained = await deferredAsk("CHAIN", lead.id);
120
+ const firstWake = await wake(chained.ask.id, lead.id, { deferAgain: true });
121
+ const lastWake = await wake(firstWake.id, lead.id);
122
+
123
+ const pending = await deferredAsk("PENDING", lead.id);
124
+ const running = await createTaskExtended("still running", {
125
+ agentId: lead.id,
126
+ parentTaskId: pending.ask.id,
127
+ taskType: "deferred",
128
+ });
129
+ await startTask(running.id);
130
+
131
+ const plainChild = await deferredAsk("PLAIN", lead.id);
132
+ const notDeferred = await createTaskExtended("ordinary child", {
133
+ agentId: lead.id,
134
+ parentTaskId: plainChild.ask.id,
135
+ });
136
+ await startTask(notDeferred.id);
137
+ await completeTask(notDeferred.id, "done");
138
+
139
+ const rows = await getResolvableDeferralOutcomes();
140
+ expect(rows.map((row) => [row.card.id, row.deferredTaskId, row.wakeTaskId]).sort()).toEqual(
141
+ [
142
+ [single.card.id, single.ask.id, singleWake.id],
143
+ [chained.card.id, chained.ask.id, lastWake.id],
144
+ ].sort(),
145
+ );
146
+ });
147
+ });