@desplega.ai/agent-swarm 1.55.0 → 1.56.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.
- package/openapi.json +26 -1
- package/package.json +1 -1
- package/src/be/db.ts +9 -1
- package/src/be/migrations/025_workflow_run_cancelled_status.sql +0 -2
- package/src/be/migrations/027_heartbeat_md.sql +1 -0
- package/src/be/migrations/runner.ts +8 -0
- package/src/commands/runner.ts +24 -1
- package/src/github/handlers.ts +122 -406
- package/src/github/index.ts +8 -1
- package/src/github/mentions.ts +10 -0
- package/src/github/templates.ts +55 -0
- package/src/github/types.ts +2 -0
- package/src/heartbeat/heartbeat.ts +201 -92
- package/src/heartbeat/templates.ts +64 -9
- package/src/hooks/hook.ts +11 -1
- package/src/http/agents.ts +5 -2
- package/src/http/heartbeat.ts +29 -1
- package/src/prompts/session-templates.ts +35 -0
- package/src/tests/concurrency.test.ts +3 -3
- package/src/tests/github-event-filter.test.ts +301 -0
- package/src/tests/github-event-labels.test.ts +24 -0
- package/src/tests/heartbeat-checklist.test.ts +417 -0
- package/src/tests/heartbeat.test.ts +2 -57
- package/src/tests/prompt-template-remaining.test.ts +1 -1
- package/src/tools/update-profile.ts +21 -3
- package/src/types.ts +4 -1
- package/templates/official/lead/CLAUDE.md +3 -0
- package/templates/official/lead/HEARTBEAT.md +12 -0
- package/templates/official/lead/config.json +2 -1
- package/templates/schema.ts +2 -0
package/src/github/handlers.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { createTaskExtended, failTask, findTaskByVcs, getAllAgents } from "../be/db";
|
|
2
2
|
import { resolveTemplate } from "../prompts/resolver";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
detectMention,
|
|
5
|
+
extractMentionContext,
|
|
6
|
+
GITHUB_BOT_NAME,
|
|
7
|
+
isBotAssignee,
|
|
8
|
+
isSwarmLabel,
|
|
9
|
+
} from "./mentions";
|
|
4
10
|
import { addIssueReaction, addReaction } from "./reactions";
|
|
5
11
|
// Side-effect import: registers all GitHub event templates in the in-memory registry
|
|
6
12
|
import "./templates";
|
|
@@ -315,42 +321,32 @@ export async function handlePullRequest(
|
|
|
315
321
|
return { created: false };
|
|
316
322
|
}
|
|
317
323
|
|
|
318
|
-
// Handle
|
|
319
|
-
if (action === "
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (!task) {
|
|
323
|
-
// No task for this PR, nothing to notify
|
|
324
|
+
// Handle labeled action - swarm label added to PR
|
|
325
|
+
if (action === "labeled") {
|
|
326
|
+
const labelName = event.label?.name;
|
|
327
|
+
if (!labelName || !isSwarmLabel(labelName)) {
|
|
324
328
|
return { created: false };
|
|
325
329
|
}
|
|
326
330
|
|
|
327
331
|
// Deduplicate
|
|
328
|
-
const eventKey = `pr-
|
|
332
|
+
const eventKey = `pr-labeled:${repository.full_name}:${pr.number}:${labelName}`;
|
|
329
333
|
if (isDuplicate(eventKey)) {
|
|
330
334
|
return { created: false };
|
|
331
335
|
}
|
|
332
336
|
|
|
333
337
|
const lead = findLeadAgent();
|
|
334
|
-
const wasMerged = pr.merged;
|
|
335
|
-
const emoji = wasMerged ? "🎉" : "❌";
|
|
336
|
-
const status = wasMerged ? "MERGED" : "CLOSED";
|
|
337
|
-
const mergedBy = wasMerged && pr.merged_by ? ` by ${pr.merged_by.login}` : "";
|
|
338
|
-
const followUpSuggestion = wasMerged
|
|
339
|
-
? "💡 PR successfully merged! Update any related issues or documentation."
|
|
340
|
-
: "💡 PR was closed without merging. Review if follow-up is needed.";
|
|
341
|
-
|
|
342
338
|
const result = resolveTemplate(
|
|
343
|
-
"github.pull_request.
|
|
339
|
+
"github.pull_request.labeled",
|
|
344
340
|
{
|
|
345
|
-
status_emoji: emoji,
|
|
346
341
|
pr_number: pr.number,
|
|
347
|
-
status,
|
|
348
|
-
merged_by: mergedBy,
|
|
349
342
|
pr_title: pr.title,
|
|
343
|
+
label_name: labelName,
|
|
344
|
+
sender_login: sender.login,
|
|
350
345
|
repo_full_name: repository.full_name,
|
|
346
|
+
head_ref: pr.head.ref,
|
|
347
|
+
base_ref: pr.base.ref,
|
|
351
348
|
pr_url: pr.html_url,
|
|
352
|
-
|
|
353
|
-
follow_up_suggestion: followUpSuggestion,
|
|
349
|
+
context: pr.body || pr.title,
|
|
354
350
|
},
|
|
355
351
|
{ agentId: lead?.id, repoId: repository.full_name },
|
|
356
352
|
);
|
|
@@ -359,11 +355,11 @@ export async function handlePullRequest(
|
|
|
359
355
|
return { created: false };
|
|
360
356
|
}
|
|
361
357
|
|
|
362
|
-
const
|
|
358
|
+
const task = createTaskExtended(result.text, {
|
|
363
359
|
agentId: lead?.id ?? "",
|
|
364
360
|
source: "github",
|
|
365
361
|
vcsProvider: "github",
|
|
366
|
-
taskType: "github-pr
|
|
362
|
+
taskType: "github-pr",
|
|
367
363
|
vcsRepo: repository.full_name,
|
|
368
364
|
vcsEventType: "pull_request",
|
|
369
365
|
vcsNumber: pr.number,
|
|
@@ -371,64 +367,37 @@ export async function handlePullRequest(
|
|
|
371
367
|
vcsUrl: pr.html_url,
|
|
372
368
|
});
|
|
373
369
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (action === "synchronize") {
|
|
383
|
-
// Find the related task
|
|
384
|
-
const task = findTaskByVcs(repository.full_name, pr.number);
|
|
385
|
-
if (!task) {
|
|
386
|
-
// No task for this PR, nothing to notify
|
|
387
|
-
return { created: false };
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Deduplicate using SHA to avoid duplicate notifications for same push
|
|
391
|
-
const eventKey = `pr-sync:${repository.full_name}:${pr.number}:${pr.head.sha}`;
|
|
392
|
-
if (isDuplicate(eventKey)) {
|
|
393
|
-
return { created: false };
|
|
370
|
+
if (lead) {
|
|
371
|
+
console.log(
|
|
372
|
+
`[GitHub] Created task ${task.id} for PR #${pr.number} (labeled: ${labelName}) -> ${lead.name}`,
|
|
373
|
+
);
|
|
374
|
+
} else {
|
|
375
|
+
console.log(
|
|
376
|
+
`[GitHub] Created unassigned task ${task.id} for PR #${pr.number} (labeled: ${labelName}, no lead available)`,
|
|
377
|
+
);
|
|
394
378
|
}
|
|
395
379
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
"github.pull_request.synchronize",
|
|
399
|
-
{
|
|
400
|
-
pr_number: pr.number,
|
|
401
|
-
pr_title: pr.title,
|
|
402
|
-
repo_full_name: repository.full_name,
|
|
403
|
-
head_ref: pr.head.ref,
|
|
404
|
-
head_sha_short: pr.head.sha.substring(0, 7),
|
|
405
|
-
pr_url: pr.html_url,
|
|
406
|
-
related_task_id: task.id,
|
|
407
|
-
},
|
|
408
|
-
{ agentId: lead?.id, repoId: repository.full_name },
|
|
409
|
-
);
|
|
410
|
-
|
|
411
|
-
if (result.skipped) {
|
|
412
|
-
return { created: false };
|
|
380
|
+
if (installation?.id) {
|
|
381
|
+
addIssueReaction(repository.full_name, pr.number, "eyes", installation.id);
|
|
413
382
|
}
|
|
414
383
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
source: "github",
|
|
418
|
-
vcsProvider: "github",
|
|
419
|
-
taskType: "github-pr-update",
|
|
420
|
-
vcsRepo: repository.full_name,
|
|
421
|
-
vcsEventType: "pull_request",
|
|
422
|
-
vcsNumber: pr.number,
|
|
423
|
-
vcsAuthor: sender.login,
|
|
424
|
-
vcsUrl: pr.html_url,
|
|
425
|
-
});
|
|
384
|
+
return { created: true, taskId: task.id };
|
|
385
|
+
}
|
|
426
386
|
|
|
387
|
+
// Suppressed: see thoughts/taras/plans/2026-03-30-github-event-safety-defaults.md
|
|
388
|
+
if (action === "closed") {
|
|
427
389
|
console.log(
|
|
428
|
-
`[GitHub]
|
|
390
|
+
`[GitHub:suppressed] pull_request.closed on ${repository.full_name}#${pr.number} — lifecycle events disabled by default`,
|
|
429
391
|
);
|
|
392
|
+
return { created: false };
|
|
393
|
+
}
|
|
430
394
|
|
|
431
|
-
|
|
395
|
+
// Suppressed: see thoughts/taras/plans/2026-03-30-github-event-safety-defaults.md
|
|
396
|
+
if (action === "synchronize") {
|
|
397
|
+
console.log(
|
|
398
|
+
`[GitHub:suppressed] pull_request.synchronize on ${repository.full_name}#${pr.number} — lifecycle events disabled by default`,
|
|
399
|
+
);
|
|
400
|
+
return { created: false };
|
|
432
401
|
}
|
|
433
402
|
|
|
434
403
|
// Only handle opened/edited actions for mention-based flow
|
|
@@ -595,6 +564,67 @@ export async function handleIssue(
|
|
|
595
564
|
return { created: false };
|
|
596
565
|
}
|
|
597
566
|
|
|
567
|
+
// Handle labeled action - swarm label added to issue
|
|
568
|
+
if (action === "labeled") {
|
|
569
|
+
const labelName = event.label?.name;
|
|
570
|
+
if (!labelName || !isSwarmLabel(labelName)) {
|
|
571
|
+
return { created: false };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Deduplicate
|
|
575
|
+
const eventKey = `issue-labeled:${repository.full_name}:${issue.number}:${labelName}`;
|
|
576
|
+
if (isDuplicate(eventKey)) {
|
|
577
|
+
return { created: false };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const lead = findLeadAgent();
|
|
581
|
+
const result = resolveTemplate(
|
|
582
|
+
"github.issue.labeled",
|
|
583
|
+
{
|
|
584
|
+
issue_number: issue.number,
|
|
585
|
+
issue_title: issue.title,
|
|
586
|
+
label_name: labelName,
|
|
587
|
+
sender_login: sender.login,
|
|
588
|
+
repo_full_name: repository.full_name,
|
|
589
|
+
issue_url: issue.html_url,
|
|
590
|
+
context: issue.body || issue.title,
|
|
591
|
+
},
|
|
592
|
+
{ agentId: lead?.id, repoId: repository.full_name },
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
if (result.skipped) {
|
|
596
|
+
return { created: false };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const task = createTaskExtended(result.text, {
|
|
600
|
+
agentId: lead?.id ?? "",
|
|
601
|
+
source: "github",
|
|
602
|
+
vcsProvider: "github",
|
|
603
|
+
taskType: "github-issue",
|
|
604
|
+
vcsRepo: repository.full_name,
|
|
605
|
+
vcsEventType: "issues",
|
|
606
|
+
vcsNumber: issue.number,
|
|
607
|
+
vcsAuthor: sender.login,
|
|
608
|
+
vcsUrl: issue.html_url,
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
if (lead) {
|
|
612
|
+
console.log(
|
|
613
|
+
`[GitHub] Created task ${task.id} for issue #${issue.number} (labeled: ${labelName}) -> ${lead.name}`,
|
|
614
|
+
);
|
|
615
|
+
} else {
|
|
616
|
+
console.log(
|
|
617
|
+
`[GitHub] Created unassigned task ${task.id} for issue #${issue.number} (labeled: ${labelName}, no lead available)`,
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (installation?.id) {
|
|
622
|
+
addIssueReaction(repository.full_name, issue.number, "eyes", installation.id);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return { created: true, taskId: task.id };
|
|
626
|
+
}
|
|
627
|
+
|
|
598
628
|
// Only handle opened/edited actions for mention-based flow
|
|
599
629
|
if (action !== "opened" && action !== "edited") {
|
|
600
630
|
return { created: false };
|
|
@@ -770,103 +800,13 @@ export async function handleComment(
|
|
|
770
800
|
export async function handlePullRequestReview(
|
|
771
801
|
event: PullRequestReviewEvent,
|
|
772
802
|
): Promise<{ created: boolean; taskId?: string }> {
|
|
773
|
-
const { action,
|
|
803
|
+
const { action, pull_request: pr, repository } = event;
|
|
774
804
|
|
|
775
|
-
//
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
return { created: false };
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// Skip "commented" reviews that are empty - these are often just line comments
|
|
782
|
-
// without an overall review body
|
|
783
|
-
if (review.state === "commented" && !review.body) {
|
|
784
|
-
return { created: false };
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
// Deduplicate
|
|
788
|
-
const eventKey = `pr-review:${repository.full_name}:${pr.number}:${review.id}`;
|
|
789
|
-
if (isDuplicate(eventKey)) {
|
|
790
|
-
return { created: false };
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
// Find any existing task for this PR
|
|
794
|
-
const existingTask = findTaskByVcs(repository.full_name, pr.number);
|
|
795
|
-
|
|
796
|
-
// Only notify for PRs where bot is creator or already has a task
|
|
797
|
-
const isBotCreator = isBotAssignee(pr.user.login);
|
|
798
|
-
if (!isBotCreator && !existingTask) {
|
|
799
|
-
return { created: false };
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
// Find lead agent for new task
|
|
803
|
-
const lead = findLeadAgent();
|
|
804
|
-
|
|
805
|
-
// Get review state info
|
|
806
|
-
const { emoji, label } = getReviewStateInfo(review.state);
|
|
807
|
-
|
|
808
|
-
// Build task description
|
|
809
|
-
const reviewBodySection = review.body ? `\n\nReview Comment:\n${review.body}` : "";
|
|
810
|
-
const relatedTaskSection = existingTask
|
|
811
|
-
? `Related task: ${existingTask.id}\n🔀 Consider routing to the same agent working on the related task.\n`
|
|
812
|
-
: "";
|
|
813
|
-
const reviewSuggestions =
|
|
814
|
-
review.state === "approved"
|
|
815
|
-
? "💡 Suggested: Merge the PR or wait for additional reviews"
|
|
816
|
-
: review.state === "changes_requested"
|
|
817
|
-
? "💡 Suggested: Address the requested changes and update the PR"
|
|
818
|
-
: "💡 Suggested: Review the feedback and respond if needed";
|
|
819
|
-
|
|
820
|
-
const result = resolveTemplate(
|
|
821
|
-
"github.pull_request.review_submitted",
|
|
822
|
-
{
|
|
823
|
-
review_emoji: emoji,
|
|
824
|
-
pr_number: pr.number,
|
|
825
|
-
review_label: label,
|
|
826
|
-
pr_title: pr.title,
|
|
827
|
-
sender_login: sender.login,
|
|
828
|
-
repo_full_name: repository.full_name,
|
|
829
|
-
review_url: review.html_url,
|
|
830
|
-
review_body_section: reviewBodySection,
|
|
831
|
-
related_task_section: relatedTaskSection,
|
|
832
|
-
review_suggestions: reviewSuggestions,
|
|
833
|
-
},
|
|
834
|
-
{ agentId: lead?.id, repoId: repository.full_name },
|
|
805
|
+
// Suppressed: see thoughts/taras/plans/2026-03-30-github-event-safety-defaults.md
|
|
806
|
+
console.log(
|
|
807
|
+
`[GitHub:suppressed] pull_request_review.${action} on ${repository.full_name}#${pr.number} — review events disabled by default`,
|
|
835
808
|
);
|
|
836
|
-
|
|
837
|
-
if (result.skipped) {
|
|
838
|
-
return { created: false };
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
// Create task (assigned to lead if available, otherwise unassigned)
|
|
842
|
-
const task = createTaskExtended(result.text, {
|
|
843
|
-
agentId: lead?.id ?? "",
|
|
844
|
-
source: "github",
|
|
845
|
-
vcsProvider: "github",
|
|
846
|
-
taskType: "github-review",
|
|
847
|
-
vcsRepo: repository.full_name,
|
|
848
|
-
vcsEventType: "pull_request_review",
|
|
849
|
-
vcsNumber: pr.number,
|
|
850
|
-
vcsAuthor: sender.login,
|
|
851
|
-
vcsUrl: review.html_url,
|
|
852
|
-
});
|
|
853
|
-
|
|
854
|
-
if (lead) {
|
|
855
|
-
console.log(
|
|
856
|
-
`[GitHub] Created task ${task.id} for PR #${pr.number} review (${review.state}) -> ${lead.name}`,
|
|
857
|
-
);
|
|
858
|
-
} else {
|
|
859
|
-
console.log(
|
|
860
|
-
`[GitHub] Created unassigned task ${task.id} for PR #${pr.number} review (${review.state}, no lead available)`,
|
|
861
|
-
);
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// Add reaction to acknowledge the review
|
|
865
|
-
if (installation?.id) {
|
|
866
|
-
addIssueReaction(repository.full_name, pr.number, "eyes", installation.id);
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
return { created: true, taskId: task.id };
|
|
809
|
+
return { created: false };
|
|
870
810
|
}
|
|
871
811
|
|
|
872
812
|
/**
|
|
@@ -879,89 +819,12 @@ export async function handleCheckRun(
|
|
|
879
819
|
): Promise<{ created: boolean; taskId?: string }> {
|
|
880
820
|
const { action, check_run, repository } = event;
|
|
881
821
|
|
|
882
|
-
//
|
|
883
|
-
|
|
884
|
-
return { created: false };
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
// Only notify on failure or action_required - success is less critical
|
|
888
|
-
// Skip neutral/skipped/cancelled as they're usually not actionable
|
|
889
|
-
const conclusion = check_run.conclusion;
|
|
890
|
-
if (conclusion !== "failure" && conclusion !== "action_required") {
|
|
891
|
-
return { created: false };
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// Must be associated with at least one PR
|
|
895
|
-
if (!check_run.pull_requests || check_run.pull_requests.length === 0) {
|
|
896
|
-
return { created: false };
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
// Check if we have a task for any of these PRs
|
|
900
|
-
let relatedTask = null;
|
|
901
|
-
let prNumber = 0;
|
|
902
|
-
for (const pr of check_run.pull_requests) {
|
|
903
|
-
const task = findTaskByVcs(repository.full_name, pr.number);
|
|
904
|
-
if (task) {
|
|
905
|
-
relatedTask = task;
|
|
906
|
-
prNumber = pr.number;
|
|
907
|
-
break;
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
if (!relatedTask) {
|
|
912
|
-
// No task for any of the associated PRs
|
|
913
|
-
return { created: false };
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
// Deduplicate
|
|
917
|
-
const eventKey = `check-run:${repository.full_name}:${check_run.id}`;
|
|
918
|
-
if (isDuplicate(eventKey)) {
|
|
919
|
-
return { created: false };
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
const lead = findLeadAgent();
|
|
923
|
-
const { emoji, label } = getCheckConclusionInfo(conclusion);
|
|
924
|
-
|
|
925
|
-
const outputSummarySection = check_run.output.summary
|
|
926
|
-
? `\n\nSummary:\n${check_run.output.summary.substring(0, 500)}`
|
|
927
|
-
: "";
|
|
928
|
-
|
|
929
|
-
const result = resolveTemplate(
|
|
930
|
-
"github.check_run.failed",
|
|
931
|
-
{
|
|
932
|
-
conclusion_emoji: emoji,
|
|
933
|
-
pr_number: prNumber,
|
|
934
|
-
check_name: check_run.name,
|
|
935
|
-
conclusion_label: label,
|
|
936
|
-
repo_full_name: repository.full_name,
|
|
937
|
-
check_url: check_run.html_url,
|
|
938
|
-
output_summary_section: outputSummarySection,
|
|
939
|
-
related_task_id: relatedTask.id,
|
|
940
|
-
},
|
|
941
|
-
{ agentId: lead?.id, repoId: repository.full_name },
|
|
942
|
-
);
|
|
943
|
-
|
|
944
|
-
if (result.skipped) {
|
|
945
|
-
return { created: false };
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
const task = createTaskExtended(result.text, {
|
|
949
|
-
agentId: lead?.id ?? "",
|
|
950
|
-
source: "github",
|
|
951
|
-
vcsProvider: "github",
|
|
952
|
-
taskType: "github-ci",
|
|
953
|
-
vcsRepo: repository.full_name,
|
|
954
|
-
vcsEventType: "check_run",
|
|
955
|
-
vcsNumber: prNumber,
|
|
956
|
-
vcsAuthor: "",
|
|
957
|
-
vcsUrl: check_run.html_url,
|
|
958
|
-
});
|
|
959
|
-
|
|
822
|
+
// Suppressed: see thoughts/taras/plans/2026-03-30-github-event-safety-defaults.md
|
|
823
|
+
const conclusion = check_run.conclusion ?? "unknown";
|
|
960
824
|
console.log(
|
|
961
|
-
`[GitHub]
|
|
825
|
+
`[GitHub:suppressed] check_run.${action} (${conclusion}) on ${repository.full_name} — CI events disabled by default`,
|
|
962
826
|
);
|
|
963
|
-
|
|
964
|
-
return { created: true, taskId: task.id };
|
|
827
|
+
return { created: false };
|
|
965
828
|
}
|
|
966
829
|
|
|
967
830
|
/**
|
|
@@ -974,84 +837,12 @@ export async function handleCheckSuite(
|
|
|
974
837
|
): Promise<{ created: boolean; taskId?: string }> {
|
|
975
838
|
const { action, check_suite, repository } = event;
|
|
976
839
|
|
|
977
|
-
//
|
|
978
|
-
|
|
979
|
-
return { created: false };
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
// Only notify on failure - success notifications would be too noisy
|
|
983
|
-
const conclusion = check_suite.conclusion;
|
|
984
|
-
if (conclusion !== "failure") {
|
|
985
|
-
return { created: false };
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
// Must be associated with at least one PR
|
|
989
|
-
if (!check_suite.pull_requests || check_suite.pull_requests.length === 0) {
|
|
990
|
-
return { created: false };
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
// Check if we have a task for any of these PRs
|
|
994
|
-
let relatedTask = null;
|
|
995
|
-
let prNumber = 0;
|
|
996
|
-
for (const pr of check_suite.pull_requests) {
|
|
997
|
-
const task = findTaskByVcs(repository.full_name, pr.number);
|
|
998
|
-
if (task) {
|
|
999
|
-
relatedTask = task;
|
|
1000
|
-
prNumber = pr.number;
|
|
1001
|
-
break;
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
if (!relatedTask) {
|
|
1006
|
-
// No task for any of the associated PRs
|
|
1007
|
-
return { created: false };
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
// Deduplicate
|
|
1011
|
-
const eventKey = `check-suite:${repository.full_name}:${check_suite.id}`;
|
|
1012
|
-
if (isDuplicate(eventKey)) {
|
|
1013
|
-
return { created: false };
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
const lead = findLeadAgent();
|
|
1017
|
-
const { emoji, label } = getCheckConclusionInfo(conclusion);
|
|
1018
|
-
const branch = check_suite.head_branch ?? "unknown";
|
|
1019
|
-
|
|
1020
|
-
const result = resolveTemplate(
|
|
1021
|
-
"github.check_suite.failed",
|
|
1022
|
-
{
|
|
1023
|
-
conclusion_emoji: emoji,
|
|
1024
|
-
pr_number: prNumber,
|
|
1025
|
-
conclusion_label: label,
|
|
1026
|
-
repo_full_name: repository.full_name,
|
|
1027
|
-
branch,
|
|
1028
|
-
head_sha_short: check_suite.head_sha.substring(0, 7),
|
|
1029
|
-
related_task_id: relatedTask.id,
|
|
1030
|
-
},
|
|
1031
|
-
{ agentId: lead?.id, repoId: repository.full_name },
|
|
1032
|
-
);
|
|
1033
|
-
|
|
1034
|
-
if (result.skipped) {
|
|
1035
|
-
return { created: false };
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
const task = createTaskExtended(result.text, {
|
|
1039
|
-
agentId: lead?.id ?? "",
|
|
1040
|
-
source: "github",
|
|
1041
|
-
vcsProvider: "github",
|
|
1042
|
-
taskType: "github-ci",
|
|
1043
|
-
vcsRepo: repository.full_name,
|
|
1044
|
-
vcsEventType: "check_suite",
|
|
1045
|
-
vcsNumber: prNumber,
|
|
1046
|
-
vcsAuthor: "",
|
|
1047
|
-
vcsUrl: repository.html_url,
|
|
1048
|
-
});
|
|
1049
|
-
|
|
840
|
+
// Suppressed: see thoughts/taras/plans/2026-03-30-github-event-safety-defaults.md
|
|
841
|
+
const conclusion = check_suite.conclusion ?? "unknown";
|
|
1050
842
|
console.log(
|
|
1051
|
-
`[GitHub]
|
|
843
|
+
`[GitHub:suppressed] check_suite.${action} (${conclusion}) on ${repository.full_name} — CI events disabled by default`,
|
|
1052
844
|
);
|
|
1053
|
-
|
|
1054
|
-
return { created: true, taskId: task.id };
|
|
845
|
+
return { created: false };
|
|
1055
846
|
}
|
|
1056
847
|
|
|
1057
848
|
/**
|
|
@@ -1065,87 +856,12 @@ export async function handleCheckSuite(
|
|
|
1065
856
|
export async function handleWorkflowRun(
|
|
1066
857
|
event: WorkflowRunEvent,
|
|
1067
858
|
): Promise<{ created: boolean; taskId?: string }> {
|
|
1068
|
-
const { action, workflow_run,
|
|
1069
|
-
|
|
1070
|
-
// Only handle completed workflow runs
|
|
1071
|
-
if (action !== "completed") {
|
|
1072
|
-
return { created: false };
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
// Only notify on failure - success notifications would be too noisy
|
|
1076
|
-
const conclusion = workflow_run.conclusion;
|
|
1077
|
-
if (conclusion !== "failure") {
|
|
1078
|
-
return { created: false };
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
// Must be associated with at least one PR
|
|
1082
|
-
if (!workflow_run.pull_requests || workflow_run.pull_requests.length === 0) {
|
|
1083
|
-
return { created: false };
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
// Check if we have a task for any of these PRs
|
|
1087
|
-
let relatedTask = null;
|
|
1088
|
-
let prNumber = 0;
|
|
1089
|
-
for (const pr of workflow_run.pull_requests) {
|
|
1090
|
-
const task = findTaskByVcs(repository.full_name, pr.number);
|
|
1091
|
-
if (task) {
|
|
1092
|
-
relatedTask = task;
|
|
1093
|
-
prNumber = pr.number;
|
|
1094
|
-
break;
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
if (!relatedTask) {
|
|
1099
|
-
// No task for any of the associated PRs
|
|
1100
|
-
return { created: false };
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
// Deduplicate
|
|
1104
|
-
const eventKey = `workflow-run:${repository.full_name}:${workflow_run.id}`;
|
|
1105
|
-
if (isDuplicate(eventKey)) {
|
|
1106
|
-
return { created: false };
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
const lead = findLeadAgent();
|
|
1110
|
-
const { emoji, label } = getCheckConclusionInfo(conclusion);
|
|
1111
|
-
|
|
1112
|
-
const result = resolveTemplate(
|
|
1113
|
-
"github.workflow_run.failed",
|
|
1114
|
-
{
|
|
1115
|
-
conclusion_emoji: emoji,
|
|
1116
|
-
pr_number: prNumber,
|
|
1117
|
-
workflow_run_name: workflow_run.name,
|
|
1118
|
-
conclusion_label: label,
|
|
1119
|
-
repo_full_name: repository.full_name,
|
|
1120
|
-
workflow_name: workflow.name,
|
|
1121
|
-
run_number: workflow_run.run_number,
|
|
1122
|
-
head_branch: workflow_run.head_branch,
|
|
1123
|
-
trigger_event: workflow_run.event,
|
|
1124
|
-
logs_url: workflow_run.html_url,
|
|
1125
|
-
related_task_id: relatedTask.id,
|
|
1126
|
-
},
|
|
1127
|
-
{ agentId: lead?.id, repoId: repository.full_name },
|
|
1128
|
-
);
|
|
1129
|
-
|
|
1130
|
-
if (result.skipped) {
|
|
1131
|
-
return { created: false };
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
const task = createTaskExtended(result.text, {
|
|
1135
|
-
agentId: lead?.id ?? "",
|
|
1136
|
-
source: "github",
|
|
1137
|
-
vcsProvider: "github",
|
|
1138
|
-
taskType: "github-ci",
|
|
1139
|
-
vcsRepo: repository.full_name,
|
|
1140
|
-
vcsEventType: "workflow_run",
|
|
1141
|
-
vcsNumber: prNumber,
|
|
1142
|
-
vcsAuthor: "",
|
|
1143
|
-
vcsUrl: workflow_run.html_url,
|
|
1144
|
-
});
|
|
859
|
+
const { action, workflow_run, repository } = event;
|
|
1145
860
|
|
|
861
|
+
// Suppressed: see thoughts/taras/plans/2026-03-30-github-event-safety-defaults.md
|
|
862
|
+
const conclusion = workflow_run.conclusion ?? "unknown";
|
|
1146
863
|
console.log(
|
|
1147
|
-
`[GitHub]
|
|
864
|
+
`[GitHub:suppressed] workflow_run.${action} (${conclusion}) on ${repository.full_name} — CI events disabled by default`,
|
|
1148
865
|
);
|
|
1149
|
-
|
|
1150
|
-
return { created: true, taskId: task.id };
|
|
866
|
+
return { created: false };
|
|
1151
867
|
}
|
package/src/github/index.ts
CHANGED
|
@@ -17,7 +17,14 @@ export {
|
|
|
17
17
|
handlePullRequestReview,
|
|
18
18
|
handleWorkflowRun,
|
|
19
19
|
} from "./handlers";
|
|
20
|
-
export {
|
|
20
|
+
export {
|
|
21
|
+
detectMention,
|
|
22
|
+
extractMentionContext,
|
|
23
|
+
GITHUB_BOT_NAME,
|
|
24
|
+
GITHUB_EVENT_LABELS,
|
|
25
|
+
isBotAssignee,
|
|
26
|
+
isSwarmLabel,
|
|
27
|
+
} from "./mentions";
|
|
21
28
|
export type { ReactionType } from "./reactions";
|
|
22
29
|
export { addIssueReaction, addReaction, postComment } from "./reactions";
|
|
23
30
|
export type {
|
package/src/github/mentions.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
// Bot name for @mentions (can be overridden via env)
|
|
2
2
|
export const GITHUB_BOT_NAME = process.env.GITHUB_BOT_NAME || "agent-swarm-bot";
|
|
3
3
|
|
|
4
|
+
// Labels that trigger agent action on PR/issue label events (comma-separated env var)
|
|
5
|
+
const GITHUB_EVENT_LABELS_RAW = process.env.GITHUB_EVENT_LABELS || "swarm-review";
|
|
6
|
+
export const GITHUB_EVENT_LABELS: string[] = GITHUB_EVENT_LABELS_RAW.split(",")
|
|
7
|
+
.map((l) => l.trim().toLowerCase())
|
|
8
|
+
.filter(Boolean);
|
|
9
|
+
|
|
10
|
+
export function isSwarmLabel(label: string): boolean {
|
|
11
|
+
return GITHUB_EVENT_LABELS.includes(label.toLowerCase());
|
|
12
|
+
}
|
|
13
|
+
|
|
4
14
|
// Additional aliases that also trigger the bot (comma-separated env var)
|
|
5
15
|
// Example: GITHUB_BOT_ALIASES=heysidekick,sidekick,review-bot
|
|
6
16
|
function computeBotNames(): string[] {
|