@desplega.ai/agent-swarm 2.5.1 → 2.5.2

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 (155) hide show
  1. package/dist/{acp-adapter-7g6bpwbk.js → acp-adapter-csh0zz3b.js} +1 -1
  2. package/dist/{actions-w8xd014k.js → actions-6xx079d1.js} +17 -17
  3. package/dist/{amp-adapter-z4qvhacj.js → amp-adapter-8n6f0hz8.js} +2 -2
  4. package/dist/{app-nndcvftq.js → app-ft6r6v23.js} +3 -3
  5. package/dist/{assistant-hjmqyscx.js → assistant-vt2cyfzd.js} +18 -18
  6. package/dist/{boot-reembed-r8rhf9h8.js → boot-reembed-r3e808ky.js} +1 -1
  7. package/dist/{boot-reembed-vwcj2y5d.js → boot-reembed-tgrfy27t.js} +1 -1
  8. package/dist/{boot-scrub-sweep-fr9q36ne.js → boot-scrub-sweep-masdwxkm.js} +2 -2
  9. package/dist/{boot-seeds-9jqwnegq.js → boot-seeds-0n82qc7q.js} +4 -4
  10. package/dist/{claude-adapter-2f8b85df.js → claude-adapter-s7j47k06.js} +2 -2
  11. package/dist/{claude-managed-setup-c3hp36ph.js → claude-managed-setup-3hzrgrnk.js} +1 -1
  12. package/dist/{claude-sdk-session-05kat8rv.js → claude-sdk-session-0zrdzyx2.js} +6905 -6855
  13. package/dist/{cli-mz2ygeyq.js → cli-0vgy5wfw.js} +2 -2
  14. package/dist/{cli-eym3jg7w.js → cli-14v1sxxy.js} +6 -6
  15. package/dist/{cli-e9btqw4y.js → cli-15xd0j7w.js} +18 -13
  16. package/dist/{cli-v2g93k4v.js → cli-3e9vs93g.js} +4 -4
  17. package/dist/{cli-1dp1zj04.js → cli-4qkg9968.js} +3 -3
  18. package/dist/{cli-ba8ne1es.js → cli-4rvr7zfx.js} +10 -0
  19. package/dist/{cli-rngde2jv.js → cli-6y43v79h.js} +1 -1
  20. package/dist/{cli-z63a269p.js → cli-7ccf7akh.js} +1 -1
  21. package/dist/{cli-z2dv25mf.js → cli-86zpfje5.js} +116 -5
  22. package/dist/{cli-fes51ya2.js → cli-aaqnqpyw.js} +2 -2
  23. package/dist/{cli-qfm6my86.js → cli-b1tv22wr.js} +1 -1
  24. package/dist/{cli-xc65x52q.js → cli-b63d08rs.js} +1 -1
  25. package/dist/{cli-gwcf52gz.js → cli-b6tnjdvx.js} +11 -7
  26. package/dist/{cli-9mm5ade4.js → cli-bpbcpd8b.js} +1 -1
  27. package/dist/{cli-qfjm4gvt.js → cli-cf63dnxj.js} +2 -2
  28. package/dist/{cli-q9pzwxvk.js → cli-csh8fnbg.js} +82 -16
  29. package/dist/{cli-0jy85zz4.js → cli-cyfgxnvb.js} +7 -7
  30. package/dist/{cli-y3zyaee5.js → cli-d172svcw.js} +5 -5
  31. package/dist/{cli-mq7kg1b1.js → cli-e91vshb1.js} +41 -6
  32. package/dist/{cli-cah5detr.js → cli-edtm741g.js} +5 -5
  33. package/dist/{cli-ajqhxtnz.js → cli-f3pfn4x3.js} +1 -1
  34. package/dist/{cli-hadv0725.js → cli-f55b6ky0.js} +1 -1
  35. package/dist/{cli-r9fy0ddc.js → cli-fgn7yg5r.js} +1 -1
  36. package/dist/{cli-kcsd1ez8.js → cli-h5d22pfs.js} +249 -68
  37. package/dist/{cli-2wba1ac9.js → cli-ha22h0eg.js} +20 -20
  38. package/dist/{cli-pyy4c9ch.js → cli-j3dwd4kz.js} +173 -134
  39. package/dist/{cli-j1eghqfm.js → cli-jmcjbjkt.js} +1 -1
  40. package/dist/{cli-22rmcjkg.js → cli-kcnwn85g.js} +4 -3
  41. package/dist/{cli-rh5smt8t.js → cli-mntxtkhn.js} +12 -2
  42. package/dist/{cli-2jg61wab.js → cli-n2918sed.js} +9 -9
  43. package/dist/{cli-3ee5z0q0.js → cli-qrjv7t18.js} +1 -1
  44. package/dist/{cli-sn1vbtg2.js → cli-vaxw3bg0.js} +3 -3
  45. package/dist/{cli-cwyafhw1.js → cli-wadzapwb.js} +1 -1
  46. package/dist/{cli-jpxsrxcb.js → cli-xw9q173h.js} +1 -1
  47. package/dist/{cli-znvq0k3t.js → cli-xzge46sj.js} +2 -2
  48. package/dist/{cli-nj12tn2c.js → cli-y3dzwyy3.js} +3 -3
  49. package/dist/{cli-xd4eqtr2.js → cli-ydg6ep51.js} +1 -1
  50. package/dist/cli.js +16 -15
  51. package/dist/{codex-adapter-p4ptwar5.js → codex-adapter-brh2tcxr.js} +1 -1
  52. package/dist/{codex-session-runner-964c7dnk.js → codex-session-runner-0dnj21qs.js} +1 -1
  53. package/dist/{commands-mbvhbrcy.js → commands-1cnvseqx.js} +2 -2
  54. package/dist/{controller-gnq1k1mf.js → controller-kc7s9xhn.js} +2 -2
  55. package/dist/{controllers-connect-6dkm71tg.js → controllers-connect-jzx7t4bn.js} +18 -18
  56. package/dist/{cursor-adapter-yq5xgfcg.js → cursor-adapter-qpwp5sfg.js} +1 -1
  57. package/dist/{db-af9y8nkf.js → db-4vq88jkc.js} +3 -3
  58. package/dist/{db-88wcymxt.js → db-v5ytg5pw.js} +1 -1
  59. package/dist/{dispatcher-606ydx5v.js → dispatcher-trtcrqae.js} +16 -16
  60. package/dist/{dsh-adapter-tvhc4k2d.js → dsh-adapter-f20xwn1z.js} +1 -1
  61. package/dist/{external-channel-y8q5z8z2.js → external-channel-n9bymc2n.js} +2 -2
  62. package/dist/{grok-adapter-39aha52t.js → grok-adapter-4kpmmnr6.js} +2 -2
  63. package/dist/{handlers-rk9dqech.js → handlers-ptsraqa8.js} +19 -19
  64. package/dist/{heartbeat-jfe9zfjr.js → heartbeat-60qtxtjf.js} +19 -19
  65. package/dist/{hook-ffevzed1.js → hook-ngtvh729.js} +1 -1
  66. package/dist/{hook-b1mez1m8.js → hook-rbktcstg.js} +1 -1
  67. package/dist/{http-d5b44f1r.js → http-z8xy72gp.js} +334 -86
  68. package/dist/{index-nq8hhv28.js → index-06qkmcx6.js} +10 -9
  69. package/dist/{index-d2qeem97.js → index-1cscynkb.js} +16 -16
  70. package/dist/{index-5en23s5z.js → index-a9nsavhx.js} +1 -1
  71. package/dist/{index-4q3vj1yr.js → index-r70q0xjx.js} +16 -16
  72. package/dist/{index-mj46rk9k.js → index-s193j5fh.js} +19 -19
  73. package/dist/{keepalive-wsby1btq.js → keepalive-a9srcphj.js} +3 -3
  74. package/dist/{lead-paa141j6.js → lead-3devneqy.js} +26 -26
  75. package/dist/{maintenance-vnshcyv9.js → maintenance-0er16h2y.js} +1 -1
  76. package/dist/{oauth-refresh-sweep-d6sqn5np.js → oauth-refresh-sweep-b65jcyw2.js} +2 -2
  77. package/dist/{onboard-nr4xhpzy.js → onboard-wqywz5py.js} +2 -2
  78. package/dist/{otel-impl-ytn9wyv9.js → otel-impl-vk32s884.js} +1 -1
  79. package/dist/{pricing-refresh-pva04zee.js → pricing-refresh-t27anv4z.js} +3 -3
  80. package/dist/rbac-roles-452665y0.js +26 -0
  81. package/dist/{rbac-roles-7rc1q3g4.js → rbac-roles-bmcgsxjn.js} +12 -2
  82. package/dist/{render-v2-pyfsg9zk.js → render-v2-0ttsk2j4.js} +3 -3
  83. package/dist/{resume-phj8xhqr.js → resume-a26kvr41.js} +5 -5
  84. package/dist/{session-access-jzx2z9h5.js → session-access-dgh9nkwk.js} +4 -4
  85. package/dist/{setup-rfg0f0sg.js → setup-9vc9qahm.js} +1 -1
  86. package/dist/{work-objects-44z2yjg8.js → work-objects-vn8nkxwn.js} +19 -19
  87. package/dist/{worker-zsfqzxhm.js → worker-1x1h8vnm.js} +26 -26
  88. package/openapi.json +463 -3
  89. package/package.json +4 -3
  90. package/src/be/db/session-tree.ts +89 -2
  91. package/src/be/db/session-visibility.ts +31 -0
  92. package/src/be/db/tasks/read.ts +18 -5
  93. package/src/be/db.ts +2 -0
  94. package/src/be/migrations/233_script_run_session_tokens.sql +8 -0
  95. package/src/be/migrations/235_chat_relay_defer.sql +86 -0
  96. package/src/be/migrations/237_session_inbox_cleanup.sql +75 -0
  97. package/src/be/rbac-roles.ts +138 -0
  98. package/src/be/scripts/run-credential.ts +64 -0
  99. package/src/be/scripts/run-saved.ts +3 -2
  100. package/src/be/users.ts +28 -4
  101. package/src/http/all-routes.ts +1 -0
  102. package/src/http/approval-requests.ts +46 -6
  103. package/src/http/index.ts +2 -0
  104. package/src/http/schedules.ts +26 -0
  105. package/src/http/scripts.ts +2 -2
  106. package/src/http/sessions.ts +93 -2
  107. package/src/http/user-roles.ts +173 -0
  108. package/src/http/users.ts +9 -0
  109. package/src/http/x.ts +2 -2
  110. package/src/integrations/chat-sdk/commands.ts +14 -1
  111. package/src/integrations/chat-sdk/inbound.ts +40 -5
  112. package/src/integrations/chat-sdk/outbound.ts +35 -10
  113. package/src/integrations/chat-sdk/reactions.ts +107 -0
  114. package/src/integrations/chat-sdk/relay.ts +75 -7
  115. package/src/integrations/chat-sdk/thread-buffer.ts +2 -0
  116. package/src/integrations/reaction-events.ts +20 -0
  117. package/src/providers/amp-adapter.ts +1 -1
  118. package/src/rbac/legacy-policy.ts +3 -0
  119. package/src/rbac/permissions.ts +8 -0
  120. package/src/scheduler/schedule-task.ts +44 -3
  121. package/src/scheduler/scheduler.ts +4 -4
  122. package/src/scripts-runtime/loader.ts +8 -1
  123. package/src/slack/ack.ts +2 -1
  124. package/src/slack/reaction-shortcode.ts +4 -7
  125. package/src/tasks/worker-follow-up.ts +30 -0
  126. package/src/tests/approval-requests-notifications.test.ts +189 -0
  127. package/src/tests/chat-sdk-commands.test.ts +12 -0
  128. package/src/tests/chat-sdk-inbound.test.ts +163 -9
  129. package/src/tests/chat-sdk-outbound-files.test.ts +9 -2
  130. package/src/tests/chat-sdk-relay.test.ts +361 -10
  131. package/src/tests/chat-sdk-tools.test.ts +104 -0
  132. package/src/tests/docs-index-footprint.test.js +3 -3
  133. package/src/tests/http-users.test.ts +38 -0
  134. package/src/tests/migration-237-session-inbox-cleanup.test.ts +136 -0
  135. package/src/tests/rbac-engine.test.ts +2 -0
  136. package/src/tests/rbac-roles.test.ts +10 -0
  137. package/src/tests/schedule-http-triage-tools.test.ts +44 -0
  138. package/src/tests/script-api-credential.test.ts +173 -0
  139. package/src/tests/script-connections.test.ts +4 -0
  140. package/src/tests/script-run-config-scope.test.ts +120 -0
  141. package/src/tests/script-run-token.test.ts +75 -0
  142. package/src/tests/scripts-http.test.ts +19 -0
  143. package/src/tests/sessions-tree.test.ts +242 -0
  144. package/src/tests/user-roles-http.test.ts +204 -0
  145. package/src/tests/workflow-hitl-dynamic-questions.test.ts +1 -1
  146. package/src/tools/chat-reply.ts +2 -0
  147. package/src/tools/schedules/patch-schedule.ts +18 -0
  148. package/src/tools/schedules/update-schedule.ts +18 -0
  149. package/src/types.ts +16 -0
  150. package/src/workflows/approval-notifications.ts +214 -0
  151. package/src/workflows/executors/human-in-the-loop.ts +11 -172
  152. package/src/workflows/executors/swarm-script.ts +2 -2
  153. package/templates/schedules/weekly-harness-upgrade-check/content.md +1 -1
  154. package/templates/skills/code-quality/content.md +1 -1
  155. package/dist/rbac-roles-ff1ffmec.js +0 -12
