@desplega.ai/agent-swarm 2.4.2 → 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 (127) hide show
  1. package/dist/{acp-adapter-dv7hmx4r.js → acp-adapter-1885dpr7.js} +4 -4
  2. package/dist/{actions-51nc1gny.js → actions-pn76n32q.js} +18 -18
  3. package/dist/{amp-adapter-abp5tdx8.js → amp-adapter-af84rx1d.js} +3 -3
  4. package/dist/{app-9h14qvr6.js → app-6sv8mhzm.js} +3 -3
  5. package/dist/{assistant-wv1gz3mt.js → assistant-sc2tf825.js} +20 -20
  6. package/dist/{boot-reembed-7fcka4z5.js → boot-reembed-gvxjs2py.js} +1 -1
  7. package/dist/{boot-reembed-6yn53ms9.js → boot-reembed-y3m0ydzj.js} +1 -1
  8. package/dist/{boot-scrub-sweep-pm1580ax.js → boot-scrub-sweep-1j9mk05f.js} +3 -3
  9. package/dist/{boot-seeds-g5hfc1x8.js → boot-seeds-37sp626b.js} +4 -4
  10. package/dist/{claude-adapter-gf3kxhf0.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-s2k1qd8y.js → claude-sdk-session-53rcw5we.js} +9822 -9749
  13. package/dist/{cli-z4wd2ftv.js → cli-038fga5k.js} +2 -2
  14. package/dist/{cli-y2x52gs2.js → cli-0dxxwfkt.js} +8 -8
  15. package/dist/{cli-y8n5k515.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-r99kc9yq.js → cli-1ma335fz.js} +3 -3
  19. package/dist/{cli-faypxz7q.js → cli-1n9gdg56.js} +4 -4
  20. package/dist/{cli-shvfedvm.js → cli-1xsraagq.js} +2 -2
  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-bcj753sq.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-ftyypbg5.js → cli-6aen3ezb.js} +1 -1
  27. package/dist/{cli-efaqv1c7.js → cli-6eyncg0y.js} +6 -6
  28. package/dist/{cli-h38rjv87.js → cli-73ezfhha.js} +5 -5
  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-hyfw2j1e.js → cli-8y99kfey.js} +11 -11
  34. package/dist/{cli-ahtpy9kf.js → cli-aa3tb29r.js} +10 -5
  35. package/dist/{cli-fyzjmseb.js → cli-aypbg9r2.js} +1 -1
  36. package/dist/{cli-0qvtffe9.js → cli-bxrqvpg5.js} +6 -6
  37. package/dist/{cli-0hyhdkdk.js → cli-cebvbc9t.js} +4 -1
  38. package/dist/{cli-10hhg0km.js → cli-dm8p2dpf.js} +7 -7
  39. package/dist/{cli-8sxqsbbq.js → cli-e4d78xsv.js} +1 -1
  40. package/dist/{cli-7a91w22y.js → cli-e8xyjvfc.js} +1 -1
  41. package/dist/{cli-8zwe9mpk.js → cli-f0p0f6ja.js} +10 -10
  42. package/dist/{cli-w9g4sz81.js → cli-gf2850q7.js} +1 -1
  43. package/dist/{cli-n85dqg6d.js → cli-gx0j2khs.js} +4 -4
  44. package/dist/{cli-239yjk27.js → cli-h0mgxann.js} +5 -5
  45. package/dist/{cli-5rk36sjf.js → cli-kgnkyabk.js} +35 -34
  46. package/dist/{cli-hqhzg1qq.js → cli-mzkwkbdy.js} +1 -1
  47. package/dist/{cli-saw9a6f6.js → cli-npfe9t1s.js} +2 -2
  48. package/dist/{cli-t30yj95q.js → cli-p0qerd2r.js} +1 -1
  49. package/dist/{cli-s6vr7r2d.js → cli-pygdsgh8.js} +1 -1
  50. package/dist/{cli-2wag8973.js → cli-qeaq3j6t.js} +1 -1
  51. package/dist/{cli-7awf1fp8.js → cli-qp2m4btv.js} +3 -3
  52. package/dist/{cli-02qm7src.js → cli-s9rr4gr2.js} +5 -5
  53. package/dist/{cli-nmqdc2t3.js → cli-sds5zzs8.js} +1 -1
  54. package/dist/{cli-nn47d7e8.js → cli-tkf4x6ha.js} +15 -15
  55. package/dist/{cli-pkht5hap.js → cli-v87dr6e3.js} +2 -2
  56. package/dist/{cli-d3qqdy4c.js → cli-vcgscaan.js} +12 -12
  57. package/dist/{cli-zfg9enh9.js → cli-vfha21c9.js} +6 -1
  58. package/dist/{cli-h93c9vb3.js → cli-vq9gqarq.js} +1 -1
  59. package/dist/{cli-a4gvwttf.js → cli-wk28hzqn.js} +2 -2
  60. package/dist/{cli-rdyw3mct.js → cli-zvbb4n0e.js} +6 -5
  61. package/dist/cli.js +16 -16
  62. package/dist/{codex-adapter-ttygw382.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-60b58vv3.js → codex-session-runner-kbkz62vc.js} +2 -2
  65. package/dist/{commands-edmdwa4x.js → commands-hd574th6.js} +3 -3
  66. package/dist/{controller-m263fg43.js → controller-pz5mcfg4.js} +3 -3
  67. package/dist/{controllers-connect-5fym8sw1.js → controllers-connect-5rgq2hfk.js} +19 -19
  68. package/dist/{cursor-adapter-cz6nm6mj.js → cursor-adapter-mgaprq0j.js} +2 -2
  69. package/dist/{db-nn0hcmx5.js → db-6nf1207q.js} +2 -2
  70. package/dist/{db-2y0mgstc.js → db-h06dxp05.js} +3 -3
  71. package/dist/{dispatcher-0a9wwjcx.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-zhvt95n3.js → external-channel-x01t7ke6.js} +2 -2
  76. package/dist/{grok-adapter-qnr1hp2y.js → grok-adapter-h21jv594.js} +5 -5
  77. package/dist/handlers-2qjw434w.js +39 -0
  78. package/dist/{heartbeat-gz97r7ac.js → heartbeat-zzfksckz.js} +20 -20
  79. package/dist/{hook-x8rthca4.js → hook-3xfqhh2s.js} +2 -2
  80. package/dist/{hook-1mmdgjqs.js → hook-z4zhzj1f.js} +10 -5
  81. package/dist/{http-r8f85xr9.js → http-q813tmya.js} +46 -46
  82. package/dist/{index-y7rbcrc1.js → index-220tk9bw.js} +9 -9
  83. package/dist/index-d0g18bax.js +41 -0
  84. package/dist/{index-vzvfqzfj.js → index-m0a4ydqj.js} +17 -17
  85. package/dist/{index-aex0eba9.js → index-wbbs14c5.js} +17 -17
  86. package/dist/{index-smxyk04s.js → index-x8xbyn1t.js} +1 -1
  87. package/dist/{keepalive-64c0f34c.js → keepalive-y5t1my7m.js} +3 -3
  88. package/dist/{lead-b1da0f97.js → lead-2xwtq66b.js} +30 -30
  89. package/dist/{maintenance-mnxpar12.js → maintenance-ybfsaedv.js} +1 -1
  90. package/dist/{oauth-refresh-sweep-36hs2v4h.js → oauth-refresh-sweep-909jmn12.js} +2 -2
  91. package/dist/{onboard-xes8de61.js → onboard-xn6fkkys.js} +3 -3
  92. package/dist/{opencode-adapter-3s7ax02e.js → opencode-adapter-x4tqzhkh.js} +3 -3
  93. package/dist/{otel-impl-fzc57kcq.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-1nek2w7b.js → pricing-refresh-ygd362ep.js} +3 -3
  96. package/dist/{rbac-roles-ttyfaep2.js → rbac-roles-c9axvfnz.js} +3 -3
  97. package/dist/{rbac-roles-sqefb5hw.js → rbac-roles-mb470p4e.js} +2 -2
  98. package/dist/{render-v2-vb527jwn.js → render-v2-g0306nh1.js} +3 -3
  99. package/dist/{resume-jcdzg1bn.js → resume-v315hnq6.js} +5 -5
  100. package/dist/{session-access-zsp4wsp0.js → session-access-2zg285a1.js} +4 -4
  101. package/dist/{setup-1nb2zqd3.js → setup-a4mtf02k.js} +1 -1
  102. package/dist/{work-objects-61w08fwr.js → work-objects-9fr0fgtn.js} +21 -21
  103. package/dist/{worker-c1e68hpa.js → worker-qae22jeb.js} +30 -30
  104. package/openapi.json +1 -1
  105. package/package.json +2 -2
  106. package/src/be/db.ts +7 -4
  107. package/src/be/migrations/231_slack_tasks_last_updated_index.sql +5 -0
  108. package/src/github/handlers.ts +6 -5
  109. package/src/hooks/hook.ts +2 -1
  110. package/src/providers/amp-adapter.ts +1 -1
  111. package/src/providers/codex-adapter.ts +4 -4
  112. package/src/providers/opencode-adapter.ts +2 -2
  113. package/src/providers/pi-mono-extension.ts +2 -1
  114. package/src/tests/claude-stop-hook.test.ts +47 -0
  115. package/src/tests/codex-adapter.test.ts +42 -0
  116. package/src/tests/env-flag.test.ts +15 -1
  117. package/src/tests/github-handlers-cancel-config.test.ts +29 -8
  118. package/src/tests/github-handlers-inline-comments.test.ts +9 -2
  119. package/src/tests/script-connections.test.ts +2 -0
  120. package/src/tests/skill-script-refs.test.ts +63 -0
  121. package/src/tests/slack-render-v2-query-plans.test.ts +147 -0
  122. package/src/utils/env-flag.ts +9 -0
  123. package/templates/skills/apps/SKILL.md +1 -0
  124. package/templates/skills/apps/content.md +1 -0
  125. package/templates/skills/code-quality/content.md +9 -14
  126. package/dist/handlers-dkcarkd0.js +0 -39
  127. package/dist/index-5jecgp3n.js +0 -41
