@desplega.ai/agent-swarm 1.59.3 โ 1.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openapi.json +1 -1
- package/package.json +1 -1
- package/src/be/db.ts +38 -3
- package/src/be/migrations/033_vcs_installation_node_id.sql +4 -0
- package/src/be/migrations/034_slack_reply_sent.sql +4 -0
- package/src/github/handlers.ts +11 -0
- package/src/github/reactions.ts +119 -0
- package/src/github/task-reactions.ts +66 -0
- package/src/github/types.ts +2 -0
- package/src/slack/HEURISTICS.md +2 -0
- package/src/slack/blocks.ts +179 -2
- package/src/slack/handlers.ts +49 -8
- package/src/slack/responses.ts +59 -4
- package/src/slack/router.ts +3 -1
- package/src/slack/thread-buffer.ts +10 -1
- package/src/slack/watcher.ts +417 -22
- package/src/tests/slack-blocks.test.ts +597 -0
- package/src/tests/slack-router-require-mention.test.ts +137 -0
- package/src/tests/slack-watcher.test.ts +737 -2
- package/src/tests/task-reactions.test.ts +175 -0
- package/src/tools/send-task.ts +8 -5
- package/src/tools/slack-reply.ts +13 -1
- package/src/types.ts +3 -0
- package/templates/official/discoverability-optimizer/CLAUDE.md +79 -0
- package/templates/official/discoverability-optimizer/IDENTITY.md +29 -0
- package/templates/official/discoverability-optimizer/SOUL.md +44 -0
- package/templates/official/discoverability-optimizer/TOOLS.md +47 -0
- package/templates/official/discoverability-optimizer/config.json +23 -0
- package/templates/official/discoverability-optimizer/start-up.sh +23 -0
package/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Swarm API",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.61.0",
|
|
6
6
|
"description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
|
|
7
7
|
},
|
|
8
8
|
"servers": [
|
package/package.json
CHANGED
package/src/be/db.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
|
+
import { addEyesReactionOnTaskStart } from "../github/task-reactions";
|
|
2
3
|
import { configureDbResolver } from "../prompts/resolver";
|
|
3
4
|
import type {
|
|
4
5
|
ActiveSession,
|
|
@@ -709,6 +710,7 @@ type AgentTaskRow = {
|
|
|
709
710
|
slackChannelId: string | null;
|
|
710
711
|
slackThreadTs: string | null;
|
|
711
712
|
slackUserId: string | null;
|
|
713
|
+
slackReplySent: number;
|
|
712
714
|
vcsProvider: string | null;
|
|
713
715
|
vcsRepo: string | null;
|
|
714
716
|
vcsEventType: string | null;
|
|
@@ -716,6 +718,8 @@ type AgentTaskRow = {
|
|
|
716
718
|
vcsCommentId: number | null;
|
|
717
719
|
vcsAuthor: string | null;
|
|
718
720
|
vcsUrl: string | null;
|
|
721
|
+
vcsInstallationId: number | null;
|
|
722
|
+
vcsNodeId: string | null;
|
|
719
723
|
agentmailInboxId: string | null;
|
|
720
724
|
agentmailMessageId: string | null;
|
|
721
725
|
agentmailThreadId: string | null;
|
|
@@ -765,6 +769,7 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
|
765
769
|
slackChannelId: row.slackChannelId ?? undefined,
|
|
766
770
|
slackThreadTs: row.slackThreadTs ?? undefined,
|
|
767
771
|
slackUserId: row.slackUserId ?? undefined,
|
|
772
|
+
slackReplySent: !!row.slackReplySent,
|
|
768
773
|
vcsProvider: (row.vcsProvider as "github" | "gitlab" | null) ?? undefined,
|
|
769
774
|
vcsRepo: row.vcsRepo ?? undefined,
|
|
770
775
|
vcsEventType: row.vcsEventType ?? undefined,
|
|
@@ -772,6 +777,8 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
|
772
777
|
vcsCommentId: row.vcsCommentId ?? undefined,
|
|
773
778
|
vcsAuthor: row.vcsAuthor ?? undefined,
|
|
774
779
|
vcsUrl: row.vcsUrl ?? undefined,
|
|
780
|
+
vcsInstallationId: row.vcsInstallationId ?? undefined,
|
|
781
|
+
vcsNodeId: row.vcsNodeId ?? undefined,
|
|
775
782
|
agentmailInboxId: row.agentmailInboxId ?? undefined,
|
|
776
783
|
agentmailMessageId: row.agentmailMessageId ?? undefined,
|
|
777
784
|
agentmailThreadId: row.agentmailThreadId ?? undefined,
|
|
@@ -950,7 +957,12 @@ export function startTask(taskId: string): AgentTask | null {
|
|
|
950
957
|
});
|
|
951
958
|
} catch {}
|
|
952
959
|
}
|
|
953
|
-
|
|
960
|
+
const result = row ? rowToAgentTask(row) : null;
|
|
961
|
+
// Fire-and-forget: add eyes reaction for GitHub-sourced tasks
|
|
962
|
+
if (result && oldTask.status !== "in_progress") {
|
|
963
|
+
addEyesReactionOnTaskStart(result).catch(() => {});
|
|
964
|
+
}
|
|
965
|
+
return result;
|
|
954
966
|
}
|
|
955
967
|
|
|
956
968
|
export function getTaskById(id: string): AgentTask | null {
|
|
@@ -958,6 +970,19 @@ export function getTaskById(id: string): AgentTask | null {
|
|
|
958
970
|
return row ? rowToAgentTask(row) : null;
|
|
959
971
|
}
|
|
960
972
|
|
|
973
|
+
export function markTaskSlackReplySent(taskId: string): void {
|
|
974
|
+
getDb().run(`UPDATE agent_tasks SET slackReplySent = 1 WHERE id = ?`, [taskId]);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
export function getChildTasks(parentTaskId: string): AgentTask[] {
|
|
978
|
+
return getDb()
|
|
979
|
+
.prepare<AgentTaskRow, [string]>(
|
|
980
|
+
`SELECT * FROM agent_tasks WHERE parentTaskId = ? ORDER BY createdAt ASC`,
|
|
981
|
+
)
|
|
982
|
+
.all(parentTaskId)
|
|
983
|
+
.map(rowToAgentTask);
|
|
984
|
+
}
|
|
985
|
+
|
|
961
986
|
export function updateTaskClaudeSessionId(
|
|
962
987
|
taskId: string,
|
|
963
988
|
claudeSessionId: string,
|
|
@@ -1800,6 +1825,8 @@ export interface CreateTaskOptions {
|
|
|
1800
1825
|
vcsCommentId?: number;
|
|
1801
1826
|
vcsAuthor?: string;
|
|
1802
1827
|
vcsUrl?: string;
|
|
1828
|
+
vcsInstallationId?: number;
|
|
1829
|
+
vcsNodeId?: string;
|
|
1803
1830
|
agentmailInboxId?: string;
|
|
1804
1831
|
agentmailMessageId?: string;
|
|
1805
1832
|
agentmailThreadId?: string;
|
|
@@ -1906,10 +1933,11 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
1906
1933
|
taskType, tags, priority, dependsOn, offeredTo, offeredAt,
|
|
1907
1934
|
slackChannelId, slackThreadTs, slackUserId,
|
|
1908
1935
|
vcsProvider, vcsRepo, vcsEventType, vcsNumber, vcsCommentId, vcsAuthor, vcsUrl,
|
|
1936
|
+
vcsInstallationId, vcsNodeId,
|
|
1909
1937
|
agentmailInboxId, agentmailMessageId, agentmailThreadId,
|
|
1910
1938
|
mentionMessageId, mentionChannelId, dir, parentTaskId, model, scheduleId,
|
|
1911
1939
|
workflowRunId, workflowRunStepId, outputSchema, requestedByUserId, createdAt, lastUpdatedAt
|
|
1912
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
1940
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
1913
1941
|
)
|
|
1914
1942
|
.get(
|
|
1915
1943
|
id,
|
|
@@ -1934,6 +1962,8 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
1934
1962
|
options?.vcsCommentId ?? null,
|
|
1935
1963
|
options?.vcsAuthor ?? null,
|
|
1936
1964
|
options?.vcsUrl ?? null,
|
|
1965
|
+
options?.vcsInstallationId ?? null,
|
|
1966
|
+
options?.vcsNodeId ?? null,
|
|
1937
1967
|
options?.agentmailInboxId ?? null,
|
|
1938
1968
|
options?.agentmailMessageId ?? null,
|
|
1939
1969
|
options?.agentmailThreadId ?? null,
|
|
@@ -2005,7 +2035,12 @@ export function claimTask(taskId: string, agentId: string): AgentTask | null {
|
|
|
2005
2035
|
} catch {}
|
|
2006
2036
|
}
|
|
2007
2037
|
|
|
2008
|
-
|
|
2038
|
+
const result = row ? rowToAgentTask(row) : null;
|
|
2039
|
+
// Fire-and-forget: add eyes reaction for GitHub-sourced tasks
|
|
2040
|
+
if (result) {
|
|
2041
|
+
addEyesReactionOnTaskStart(result).catch(() => {});
|
|
2042
|
+
}
|
|
2043
|
+
return result;
|
|
2009
2044
|
}
|
|
2010
2045
|
|
|
2011
2046
|
export function releaseTask(taskId: string): AgentTask | null {
|
package/src/github/handlers.ts
CHANGED
|
@@ -183,6 +183,7 @@ export async function handlePullRequest(
|
|
|
183
183
|
vcsAuthor: sender.login,
|
|
184
184
|
requestedByUserId,
|
|
185
185
|
vcsUrl: pr.html_url,
|
|
186
|
+
vcsInstallationId: installation?.id,
|
|
186
187
|
});
|
|
187
188
|
|
|
188
189
|
if (lead) {
|
|
@@ -281,6 +282,7 @@ export async function handlePullRequest(
|
|
|
281
282
|
vcsAuthor: sender.login,
|
|
282
283
|
requestedByUserId,
|
|
283
284
|
vcsUrl: pr.html_url,
|
|
285
|
+
vcsInstallationId: installation?.id,
|
|
284
286
|
});
|
|
285
287
|
|
|
286
288
|
if (lead) {
|
|
@@ -371,6 +373,7 @@ export async function handlePullRequest(
|
|
|
371
373
|
vcsAuthor: sender.login,
|
|
372
374
|
requestedByUserId,
|
|
373
375
|
vcsUrl: pr.html_url,
|
|
376
|
+
vcsInstallationId: installation?.id,
|
|
374
377
|
});
|
|
375
378
|
|
|
376
379
|
if (lead) {
|
|
@@ -458,6 +461,7 @@ export async function handlePullRequest(
|
|
|
458
461
|
vcsNumber: pr.number,
|
|
459
462
|
vcsAuthor: sender.login,
|
|
460
463
|
vcsUrl: pr.html_url,
|
|
464
|
+
vcsInstallationId: installation?.id,
|
|
461
465
|
});
|
|
462
466
|
|
|
463
467
|
if (lead) {
|
|
@@ -531,6 +535,7 @@ export async function handleIssue(
|
|
|
531
535
|
vcsAuthor: sender.login,
|
|
532
536
|
requestedByUserId,
|
|
533
537
|
vcsUrl: issue.html_url,
|
|
538
|
+
vcsInstallationId: installation?.id,
|
|
534
539
|
});
|
|
535
540
|
|
|
536
541
|
if (lead) {
|
|
@@ -617,6 +622,7 @@ export async function handleIssue(
|
|
|
617
622
|
vcsAuthor: sender.login,
|
|
618
623
|
requestedByUserId,
|
|
619
624
|
vcsUrl: issue.html_url,
|
|
625
|
+
vcsInstallationId: installation?.id,
|
|
620
626
|
});
|
|
621
627
|
|
|
622
628
|
if (lead) {
|
|
@@ -686,6 +692,7 @@ export async function handleIssue(
|
|
|
686
692
|
vcsNumber: issue.number,
|
|
687
693
|
vcsAuthor: sender.login,
|
|
688
694
|
vcsUrl: issue.html_url,
|
|
695
|
+
vcsInstallationId: installation?.id,
|
|
689
696
|
});
|
|
690
697
|
|
|
691
698
|
if (lead) {
|
|
@@ -784,6 +791,8 @@ export async function handleComment(
|
|
|
784
791
|
vcsCommentId: comment.id,
|
|
785
792
|
vcsAuthor: sender.login,
|
|
786
793
|
vcsUrl: targetUrl,
|
|
794
|
+
vcsInstallationId: installation?.id,
|
|
795
|
+
vcsNodeId: comment.node_id,
|
|
787
796
|
});
|
|
788
797
|
|
|
789
798
|
if (lead) {
|
|
@@ -896,6 +905,8 @@ export async function handlePullRequestReview(
|
|
|
896
905
|
vcsNumber: pr.number,
|
|
897
906
|
vcsAuthor: sender.login,
|
|
898
907
|
vcsUrl: review.html_url,
|
|
908
|
+
vcsInstallationId: installation?.id,
|
|
909
|
+
vcsNodeId: review.node_id,
|
|
899
910
|
});
|
|
900
911
|
|
|
901
912
|
if (lead) {
|
package/src/github/reactions.ts
CHANGED
|
@@ -102,6 +102,125 @@ export async function addIssueReaction(
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Add a reaction to a PR review comment (inline comment on a diff)
|
|
107
|
+
* Uses the pulls/comments endpoint (different from issues/comments)
|
|
108
|
+
*/
|
|
109
|
+
export async function addPullReviewCommentReaction(
|
|
110
|
+
repo: string,
|
|
111
|
+
commentId: number,
|
|
112
|
+
reaction: ReactionType,
|
|
113
|
+
installationId: number,
|
|
114
|
+
): Promise<boolean> {
|
|
115
|
+
if (!isReactionsEnabled()) {
|
|
116
|
+
console.log("[GitHub] Reactions not enabled, skipping");
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const token = await getInstallationToken(installationId);
|
|
121
|
+
if (!token) {
|
|
122
|
+
console.log("[GitHub] No installation token, skipping reaction");
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const response = await fetch(
|
|
128
|
+
`https://api.github.com/repos/${repo}/pulls/comments/${commentId}/reactions`,
|
|
129
|
+
{
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers: {
|
|
132
|
+
Accept: "application/vnd.github+json",
|
|
133
|
+
Authorization: `Bearer ${token}`,
|
|
134
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
135
|
+
"Content-Type": "application/json",
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify({ content: reaction }),
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
const errorText = await response.text();
|
|
143
|
+
console.error(
|
|
144
|
+
`[GitHub] Failed to add PR review comment reaction: ${response.status} ${errorText}`,
|
|
145
|
+
);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log(`[GitHub] Added ${reaction} reaction to PR review comment ${commentId}`);
|
|
150
|
+
return true;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error("[GitHub] Error adding PR review comment reaction:", error);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Add a reaction via the GraphQL API (for PR review bodies which REST doesn't support)
|
|
159
|
+
* Requires the node_id of the subject
|
|
160
|
+
*/
|
|
161
|
+
export async function addGraphQLReaction(
|
|
162
|
+
nodeId: string,
|
|
163
|
+
reaction:
|
|
164
|
+
| "THUMBS_UP"
|
|
165
|
+
| "THUMBS_DOWN"
|
|
166
|
+
| "LAUGH"
|
|
167
|
+
| "HOORAY"
|
|
168
|
+
| "CONFUSED"
|
|
169
|
+
| "HEART"
|
|
170
|
+
| "ROCKET"
|
|
171
|
+
| "EYES",
|
|
172
|
+
installationId: number,
|
|
173
|
+
): Promise<boolean> {
|
|
174
|
+
if (!isReactionsEnabled()) {
|
|
175
|
+
console.log("[GitHub] Reactions not enabled, skipping");
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const token = await getInstallationToken(installationId);
|
|
180
|
+
if (!token) {
|
|
181
|
+
console.log("[GitHub] No installation token, skipping GraphQL reaction");
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const response = await fetch("https://api.github.com/graphql", {
|
|
187
|
+
method: "POST",
|
|
188
|
+
headers: {
|
|
189
|
+
Authorization: `Bearer ${token}`,
|
|
190
|
+
"Content-Type": "application/json",
|
|
191
|
+
},
|
|
192
|
+
body: JSON.stringify({
|
|
193
|
+
query: `mutation AddReaction($input: AddReactionInput!) {
|
|
194
|
+
addReaction(input: $input) {
|
|
195
|
+
reaction { content }
|
|
196
|
+
}
|
|
197
|
+
}`,
|
|
198
|
+
variables: {
|
|
199
|
+
input: { subjectId: nodeId, content: reaction },
|
|
200
|
+
},
|
|
201
|
+
}),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (!response.ok) {
|
|
205
|
+
const errorText = await response.text();
|
|
206
|
+
console.error(`[GitHub] GraphQL reaction failed: ${response.status} ${errorText}`);
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const data = (await response.json()) as { errors?: Array<{ message: string }> };
|
|
211
|
+
if (data.errors?.length) {
|
|
212
|
+
console.error(`[GitHub] GraphQL reaction errors: ${JSON.stringify(data.errors)}`);
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
console.log(`[GitHub] Added ${reaction} GraphQL reaction to node ${nodeId}`);
|
|
217
|
+
return true;
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error("[GitHub] Error adding GraphQL reaction:", error);
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
105
224
|
/**
|
|
106
225
|
* Post a comment on an issue or PR
|
|
107
226
|
* Appears as agent-swarm-bot[bot] commenting
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { AgentTask } from "../types";
|
|
2
|
+
import {
|
|
3
|
+
addGraphQLReaction,
|
|
4
|
+
addIssueReaction,
|
|
5
|
+
addPullReviewCommentReaction,
|
|
6
|
+
addReaction,
|
|
7
|
+
} from "./reactions";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Add an ๐ eyes reaction to the source GitHub comment/review when a task starts.
|
|
11
|
+
* Called when a task transitions to `in_progress`.
|
|
12
|
+
*
|
|
13
|
+
* Handles three GitHub event types:
|
|
14
|
+
* - issue_comment: REST reaction on issue comment
|
|
15
|
+
* - pull_request_review_comment: REST reaction on PR review comment (inline)
|
|
16
|
+
* - pull_request_review: GraphQL reaction on review body (REST doesn't support review reactions)
|
|
17
|
+
*/
|
|
18
|
+
export async function addEyesReactionOnTaskStart(task: AgentTask): Promise<void> {
|
|
19
|
+
// Only for GitHub-sourced tasks with installation info
|
|
20
|
+
if (task.source !== "github" || task.vcsProvider !== "github") return;
|
|
21
|
+
if (!task.vcsInstallationId) return;
|
|
22
|
+
|
|
23
|
+
const installationId = task.vcsInstallationId;
|
|
24
|
+
const repo = task.vcsRepo;
|
|
25
|
+
if (!repo) return;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
switch (task.vcsEventType) {
|
|
29
|
+
case "issue_comment": {
|
|
30
|
+
// Issue comment โ use REST issues/comments endpoint
|
|
31
|
+
if (task.vcsCommentId) {
|
|
32
|
+
await addReaction(repo, task.vcsCommentId, "eyes", installationId);
|
|
33
|
+
}
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
case "pull_request_review_comment": {
|
|
38
|
+
// Inline PR review comment โ use REST pulls/comments endpoint
|
|
39
|
+
if (task.vcsCommentId) {
|
|
40
|
+
await addPullReviewCommentReaction(repo, task.vcsCommentId, "eyes", installationId);
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
case "pull_request_review": {
|
|
46
|
+
// PR review body โ requires GraphQL API
|
|
47
|
+
if (task.vcsNodeId) {
|
|
48
|
+
await addGraphQLReaction(task.vcsNodeId, "EYES", installationId);
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
case "pull_request":
|
|
54
|
+
case "issues": {
|
|
55
|
+
// PR or issue opened/labeled โ react on the issue/PR itself
|
|
56
|
+
if (task.vcsNumber) {
|
|
57
|
+
await addIssueReaction(repo, task.vcsNumber, "eyes", installationId);
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// Never fail the task start due to a reaction error
|
|
64
|
+
console.error("[GitHub] Failed to add eyes reaction on task start:", error);
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/github/types.ts
CHANGED
|
@@ -37,6 +37,7 @@ export interface IssueEvent extends GitHubWebhookEvent {
|
|
|
37
37
|
export interface CommentEvent extends GitHubWebhookEvent {
|
|
38
38
|
comment: {
|
|
39
39
|
id: number;
|
|
40
|
+
node_id?: string;
|
|
40
41
|
body: string;
|
|
41
42
|
html_url: string;
|
|
42
43
|
user: { login: string };
|
|
@@ -49,6 +50,7 @@ export interface PullRequestReviewEvent extends GitHubWebhookEvent {
|
|
|
49
50
|
action: "submitted" | "edited" | "dismissed";
|
|
50
51
|
review: {
|
|
51
52
|
id: number;
|
|
53
|
+
node_id?: string;
|
|
52
54
|
body: string | null;
|
|
53
55
|
state: "approved" | "changes_requested" | "commented" | "dismissed";
|
|
54
56
|
html_url: string;
|
package/src/slack/HEURISTICS.md
CHANGED
|
@@ -17,6 +17,7 @@ When someone @mentions the bot in a thread, the router checks whether a worker a
|
|
|
17
17
|
- Message must be in a thread (`thread_ts` present)
|
|
18
18
|
- An agent must have an active task (`in_progress` or `pending`) linked to that thread via `slackChannelId` + `slackThreadTs`
|
|
19
19
|
- The matched agent must not be `offline`
|
|
20
|
+
- If `SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION=true`, non-mention thread messages are silently dropped instead of routing to the working agent. DM assistant threads are unaffected.
|
|
20
21
|
|
|
21
22
|
**Why this matters:** Without thread follow-up routing, every @mention in a thread goes to the lead, who then has to re-delegate. This shortcut sends the message directly to the worker already handling that conversation.
|
|
22
23
|
|
|
@@ -89,6 +90,7 @@ When the additive buffer flushes normally (not via `!now`), the created task use
|
|
|
89
90
|
|------|---------|-------------|
|
|
90
91
|
| `ADDITIVE_SLACK` | `false` | Enables non-mention thread message buffering and batching |
|
|
91
92
|
| `ADDITIVE_SLACK_BUFFER_MS` | `10000` (10s) | Debounce window in milliseconds for the thread buffer |
|
|
93
|
+
| `SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION` | `false` | Requires @mention for thread follow-up routing; non-mention thread messages are silently dropped |
|
|
92
94
|
|
|
93
95
|
Both are read from environment variables. `ADDITIVE_SLACK` must be exactly `"true"` to enable. `ADDITIVE_SLACK_BUFFER_MS` is parsed as a number with fallback to 10000.
|
|
94
96
|
|
package/src/slack/blocks.ts
CHANGED
|
@@ -128,6 +128,38 @@ function cancelActionBlock(taskId: string): SlackBlock {
|
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// --- Utilities ---
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Format a duration between two dates in a compact human-readable form.
|
|
135
|
+
* Examples: "45s", "2m 14s", "1h 30m"
|
|
136
|
+
*/
|
|
137
|
+
export function formatDuration(start: Date, end: Date): string {
|
|
138
|
+
const ms = end.getTime() - start.getTime();
|
|
139
|
+
const seconds = Math.floor(ms / 1000);
|
|
140
|
+
if (seconds < 60) return `${seconds}s`;
|
|
141
|
+
const minutes = Math.floor(seconds / 60);
|
|
142
|
+
const remainingSeconds = seconds % 60;
|
|
143
|
+
if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
|
|
144
|
+
const hours = Math.floor(minutes / 60);
|
|
145
|
+
const remainingMinutes = minutes % 60;
|
|
146
|
+
return `${hours}h ${remainingMinutes}m`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- Tree types ---
|
|
150
|
+
|
|
151
|
+
export interface TreeNode {
|
|
152
|
+
taskId: string;
|
|
153
|
+
agentName: string;
|
|
154
|
+
status: "pending" | "in_progress" | "completed" | "failed" | "cancelled";
|
|
155
|
+
progress?: string;
|
|
156
|
+
duration?: string;
|
|
157
|
+
slackReplySent?: boolean;
|
|
158
|
+
output?: string; // Only used when !slackReplySent on completion
|
|
159
|
+
failureReason?: string; // Always shown on failure
|
|
160
|
+
children: TreeNode[];
|
|
161
|
+
}
|
|
162
|
+
|
|
131
163
|
// --- High-level block builders ---
|
|
132
164
|
|
|
133
165
|
/**
|
|
@@ -139,14 +171,19 @@ export function buildCompletedBlocks(opts: {
|
|
|
139
171
|
taskId: string;
|
|
140
172
|
body: string;
|
|
141
173
|
duration?: string;
|
|
174
|
+
minimal?: boolean; // true = suppress body (agent already replied via slack-reply)
|
|
142
175
|
}): SlackBlock[] {
|
|
143
176
|
const taskLink = getTaskLink(opts.taskId);
|
|
144
177
|
let line = `โ
*${opts.agentName}* (${taskLink})`;
|
|
145
178
|
if (opts.duration) line += ` ยท ${opts.duration}`;
|
|
146
179
|
|
|
147
180
|
const blocks: SlackBlock[] = [sectionBlock(line)];
|
|
148
|
-
|
|
149
|
-
|
|
181
|
+
|
|
182
|
+
// Only include body if not minimal (agent didn't reply via slack-reply)
|
|
183
|
+
if (!opts.minimal) {
|
|
184
|
+
for (const chunk of splitText(opts.body)) {
|
|
185
|
+
blocks.push(sectionBlock(chunk));
|
|
186
|
+
}
|
|
150
187
|
}
|
|
151
188
|
return blocks;
|
|
152
189
|
}
|
|
@@ -231,3 +268,143 @@ export function buildBufferFlushBlocks(opts: {
|
|
|
231
268
|
|
|
232
269
|
return [contextBlock(`๐ก _${text}_ (${taskLink})`)];
|
|
233
270
|
}
|
|
271
|
+
|
|
272
|
+
// --- Tree rendering ---
|
|
273
|
+
|
|
274
|
+
const STATUS_ICON: Record<TreeNode["status"], string> = {
|
|
275
|
+
pending: "๐ก",
|
|
276
|
+
in_progress: "โณ",
|
|
277
|
+
completed: "โ
",
|
|
278
|
+
failed: "โ",
|
|
279
|
+
cancelled: "๐ซ",
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const MAX_VISIBLE_CHILDREN = 8;
|
|
283
|
+
const MAX_OUTPUT_LENGTH = 120;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Truncate output to the first sentence or MAX_OUTPUT_LENGTH, whichever is shorter.
|
|
287
|
+
*/
|
|
288
|
+
function truncateOutput(text: string): string {
|
|
289
|
+
// Find first sentence boundary (. followed by space or end)
|
|
290
|
+
const sentenceEnd = text.search(/\.\s/);
|
|
291
|
+
const firstSentence = sentenceEnd !== -1 ? text.slice(0, sentenceEnd + 1) : text;
|
|
292
|
+
if (firstSentence.length <= MAX_OUTPUT_LENGTH) return firstSentence;
|
|
293
|
+
return `${text.slice(0, MAX_OUTPUT_LENGTH)}โฆ`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Render a single node line: icon + bold name + task link + optional duration.
|
|
298
|
+
*/
|
|
299
|
+
function renderNodeLine(node: TreeNode): string {
|
|
300
|
+
const icon = STATUS_ICON[node.status];
|
|
301
|
+
const taskLink = getTaskLink(node.taskId);
|
|
302
|
+
let line = `${icon} *${node.agentName}* (${taskLink})`;
|
|
303
|
+
if (node.duration) line += ` ยท ${node.duration}`;
|
|
304
|
+
return line;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Render detail lines for a child node (progress, output, failure reason).
|
|
309
|
+
* Returns an array of indented lines to appear below the child's main line.
|
|
310
|
+
*/
|
|
311
|
+
function renderChildDetail(node: TreeNode, indent: string): string[] {
|
|
312
|
+
const lines: string[] = [];
|
|
313
|
+
|
|
314
|
+
if (node.status === "failed" && node.failureReason) {
|
|
315
|
+
lines.push(`${indent}Error: ${node.failureReason}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (node.status === "in_progress" && node.progress) {
|
|
319
|
+
lines.push(`${indent}${node.progress}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (node.status === "completed" && !node.slackReplySent && node.output) {
|
|
323
|
+
lines.push(`${indent}${truncateOutput(node.output)}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return lines;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Render a single root node and its children as a mrkdwn tree string.
|
|
331
|
+
*/
|
|
332
|
+
function renderTree(root: TreeNode): string {
|
|
333
|
+
const lines: string[] = [];
|
|
334
|
+
|
|
335
|
+
// Root line
|
|
336
|
+
lines.push(renderNodeLine(root));
|
|
337
|
+
|
|
338
|
+
// Root-level detail (progress for in-progress root with no children)
|
|
339
|
+
if (root.children.length === 0) {
|
|
340
|
+
if (root.status === "in_progress" && root.progress) {
|
|
341
|
+
lines.push(` ${root.progress}`);
|
|
342
|
+
}
|
|
343
|
+
if (root.status === "failed" && root.failureReason) {
|
|
344
|
+
lines.push(` Error: ${root.failureReason}`);
|
|
345
|
+
}
|
|
346
|
+
if (root.status === "completed" && !root.slackReplySent && root.output) {
|
|
347
|
+
lines.push(` ${truncateOutput(root.output)}`);
|
|
348
|
+
}
|
|
349
|
+
return lines.join("\n");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const visibleChildren = root.children.slice(0, MAX_VISIBLE_CHILDREN);
|
|
353
|
+
const hiddenCount = root.children.length - visibleChildren.length;
|
|
354
|
+
|
|
355
|
+
for (let i = 0; i < visibleChildren.length; i++) {
|
|
356
|
+
const child = visibleChildren[i] as TreeNode;
|
|
357
|
+
const isLast = i === visibleChildren.length - 1 && hiddenCount === 0;
|
|
358
|
+
const prefix = isLast ? "โ " : "โ ";
|
|
359
|
+
const continuationPrefix = isLast ? " " : "โ ";
|
|
360
|
+
|
|
361
|
+
lines.push(`${prefix}${renderNodeLine(child)}`);
|
|
362
|
+
|
|
363
|
+
for (const detail of renderChildDetail(child, continuationPrefix)) {
|
|
364
|
+
lines.push(detail);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (hiddenCount > 0) {
|
|
369
|
+
lines.push(`โ _and ${hiddenCount} more..._`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return lines.join("\n");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Check if any node in the tree is still active (pending or in_progress).
|
|
377
|
+
*/
|
|
378
|
+
function isTreeActive(node: TreeNode): boolean {
|
|
379
|
+
if (node.status === "pending" || node.status === "in_progress") return true;
|
|
380
|
+
return node.children.some(isTreeActive);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Build Slack blocks for a tree-based status message.
|
|
385
|
+
*
|
|
386
|
+
* Renders one or more root nodes as mrkdwn trees with status icons,
|
|
387
|
+
* agent names, task links, durations, progress text, and error details.
|
|
388
|
+
*
|
|
389
|
+
* For in-progress trees, includes a cancel button per active root.
|
|
390
|
+
*
|
|
391
|
+
* @param roots - Array of root TreeNode objects (one per assigned task in a round)
|
|
392
|
+
* @returns SlackBlock[] suitable for chat.postMessage / chat.update
|
|
393
|
+
*/
|
|
394
|
+
export function buildTreeBlocks(roots: TreeNode[]): SlackBlock[] {
|
|
395
|
+
console.log(
|
|
396
|
+
`[Slack] Building tree blocks for ${roots.length} root(s): ${roots.map((r) => r.taskId.slice(0, 8)).join(", ")}`,
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
const treeTexts = roots.map(renderTree);
|
|
400
|
+
const blocks: SlackBlock[] = [sectionBlock(treeTexts.join("\n\n"))];
|
|
401
|
+
|
|
402
|
+
// Add cancel buttons for active roots
|
|
403
|
+
for (const root of roots) {
|
|
404
|
+
if (isTreeActive(root)) {
|
|
405
|
+
blocks.push(cancelActionBlock(root.taskId));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return blocks;
|
|
410
|
+
}
|