@@ -3,14 +3,228 @@ import {
3
3
  claimApprovalCancellationNotification,
4
4
  completeApprovalCancellationNotificationClaim,
5
5
  createTaskExtended,
6
+ getApprovalRequestById,
6
7
  getTaskById,
7
8
  releaseApprovalCancellationNotificationClaim,
9
+ updateApprovalRequestNotifications,
8
10
  } from "../be/db";
9
11
  import { resolveTemplate } from "../prompts/resolver";
10
12
  import { getSlackApp } from "../slack/app";
13
+ import { resolveSlackConversationId } from "../slack/dm";
14
+ import {
15
+ externalChannelToolError,
16
+ getChannelBlockReason,
17
+ isBlockExternalEnabled,
18
+ } from "../slack/external-channel";
11
19
  import { TERMINAL_TASK_STATUSES } from "../types";
20
+ import { getAppUrl } from "../utils/constants";
12
21
  import { scrubSecrets } from "../utils/secret-scrubber";
13
22
 
23
+ /** Slack Block Kit caps a section block's text at 3000 characters. */
24
+ const SLACK_SECTION_TEXT_LIMIT = 3000;
25
+
26
+ function escapeSlackMrkdwn(text: string): string {
27
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
28
+ }
29
+
30
+ /**
31
+ * Render the Slack "Questions" section text within Block Kit's 3000-character
32
+ * section limit. Labels are escaped so upstream text cannot inject mentions or
33
+ * links. When the list does not fit, the tail is replaced by a count; every
34
+ * question stays answerable on the dashboard page the card's button opens.
35
+ */
36
+ export function buildSlackQuestionsSummary(
37
+ questions: ReadonlyArray<{ label: string }>,
38
+ timeoutText = "",
39
+ ): string {
40
+ const header = "*Questions:*\n";
41
+ const lines = questions.map((q) => `• ${escapeSlackMrkdwn(q.label.replace(/\s+/g, " ").trim())}`);
42
+ const fits = (shown: string[], omitted: number) => {
43
+ const tail =
44
+ omitted > 0 ? `\n_…and ${omitted} more. Answer all of them on the dashboard._` : "";
45
+ const text = `${header}${shown.join("\n")}${tail}${timeoutText}`;
46
+ return text.length <= SLACK_SECTION_TEXT_LIMIT ? text : null;
47
+ };
48
+
49
+ const full = fits(lines, 0);
50
+ if (full) return full;
51
+ for (let count = lines.length - 1; count >= 0; count--) {
52
+ const text = fits(lines.slice(0, count), lines.length - count);
53
+ if (text) return text;
54
+ }
55
+ return `${header}_${questions.length} questions. Answer them on the dashboard._`;
56
+ }
57
+
58
+ function formatTimeout(seconds: number): string {
59
+ if (seconds < 60) return `${seconds}s`;
60
+ if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
61
+ const hours = Math.floor(seconds / 3600);
62
+ const mins = Math.round((seconds % 3600) / 60);
63
+ return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
64
+ }
65
+
66
+ export interface ApprovalNotificationChannel {
67
+ channel: "slack" | "email";
68
+ target: string;
69
+ }
70
+
71
+ export interface ApprovalNotificationsInput {
72
+ requestId: string;
73
+ title: string;
74
+ questions: ReadonlyArray<{ label: string }>;
75
+ timeoutSeconds?: number;
76
+ notifications: ReadonlyArray<ApprovalNotificationChannel>;
77
+ }
78
+
79
+ /** The DB calls the dispatch makes; the HITL executor injects its own. */
80
+ export interface ApprovalNotificationsDb {
81
+ updateApprovalRequestNotifications: typeof updateApprovalRequestNotifications;
82
+ getApprovalRequestById: typeof getApprovalRequestById;
83
+ }
84
+
85
+ /**
86
+ * Post the "Approval Required" card for each notification channel and store
87
+ * the Slack `messageTs` on the request. Used by the workflow HITL executor and
88
+ * by `POST /api/approval-requests`, which checks the caller may post to Slack.
89
+ * Callers fire and forget: a Slack failure is logged and never fails the
90
+ * request. Email is not supported yet and is skipped.
91
+ */
92
+ export async function dispatchApprovalNotifications(
93
+ input: ApprovalNotificationsInput,
94
+ db: ApprovalNotificationsDb = { updateApprovalRequestNotifications, getApprovalRequestById },
95
+ ): Promise<void> {
96
+ const { requestId, title, questions, timeoutSeconds, notifications } = input;
97
+ if (!notifications.length) return;
98
+
99
+ const approvalUrl = `${getAppUrl()}/approval-requests/${requestId}`;
100
+ const updatedChannels: Array<ApprovalNotificationChannel & { messageTs?: string }> = [
101
+ ...notifications,
102
+ ];
103
+ let updated = false;
104
+
105
+ for (let i = 0; i < notifications.length; i++) {
106
+ const notification = notifications[i]!;
107
+
108
+ if (notification.channel === "email") {
109
+ console.warn(`[HITL] Email notifications not yet supported (target: ${notification.target})`);
110
+ continue;
111
+ }
112
+
113
+ if (notification.channel === "slack") {
114
+ try {
115
+ const slackApp = getSlackApp();
116
+ if (!slackApp) {
117
+ console.warn("[HITL] Slack not initialized — cannot send notification");
118
+ continue;
119
+ }
120
+
121
+ // Same guard as the Slack tools: never post into a channel shared with another org.
122
+ if (isBlockExternalEnabled()) {
123
+ const conversationId = await resolveSlackConversationId(
124
+ slackApp.client,
125
+ notification.target,
126
+ );
127
+ const blockReason = await getChannelBlockReason(slackApp.client, conversationId);
128
+ if (blockReason) {
129
+ console.warn(`[HITL] ${externalChannelToolError(conversationId, blockReason)}`);
130
+ continue;
131
+ }
132
+ }
133
+
134
+ const questionsText = buildSlackQuestionsSummary(questions);
135
+
136
+ // The deadline is a caption under the button, not part of the ask.
137
+ const timeoutCaption =
138
+ timeoutSeconds !== undefined
139
+ ? [
140
+ {
141
+ type: "context",
142
+ elements: [
143
+ {
144
+ type: "mrkdwn",
145
+ text: `⏱ Timeout: ${formatTimeout(timeoutSeconds)} — times out if not responded`,
146
+ },
147
+ ],
148
+ },
149
+ ]
150
+ : [];
151
+
152
+ const blocks = [
153
+ {
154
+ type: "section",
155
+ text: {
156
+ type: "mrkdwn",
157
+ text: `🔔 *Approval Required: ${escapeSlackMrkdwn(title)}*`,
158
+ },
159
+ },
160
+ {
161
+ type: "section",
162
+ text: {
163
+ type: "mrkdwn",
164
+ text: questionsText,
165
+ },
166
+ },
167
+ {
168
+ type: "actions",
169
+ elements: [
170
+ {
171
+ type: "button",
172
+ text: { type: "plain_text", text: "Review & Respond" },
173
+ url: approvalUrl,
174
+ style: "primary",
175
+ },
176
+ ],
177
+ },
178
+ ...timeoutCaption,
179
+ ];
180
+
181
+ const result = await slackApp.client.chat.postMessage({
182
+ channel: notification.target,
183
+ text: `Approval Required: ${escapeSlackMrkdwn(title)} — ${approvalUrl}`,
184
+ unfurl_links: false,
185
+ unfurl_media: false,
186
+ // biome-ignore lint/suspicious/noExplicitAny: Block Kit objects
187
+ blocks: blocks as any,
188
+ });
189
+
190
+ if (result.ts) {
191
+ updatedChannels[i] = { ...notification, messageTs: result.ts };
192
+ updated = true;
193
+ }
194
+
195
+ console.log(
196
+ `[HITL] Slack notification sent to ${notification.target} for request ${requestId}`,
197
+ );
198
+ } catch (err) {
199
+ console.error(
200
+ `[HITL] Failed to send Slack notification to ${notification.target}:`,
201
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
202
+ );
203
+ }
204
+ }
205
+ }
206
+
207
+ // Persist messageTs values back to DB
208
+ if (updated) {
209
+ try {
210
+ await db.updateApprovalRequestNotifications(requestId, updatedChannels);
211
+ // A cancel that landed while the card was posting found no messageTs to thread under.
212
+ const latest = await db.getApprovalRequestById(requestId);
213
+ if (latest?.status === "cancelled") {
214
+ await postApprovalCancellationUpdates(
215
+ [latest],
216
+ latest.resolutionReason ?? "Workflow run cancelled",
217
+ );
218
+ }
219
+ } catch (err) {
220
+ console.error(
221
+ "[HITL] Failed to update notification channels in DB:",
222
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
223
+ );
224
+ }
225
+ }
226
+ }
227
+
14
228
  export interface ApprovalSlackClient {
15
229
  chat: {
16
230
  postMessage(input: {
@@ -1,8 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import type { ExecutorMeta } from "../../types";
3
- import { getAppUrl } from "../../utils/constants";
4
3
  import { scrubSecrets } from "../../utils/secret-scrubber";
5
- import { postApprovalCancellationUpdates } from "../approval-notifications";
4
+ import { dispatchApprovalNotifications } from "../approval-notifications";
6
5
  import type { ExecutorInput, ExecutorResult } from "./base";
7
6
  import { BaseExecutor } from "./base";
8
7
 
@@ -88,9 +87,6 @@ const DYNAMIC_QUESTIONS_TOKEN_RE = /^\{\{[^}]+\}\}$/;
88
87
  */
89
88
  export const MAX_HITL_QUESTIONS = 100;
90
89
 
91
- /** Slack Block Kit caps a section block's text at 3000 characters. */
92
- const SLACK_SECTION_TEXT_LIMIT = 3000;
93
-
94
90
  const MAX_REPORTED_ISSUES = 5;
95
91
 
96
92
  const HITLConfigSchema = z.object({
@@ -197,38 +193,6 @@ export function resolveHitlQuestions(
197
193
  return { ok: true, questions };
198
194
  }
199
195
 
200
- function escapeSlackMrkdwn(text: string): string {
201
- return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
202
- }
203
-
204
- /**
205
- * Render the Slack "Questions" section text within Block Kit's 3000-character
206
- * section limit. Labels are escaped so upstream text cannot inject mentions or
207
- * links. When the list does not fit, the tail is replaced by a count; every
208
- * question stays answerable on the dashboard page the card's button opens.
209
- */
210
- export function buildSlackQuestionsSummary(
211
- questions: ReadonlyArray<{ label: string }>,
212
- timeoutText = "",
213
- ): string {
214
- const header = "*Questions:*\n";
215
- const lines = questions.map((q) => `• ${escapeSlackMrkdwn(q.label.replace(/\s+/g, " ").trim())}`);
216
- const fits = (shown: string[], omitted: number) => {
217
- const tail =
218
- omitted > 0 ? `\n_…and ${omitted} more. Answer all of them on the dashboard._` : "";
219
- const text = `${header}${shown.join("\n")}${tail}${timeoutText}`;
220
- return text.length <= SLACK_SECTION_TEXT_LIMIT ? text : null;
221
- };
222
-
223
- const full = fits(lines, 0);
224
- if (full) return full;
225
- for (let count = lines.length - 1; count >= 0; count--) {
226
- const text = fits(lines.slice(0, count), lines.length - count);
227
- if (text) return text;
228
- }
229
- return `${header}_${questions.length} questions. Answer them on the dashboard._`;
230
- }
231
-
232
196
  // ─── Executor ───────────────────────────────────────────────
233
197
 
234
198
  export class HumanInTheLoopExecutor extends BaseExecutor<
@@ -319,7 +283,16 @@ export class HumanInTheLoopExecutor extends BaseExecutor<
319
283
 
320
284
  // 3. Dispatch notifications (fire-and-forget — failures must not block the workflow)
321
285
  if (config.notifications?.length) {
322
- this.dispatchNotifications(requestId, config, db).catch((err) => {
286
+ dispatchApprovalNotifications(
287
+ {
288
+ requestId,
289
+ title: config.title,
290
+ questions: config.questions,
291
+ timeoutSeconds: config.timeout?.seconds,
292
+ notifications: config.notifications,
293
+ },
294
+ db,
295
+ ).catch((err) => {
323
296
  console.error(
324
297
  "[HITL] Unexpected error dispatching notifications:",
325
298
  scrubSecrets(err instanceof Error ? err.message : String(err)),
@@ -335,138 +308,4 @@ export class HumanInTheLoopExecutor extends BaseExecutor<
335
308
  correlationId: requestId,
336
309
  } as ExecutorResult<HITLOutput>;
337
310
  }
338
-
339
- /** Dispatch notifications for each configured channel. Updates DB with messageTs on success. */
340
- private async dispatchNotifications(
341
- requestId: string,
342
- config: z.infer<typeof HITLConfigSchema>,
343
- db: typeof import("../../be/db"),
344
- ): Promise<void> {
345
- if (!config.notifications?.length) return;
346
-
347
- const approvalUrl = `${getAppUrl()}/approval-requests/${requestId}`;
348
- const updatedChannels = [...config.notifications] as Array<
349
- z.infer<typeof NotificationConfigSchema> & { messageTs?: string }
350
- >;
351
- let updated = false;
352
-
353
- for (let i = 0; i < config.notifications.length; i++) {
354
- const notification = config.notifications[i]!;
355
-
356
- if (notification.channel === "email") {
357
- console.warn(
358
- `[HITL] Email notifications not yet supported (target: ${notification.target})`,
359
- );
360
- continue;
361
- }
362
-
363
- if (notification.channel === "slack") {
364
- try {
365
- const { getSlackApp } = await import("../../slack/app");
366
- const slackApp = getSlackApp();
367
- if (!slackApp) {
368
- console.warn("[HITL] Slack not initialized — cannot send notification");
369
- continue;
370
- }
371
-
372
- const questions = Array.isArray(config.questions) ? config.questions : [];
373
- const questionsText = buildSlackQuestionsSummary(questions);
374
-
375
- // The deadline is a caption under the button, not part of the ask.
376
- const timeoutCaption = config.timeout
377
- ? [
378
- {
379
- type: "context",
380
- elements: [
381
- {
382
- type: "mrkdwn",
383
- text: `⏱ Timeout: ${formatTimeout(config.timeout.seconds)} — times out if not responded`,
384
- },
385
- ],
386
- },
387
- ]
388
- : [];
389
-
390
- const blocks = [
391
- {
392
- type: "section",
393
- text: {
394
- type: "mrkdwn",
395
- text: `🔔 *Approval Required: ${config.title}*`,
396
- },
397
- },
398
- {
399
- type: "section",
400
- text: {
401
- type: "mrkdwn",
402
- text: questionsText,
403
- },
404
- },
405
- {
406
- type: "actions",
407
- elements: [
408
- {
409
- type: "button",
410
- text: { type: "plain_text", text: "Review & Respond" },
411
- url: approvalUrl,
412
- style: "primary",
413
- },
414
- ],
415
- },
416
- ...timeoutCaption,
417
- ];
418
-
419
- const result = await slackApp.client.chat.postMessage({
420
- channel: notification.target,
421
- text: `Approval Required: ${config.title} — ${approvalUrl}`,
422
- unfurl_links: false,
423
- unfurl_media: false,
424
- // biome-ignore lint/suspicious/noExplicitAny: Block Kit objects
425
- blocks: blocks as any,
426
- });
427
-
428
- if (result.ts) {
429
- updatedChannels[i] = { ...notification, messageTs: result.ts };
430
- updated = true;
431
- }
432
-
433
- console.log(
434
- `[HITL] Slack notification sent to ${notification.target} for request ${requestId}`,
435
- );
436
- } catch (err) {
437
- console.error(
438
- `[HITL] Failed to send Slack notification to ${notification.target}:`,
439
- scrubSecrets(err instanceof Error ? err.message : String(err)),
440
- );
441
- }
442
- }
443
- }
444
-
445
- // Persist messageTs values back to DB
446
- if (updated) {
447
- try {
448
- await db.updateApprovalRequestNotifications(requestId, updatedChannels);
449
- const latest = await db.getApprovalRequestById(requestId);
450
- if (latest?.status === "cancelled") {
451
- await postApprovalCancellationUpdates(
452
- [latest],
453
- latest.resolutionReason ?? "Workflow run cancelled",
454
- );
455
- }
456
- } catch (err) {
457
- console.error(
458
- "[HITL] Failed to update notification channels in DB:",
459
- scrubSecrets(err instanceof Error ? err.message : String(err)),
460
- );
461
- }
462
- }
463
- }
464
- }
465
-
466
- function formatTimeout(seconds: number): string {
467
- if (seconds < 60) return `${seconds}s`;
468
- if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
469
- const hours = Math.floor(seconds / 3600);
470
- const mins = Math.round((seconds % 3600) / 60);
471
- return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
472
311
  }
@@ -10,12 +10,12 @@ import {
10
10
  restoreScratchScriptLastUsedIfUnchanged,
11
11
  touchScratchScriptLastUsed,
12
12
  } from "../../be/scripts/db";
13
+ import { runScriptWithRunCredential } from "../../be/scripts/run-credential";
13
14
  import {
14
15
  DEFAULT_SCRIPT_RESOURCES,
15
16
  MAX_SCRIPT_WALL_CLOCK_MS,
16
17
  MIN_SCRIPT_WALL_CLOCK_MS,
17
18
  } from "../../scripts-runtime/executors/types";
18
- import { runScript } from "../../scripts-runtime/loader";
19
19
  import type { ExecutorMeta } from "../../types";
20
20
  import { getMcpBaseUrl } from "../../utils/constants";
21
21
  import { findInterpolationTokens } from "../../utils/template";
@@ -119,7 +119,7 @@ export class SwarmScriptExecutor extends BaseExecutor<
119
119
  const credentials = await buildScriptCredentialBindingsWithFailures({
120
120
  agentId: agentId ?? undefined,
121
121
  });
122
- const output = await runScript({
122
+ const output = await runScriptWithRunCredential({
123
123
  source: resolved.source,
124
124
  args: config.args,
125
125
  fsMode: "none",
@@ -45,7 +45,7 @@ The worker image pins these harnesses via `ARG`. Each week, check upstream for n
45
45
  bun test
46
46
  bash scripts/check-db-boundary.sh
47
47
  ```
48
- For Codex bumps: also run `bash scripts/check-codex-default-model.sh`.
48
+ For Codex bumps: also run `bash scripts/check-codex-default-model.sh` (CI runs it too). It fails when `ARG CODEX_VERSION` differs from the direct `@openai/codex` pin in `package.json`, or when the Dockerfile's baseline `model = "..."` differs from `CODEX_DEFAULT_MODEL` in `src/providers/codex-models.ts`.
49
49
  Note: do NOT run `docker build -f Dockerfile.worker .` locally — the runner lacks docker. CI's `docker-worker-build` job is the acceptance gate.
50
50
  9. If any local check fails, apply the **partial-bundle fallback** before giving up — one harness must never block the others:
51
51
  **BASELINE FIRST — do this before ANY isolation work.** A red gate does NOT mean the bumps broke anything. Re-run the EXACT failing command on a pristine `origin/main` worktree with zero bumps applied (`git worktree add /tmp/hb-baseline origin/main && cd /tmp/hb-baseline && bun install --frozen-lockfile`) and compare. If the SAME checks fail with the SAME assertions and the same pass/skip/fail totals, the failure is **PRE-EXISTING and unrelated to the bumps** ⇒ it does NOT block this PR, and you must NOT attempt isolation for it. Proceed to step 10, open the PR, and add a `## Pre-existing failures (also fail on origin/main)` section quoting the verbatim error plus both totals (baseline vs branch) so the reviewer can see they match. Only a failure that is RED on your branch and GREEN on baseline was caused by a bump — apply a–d below to that failure ONLY. A gate that is red on origin/main is never a reason to skip the weekly bump.
@@ -44,7 +44,7 @@ The Repository Guidelines carry `allowMerge` and `mergeChecks`.
44
44
  1. Read `/review/rules.md` on the shared agent-fs drive. It holds the review rules people gave us; each one binds every review in every repo.
45
45
  2. Read the repo's `REVIEW.md` when it has one. It holds the repo-specific checks.
46
46
  3. Set the effort from the PR body's `Risk:` line. `low`: a quick pass that still covers CI, tests and the security read. `medium` or no line: the full list below. `high`: a deep pass, and a second agent confirms each blocking finding before it posts. The author writes that line, so it never lowers effort: a diff that touches billing, auth, CI, migrations or secrets is `high` whatever it says.
47
- 4. Re-review check. If you reviewed this PR before, compare `gh pr view <number> --json headRefOid --jq .headRefOid` with the `commit_id` of your last review (`gh api repos/<owner>/<repo>/pulls/<number>/reviews`). Same SHA: reuse that verdict, link it and stop. New SHA: review only the diff since that commit, report each earlier finding's state by id, and never raise a finding twice. Re-run each earlier repro on the new head. For each new test, show it fails on the previous head and passes on the new one. Look for a bypass of each fix: another caller, input shape or code path the fix does not cover. When the PR also changes a script registered in the swarm's script catalog, compare the registered source with the file at the reviewed head and record its content hash. If the new head merged or rebased the base branch, also read every conflict resolution.
47
+ 4. Re-review check. If you reviewed this PR before, compare `gh pr view <number> --json headRefOid --jq .headRefOid` with the `commit_id` of your last review (`gh api repos/<owner>/<repo>/pulls/<number>/reviews`). Same SHA: reuse that verdict, link it and stop, unless the PR gained evidence that answers an open non-code blocker (a UI recording, a screenshot, a log). Then verify only that evidence, reuse the earlier code checks, and post one updated verdict that names the evidence. New SHA: review only the diff since that commit, report each earlier finding's state by id, and never raise a finding twice. Re-run each earlier repro on the new head. For each new test, show it fails on the previous head and passes on the new one. Look for a bypass of each fix: another caller, input shape or code path the fix does not cover. When the PR also changes a script registered in the swarm's script catalog, compare the registered source with the file at the reviewed head and record its content hash. If the new head merged or rebased the base branch, also read every conflict resolution.
48
48
  5. Round cap. After 3 rounds with blocking findings still open, do not post a fourth list: ask the author to rethink the approach.
49
49
 
50
50
  ### What to check
@@ -1,12 +0,0 @@
1
- import {
2
- getUserGrant2,
3
- ensureRbacSeedsSynced2,
4
- init_rbac_roles
5
- } from "./cli-z2dv25mf.js";
6
- import"./cli-q9pzwxvk.js";
7
- init_rbac_roles();
8
-
9
- export {
10
- ensureRbacSeedsSynced2 as ensureRbacSeedsSynced,
11
- getUserGrant2 as getUserGrant
12
- };