@@ -37,7 +37,7 @@ import {
37
37
  clampContextPercent,
38
38
  computeContextUsedUnified,
39
39
  } from "../utils/context-window";
40
- import { isEnvFlagEnabled } from "../utils/env-flag";
40
+ import { isEnvFlagEnabled, isSessionSummarySkipped } from "../utils/env-flag";
41
41
  import { SessionErrorTracker } from "../utils/error-tracker";
42
42
  import { summarizeSession as runSummarize } from "../utils/internal-ai";
43
43
  import { getMemoryRaterNames } from "../utils/memory-raters";
@@ -1739,8 +1739,8 @@ export class CodexSession implements ProviderSession {
1739
1739
  } finally {
1740
1740
  // Session-end summarization. Pure addition for codex — no behavior to
1741
1741
  // preserve. Wrapped in its own try/catch so summary failure must NOT
1742
- // block the existing log/AGENTS.md cleanup below. Gate `SKIP_SESSION_SUMMARY=1`
1743
- // matches the parity convention used by the claude Stop hook + pi/opencode.
1742
+ // block the existing log/AGENTS.md cleanup below. `SKIP_SESSION_SUMMARY` is
1743
+ // read with `isSessionSummarySkipped`, the same as the claude Stop hook + pi/opencode.
1744
1744
  //
1745
1745
  // Skip the summary entirely when the session was aborted. The transcript
1746
1746
  // is incomplete, the LLM call would retry 3× through openrouter and
@@ -1751,7 +1751,7 @@ export class CodexSession implements ProviderSession {
1751
1751
  this.aborted ||
1752
1752
  this.abortController?.signal.aborted === true ||
1753
1753
  this.pendingResult?.exitCode === 130;
1754
- if (process.env.SKIP_SESSION_SUMMARY !== "1" && !sessionWasAborted) {
1754
+ if (!isSessionSummarySkipped() && !sessionWasAborted) {
1755
1755
  try {
1756
1756
  await this.summarizeAtEnd();
1757
1757
  } catch (err) {
@@ -31,7 +31,7 @@ import {
31
31
  splitCredentialPool,
32
32
  validateOpencodeCredentials,
33
33
  } from "../utils/credentials";
34
- import { isEnvFlagEnabled } from "../utils/env-flag";
34
+ import { isEnvFlagEnabled, isSessionSummarySkipped } from "../utils/env-flag";
35
35
  import { fetchInstalledMcpServers } from "../utils/mcp-server-fetcher";
36
36
  import { swarmRuntimeInstanceId } from "../utils/multi-runtime";
37
37
  import {
@@ -797,7 +797,7 @@ export class OpencodeSession implements ProviderSession {
797
797
  /** Identity sync, session summary, and session close (Stop-hook parity). */
798
798
  private async runSessionEnd(signal: AbortSignal): Promise<void> {
799
799
  await sessionHooks.syncIdentity(this.swarm, "session_sync", signal);
800
- if (process.env.SKIP_SESSION_SUMMARY) {
800
+ if (isSessionSummarySkipped()) {
801
801
  console.debug("session_summary skipped (opencode): SKIP_SESSION_SUMMARY is set");
802
802
  } else {
803
803
  const transcript = this.transcript.join("\n").slice(-TRANSCRIPT_TAIL_CHARS);
@@ -10,6 +10,7 @@ import type { ExtensionFactory, McpServerConfig } from "@earendil-works/pi-codin
10
10
  import { buildRatingsFromLlm, fetchRetrievalsForTask, postRatings } from "../be/memory/raters/llm";
11
11
  import { checkToolLoop, clearToolHistory } from "../hooks/tool-loop-detection";
12
12
  import { renderGoalReminder } from "../prompts/goal-reminder";
13
+ import { isSessionSummarySkipped } from "../utils/env-flag";
13
14
  import { summarizeSession as runSummarize } from "../utils/internal-ai";
14
15
  import { getMemoryRaterNames } from "../utils/memory-raters";
15
16
  import { getOpenRouterAttributionHeaders } from "../utils/openrouter-base-url";
@@ -743,7 +744,7 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
743
744
 
744
745
  // Session summarization — get session file from context's session manager
745
746
  const sessionFile = ctx.sessionManager.getSessionFile?.();
746
- if (!process.env.SKIP_SESSION_SUMMARY) {
747
+ if (!isSessionSummarySkipped()) {
747
748
  await summarizeSessionForPi(config, sessionFile);
748
749
  } else {
749
750
  console.debug("session_summary skipped (pi): SKIP_SESSION_SUMMARY is set");
@@ -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
 
@@ -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 {
@@ -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
+ });
@@ -53,3 +53,12 @@ export function isEnvFlagEnabled(
53
53
  ): boolean {
54
54
  return parseEnvFlag(env[key], defaultValue);
55
55
  }
56
+
57
+ /**
58
+ * Whether the session-end summary is turned off via `SKIP_SESSION_SUMMARY`.
59
+ * Every harness (claude Stop hook, pi, opencode, codex) gates on this, so
60
+ * `1`/`true` skip and unset/empty/`0`/`false` keep the summary everywhere.
61
+ */
62
+ export function isSessionSummarySkipped(env: NodeJS.ProcessEnv = process.env): boolean {
63
+ return isEnvFlagEnabled("SKIP_SESSION_SUMMARY", false, env);
64
+ }
@@ -790,6 +790,7 @@ The tool returns `{ appId, url: "/apps/<id>" }`. Open that URL to verify the liv
790
790
  - Logged-in browser: a fresh dashboard has no connection and redirects to /setup. Seed the localStorage key `agent-swarm-connections` with `{connections:[{id,name,apiUrl,apiKey}],activeId}`. Build the value from an env var and pipe it to `agent-browser eval --stdin`, so the key never reaches a command line. Reload, then pick a user in the "Who are you?" dialog.
791
791
  - Apps scroll inside an inner canvas, so a full-page screenshot captures only the viewport. Scroll the canvas or raise the viewport height. For mobile, set a 375px-wide viewport before the screenshot.
792
792
  - App definitions: `GET /api/apps` and `GET /api/apps/<id>` with the API key as Bearer.
793
+ - Follow-up edit to a live app: list in the brief the control numbers the edit must not move, and assert them from the backing script before publishing. Take a signed-in screenshot of each changed selector at 1440px wide, save the shots to agent-fs, critique, and repeat at least 3 times. If the backing script caches results, bump its version or force a fresh run so the shots show new data.
793
794
 
794
795
 
795
796
  ### Rows from agents and scripts
@@ -785,6 +785,7 @@ The tool returns `{ appId, url: "/apps/<id>" }`. Open that URL to verify the liv
785
785
  - Logged-in browser: a fresh dashboard has no connection and redirects to /setup. Seed the localStorage key `agent-swarm-connections` with `{connections:[{id,name,apiUrl,apiKey}],activeId}`. Build the value from an env var and pipe it to `agent-browser eval --stdin`, so the key never reaches a command line. Reload, then pick a user in the "Who are you?" dialog.
786
786
  - Apps scroll inside an inner canvas, so a full-page screenshot captures only the viewport. Scroll the canvas or raise the viewport height. For mobile, set a 375px-wide viewport before the screenshot.
787
787
  - App definitions: `GET /api/apps` and `GET /api/apps/<id>` with the API key as Bearer.
788
+ - Follow-up edit to a live app: list in the brief the control numbers the edit must not move, and assert them from the backing script before publishing. Take a signed-in screenshot of each changed selector at 1440px wide, save the shots to agent-fs, critique, and repeat at least 3 times. If the backing script caches results, bump its version or force a fresh run so the shots show new data.
788
789
 
789
790
 
790
791
  ### Rows from agents and scripts
@@ -82,27 +82,22 @@ The review body is the verdict line: at most about 60 tokens. Each inline commen
82
82
  - Swarm PR, nit fixed: ``**Approve** at `3d4f34a` · 0 blocking · set `Risk: high` (billing) in the body``
83
83
  - Re-review: ``**Approve** at `661327f` · B1 fixed · N1 won't fix (author)``, new findings inline.
84
84
 
85
- Never on GitHub: test counts, CI lists (unless a red check changes the verdict), what you read, how the code works, or why GitHub refused a formal verdict. Those go to your task output and the ledger.
85
+ Never on GitHub: test counts, CI lists (unless a red check changes the verdict), what you read, how the code works, or why GitHub refused a formal verdict. Those go to your task output.
86
86
 
87
- ### Post through the gate
87
+ ### Post the review
88
88
 
89
- Nothing reaches GitHub without passing `review-post-gate`.
90
-
91
- 1. Draft the review as JSON: `{ pr: "<owner>/<repo>#<n>", head, swarmAuthored, risk, round, priorFindings: [{ id, state, text }], findings: [{ id?, tag, path?, line?, text, fix? }], body? }`. Write `fix` so it reads as done in the verdict line, for example "set `Risk: high` (billing) in the body". When you ran `code-reviewing`, its Standards/Spec report stays in your task output; only its findings go into the draft.
92
- 2. Run `script-run name="review-post-gate" scope="global" args=<draft>`. It checks token limits, the nit cap, tallies and CI lists, turns swarm-PR nits into `fixes`, and asks Jev to keep, drop or relabel each finding against the live `rules.md`. Every decision is logged with its reason. With `jev: "unavailable"` the deterministic result still stands.
93
- 3. Apply every entry in `fixes`, then post exactly `postable`, never your draft:
89
+ 1. Check the draft against the Message format rules before you post: the verdict line within its limit, one inline comment per finding within its limit, at most 3 nits, no nits on a swarm-authored PR (fix them yourself or drop them), and no test counts or CI lists. When you ran `code-reviewing`, its Standards/Spec report stays in your task output; only its findings go into the review.
90
+ 2. Post the verdict line and every inline comment as one review:
94
91
 
95
92
  ```bash
96
- jq -n --argjson p "$POSTABLE" --arg sha "$HEAD" \
97
- '{commit_id: $sha, event: $p.event, body: $p.body, comments: [$p.comments[] | {path, line, side: "RIGHT", body}]}' \
93
+ jq -n --arg sha "$HEAD" --arg event "$EVENT" --arg body "$VERDICT" --argjson comments "$COMMENTS" \
94
+ '{commit_id: $sha, event: $event, body: $body, comments: [$comments[] | {path, line, side: "RIGHT", body}]}' \
98
95
  | gh api repos/<owner>/<repo>/pulls/<n>/reviews --input -
99
96
  ```
100
97
 
101
- On a PR the reviewing account authored, the gate returns `event: "COMMENT"`, because GitHub refuses APPROVE and REQUEST_CHANGES there. Post it as is.
102
- 4. Record the evidence (tests run, controls, "checked, fine" notes, the gate's `logKey`) in your task output and with `script-run name="pr-review-ledger-write" scope="global"` (`action: "round"`). Never on the PR. If the ledger write fails, keep the exact payload and the error in your task output, report it to the lead once, and finish the review. Read the PR's existing ledger rounds before any retry so a round is never written twice.
103
- 5. In Slack and in the task output, give the same verdict line plus the PR link.
104
-
105
- If the gate cannot run, post nothing to GitHub. Keep the draft in your task output and report the gate failure (the error and the time) to the lead.
98
+ `EVENT` is `APPROVE` or `REQUEST_CHANGES`. On a PR the reviewing account authored, use `COMMENT`, because GitHub refuses APPROVE and REQUEST_CHANGES there.
99
+ 3. Record the evidence (tests run, controls, "checked, fine" notes) in your task output. Never on the PR.
100
+ 4. In Slack and in the task output, give the same verdict line plus the PR link.
106
101
 
107
102
  ### Capture pushback as a rule
108
103
 
@@ -1,39 +0,0 @@
1
- import {
2
- isUserAllowed2,
3
- registerMessageHandler2
4
- } from "./cli-10hhg0km.js";
5
- import"./cli-0qvtffe9.js";
6
- import"./cli-bcj753sq.js";
7
- import"./cli-5rk36sjf.js";
8
- import"./cli-smvcw9z5.js";
9
- import"./cli-sv5gb0yb.js";
10
- import"./cli-qz93y3x5.js";
11
- import"./cli-3h26v7yj.js";
12
- import"./cli-7a91w22y.js";
13
- import"./cli-saw9a6f6.js";
14
- import"./cli-h38rjv87.js";
15
- import"./cli-3ggwdtb0.js";
16
- import"./cli-s6vr7r2d.js";
17
- import"./cli-faypxz7q.js";
18
- import"./cli-h93c9vb3.js";
19
- import"./cli-qje9k2nh.js";
20
- import"./cli-y2x52gs2.js";
21
- import"./cli-02qm7src.js";
22
- import"./cli-8nyhf7za.js";
23
- import"./cli-ejs3m1sx.js";
24
- import"./cli-efaqv1c7.js";
25
- import"./cli-13p25grh.js";
26
- import"./cli-0adyhvq2.js";
27
- import"./cli-nn47d7e8.js";
28
- import"./cli-pkht5hap.js";
29
- import"./cli-n85dqg6d.js";
30
- import"./cli-nmqdc2t3.js";
31
- import"./cli-445c1vp7.js";
32
- import"./cli-kvzgak7c.js";
33
- import"./cli-7awf1fp8.js";
34
- import"./cli-d3qqdy4c.js";
35
- import"./cli-wt645v7q.js";
36
- export {
37
- isUserAllowed2 as isUserAllowed,
38
- registerMessageHandler2 as registerMessageHandler
39
- };
@@ -1,41 +0,0 @@
1
- import"./cli-ns59dc6r.js";
2
- import {
3
- startHeartbeat2,
4
- stopHeartbeat2
5
- } from "./cli-hyfw2j1e.js";
6
- import"./cli-8zwe9mpk.js";
7
- import"./cli-hqhzg1qq.js";
8
- import"./cli-5rk36sjf.js";
9
- import"./cli-smvcw9z5.js";
10
- import"./cli-sv5gb0yb.js";
11
- import"./cli-qz93y3x5.js";
12
- import"./cli-3h26v7yj.js";
13
- import"./cli-7a91w22y.js";
14
- import"./cli-saw9a6f6.js";
15
- import"./cli-h38rjv87.js";
16
- import"./cli-3ggwdtb0.js";
17
- import"./cli-s6vr7r2d.js";
18
- import"./cli-faypxz7q.js";
19
- import"./cli-h93c9vb3.js";
20
- import"./cli-qje9k2nh.js";
21
- import"./cli-y2x52gs2.js";
22
- import"./cli-02qm7src.js";
23
- import"./cli-8nyhf7za.js";
24
- import"./cli-ejs3m1sx.js";
25
- import"./cli-efaqv1c7.js";
26
- import"./cli-13p25grh.js";
27
- import"./cli-0adyhvq2.js";
28
- import"./cli-nn47d7e8.js";
29
- import"./cli-pkht5hap.js";
30
- import"./cli-n85dqg6d.js";
31
- import"./cli-nmqdc2t3.js";
32
- import"./cli-445c1vp7.js";
33
- import"./cli-kvzgak7c.js";
34
- import"./cli-b2mz5mjc.js";
35
- import"./cli-7awf1fp8.js";
36
- import"./cli-d3qqdy4c.js";
37
- import"./cli-wt645v7q.js";
38
- export {
39
- startHeartbeat2 as startHeartbeat,
40
- stopHeartbeat2 as stopHeartbeat
41
- };