@desplega.ai/agent-swarm 1.56.2 → 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 CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Agent Swarm API",
5
- "version": "1.55.0",
5
+ "version": "1.56.3",
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": [
@@ -509,6 +509,10 @@
509
509
  "type": "string",
510
510
  "maxLength": 65536
511
511
  },
512
+ "heartbeatMd": {
513
+ "type": "string",
514
+ "maxLength": 65536
515
+ },
512
516
  "changeSource": {
513
517
  "type": "string"
514
518
  },
@@ -1911,6 +1915,27 @@
1911
1915
  }
1912
1916
  }
1913
1917
  },
1918
+ "/api/heartbeat/checklist": {
1919
+ "post": {
1920
+ "summary": "Trigger an immediate heartbeat checklist check",
1921
+ "tags": [
1922
+ "Heartbeat"
1923
+ ],
1924
+ "security": [
1925
+ {
1926
+ "bearerAuth": []
1927
+ }
1928
+ ],
1929
+ "responses": {
1930
+ "200": {
1931
+ "description": "Checklist check completed successfully"
1932
+ },
1933
+ "401": {
1934
+ "description": "Unauthorized"
1935
+ }
1936
+ }
1937
+ }
1938
+ },
1914
1939
  "/api/memory/index": {
1915
1940
  "post": {
1916
1941
  "summary": "Ingest content into memory system (async embedding)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.56.2",
3
+ "version": "1.56.3",
4
4
  "description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
5
5
  "license": "MIT",
6
6
  "author": "desplega.sh <contact@desplega.sh>",
@@ -1,6 +1,12 @@
1
1
  import { createTaskExtended, failTask, findTaskByVcs, getAllAgents } from "../be/db";
2
2
  import { resolveTemplate } from "../prompts/resolver";
3
- import { detectMention, extractMentionContext, GITHUB_BOT_NAME, isBotAssignee } from "./mentions";
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 closed action - PR was merged or closed without merge
319
- if (action === "closed") {
320
- // Find the related task
321
- const task = findTaskByVcs(repository.full_name, pr.number);
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-closed:${repository.full_name}:${pr.number}`;
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.closed",
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
- related_task_id: task.id,
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 notifyTask = createTaskExtended(result.text, {
358
+ const task = createTaskExtended(result.text, {
363
359
  agentId: lead?.id ?? "",
364
360
  source: "github",
365
361
  vcsProvider: "github",
366
- taskType: "github-pr-status",
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
- console.log(
375
- `[GitHub] Created task ${notifyTask.id} for PR #${pr.number} (${status}) -> ${lead?.name ?? "unassigned"}`,
376
- );
377
-
378
- return { created: true, taskId: notifyTask.id };
379
- }
380
-
381
- // Handle synchronize action - new commits pushed to PR
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
- const lead = findLeadAgent();
397
- const result = resolveTemplate(
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
- const notifyTask = createTaskExtended(result.text, {
416
- agentId: lead?.id ?? "",
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] Created task ${notifyTask.id} for PR #${pr.number} (synchronize) -> ${lead?.name ?? "unassigned"}`,
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
- return { created: true, taskId: notifyTask.id };
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, review, pull_request: pr, repository, sender, installation } = event;
803
+ const { action, pull_request: pr, repository } = event;
774
804
 
775
- // Only handle submitted reviews (the most important action)
776
- // Edited reviews are less common and dismissed is handled by the state
777
- if (action !== "submitted") {
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
- // Only handle completed check runs
883
- if (action !== "completed") {
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] Created task ${task.id} for check_run ${check_run.name} (${conclusion}) on PR #${prNumber} -> ${lead?.name ?? "unassigned"}`,
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
- // Only handle completed check suites
978
- if (action !== "completed") {
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] Created task ${task.id} for check_suite (${conclusion}) on PR #${prNumber} -> ${lead?.name ?? "unassigned"}`,
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, workflow, repository } = event;
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] Created task ${task.id} for workflow_run "${workflow_run.name}" (${conclusion}) on PR #${prNumber} -> ${lead?.name ?? "unassigned"}`,
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
  }
@@ -17,7 +17,14 @@ export {
17
17
  handlePullRequestReview,
18
18
  handleWorkflowRun,
19
19
  } from "./handlers";
20
- export { detectMention, extractMentionContext, GITHUB_BOT_NAME, isBotAssignee } from "./mentions";
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 {
@@ -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[] {
@@ -197,6 +197,35 @@ Context:
197
197
  category: "event",
198
198
  });
199
199
 
200
+ registerTemplate({
201
+ eventType: "github.pull_request.labeled",
202
+ header: "[GitHub PR #{{pr_number}}] {{pr_title}}",
203
+ defaultBody: `Label added: {{label_name}}
204
+ From: {{sender_login}}
205
+ Repo: {{repo_full_name}}
206
+ Branch: {{head_ref}} → {{base_ref}}
207
+ URL: {{pr_url}}
208
+
209
+ Context:
210
+ {{context}}
211
+
212
+ ---
213
+ {{@template[common.delegation_instruction]}}
214
+ {{@template[common.command_suggestions.github_pr]}}`,
215
+ variables: [
216
+ { name: "pr_number", description: "Pull request number" },
217
+ { name: "pr_title", description: "Pull request title" },
218
+ { name: "label_name", description: "Label that was added" },
219
+ { name: "sender_login", description: "Event sender login" },
220
+ { name: "repo_full_name", description: "Repository full name (owner/repo)" },
221
+ { name: "head_ref", description: "Head branch name" },
222
+ { name: "base_ref", description: "Base branch name" },
223
+ { name: "pr_url", description: "Pull request HTML URL" },
224
+ { name: "context", description: "PR body or title as context" },
225
+ ],
226
+ category: "event",
227
+ });
228
+
200
229
  // ============================================================================
201
230
  // Issue events
202
231
  // ============================================================================
@@ -251,6 +280,32 @@ Context:
251
280
  category: "event",
252
281
  });
253
282
 
283
+ registerTemplate({
284
+ eventType: "github.issue.labeled",
285
+ header: "[GitHub Issue #{{issue_number}}] {{issue_title}}",
286
+ defaultBody: `Label added: {{label_name}}
287
+ From: {{sender_login}}
288
+ Repo: {{repo_full_name}}
289
+ URL: {{issue_url}}
290
+
291
+ Context:
292
+ {{context}}
293
+
294
+ ---
295
+ {{@template[common.delegation_instruction]}}
296
+ {{@template[common.command_suggestions.github_issue]}}`,
297
+ variables: [
298
+ { name: "issue_number", description: "Issue number" },
299
+ { name: "issue_title", description: "Issue title" },
300
+ { name: "label_name", description: "Label that was added" },
301
+ { name: "sender_login", description: "Event sender login" },
302
+ { name: "repo_full_name", description: "Repository full name (owner/repo)" },
303
+ { name: "issue_url", description: "Issue HTML URL" },
304
+ { name: "context", description: "Issue body or title as context" },
305
+ ],
306
+ category: "event",
307
+ });
308
+
254
309
  // ============================================================================
255
310
  // Comment events
256
311
  // ============================================================================
@@ -20,6 +20,7 @@ export interface PullRequestEvent extends GitHubWebhookEvent {
20
20
  changed_files?: number;
21
21
  };
22
22
  requested_reviewer?: { login: string; id: number }; // Added for review_requested/review_request_removed events
23
+ label?: { id: number; name: string; color: string }; // Added for labeled/unlabeled events
23
24
  }
24
25
 
25
26
  export interface IssueEvent extends GitHubWebhookEvent {
@@ -30,6 +31,7 @@ export interface IssueEvent extends GitHubWebhookEvent {
30
31
  html_url: string;
31
32
  user: { login: string };
32
33
  };
34
+ label?: { id: number; name: string; color: string }; // Added for labeled/unlabeled events
33
35
  }
34
36
 
35
37
  export interface CommentEvent extends GitHubWebhookEvent {
package/src/hooks/hook.ts CHANGED
@@ -14,6 +14,7 @@ const CLAUDE_MD_BACKUP_PATH = `${process.env.HOME}/.claude/CLAUDE.md.bak`;
14
14
  const SOUL_MD_PATH = "/workspace/SOUL.md";
15
15
  const IDENTITY_MD_PATH = "/workspace/IDENTITY.md";
16
16
  const TOOLS_MD_PATH = "/workspace/TOOLS.md";
17
+ const HEARTBEAT_MD_PATH = "/workspace/HEARTBEAT.md";
17
18
  const SETUP_SCRIPT_PATH = "/workspace/start-up.sh";
18
19
 
19
20
  type McpServerConfig = {
@@ -389,6 +390,14 @@ export async function handleHook(): Promise<void> {
389
390
  }
390
391
  }
391
392
 
393
+ const heartbeatFile = Bun.file(HEARTBEAT_MD_PATH);
394
+ if (await heartbeatFile.exists()) {
395
+ const content = await heartbeatFile.text();
396
+ if (content.length <= 65536) {
397
+ updates.heartbeatMd = content;
398
+ }
399
+ }
400
+
392
401
  if (Object.keys(updates).length === 0) return;
393
402
 
394
403
  try {
@@ -910,7 +919,8 @@ ${hasAgentIdHeader() ? `You have a pre-defined agent ID via header: ${mcpConfig?
910
919
  if (
911
920
  editedPath === SOUL_MD_PATH ||
912
921
  editedPath === IDENTITY_MD_PATH ||
913
- editedPath === TOOLS_MD_PATH
922
+ editedPath === TOOLS_MD_PATH ||
923
+ editedPath === HEARTBEAT_MD_PATH
914
924
  ) {
915
925
  await syncIdentityFilesToServer(agentInfo.id, "self_edit");
916
926
  }
@@ -77,6 +77,10 @@ Available Slack tools:
77
77
  - \`slack-read\`: Read thread/channel history (use taskId or channelId)
78
78
  - \`slack-list-channels\`: Discover available Slack channels the bot can access
79
79
 
80
+ #### Identity & profile tools
81
+
82
+ - \`update-profile\`: Update your own profile fields (name, role, capabilities, soulMd, identityMd, heartbeatMd, claudeMd, toolsMd, setupScript). As lead, you can also update other agents' profiles to shape their behavior.
83
+
80
84
  #### General monitor and control tools
81
85
 
82
86
  - \`get-swarm\`: Get the list of all workers in the swarm along with their status
@@ -226,6 +230,37 @@ Task: {describe what needs to be done}
226
230
  - Complex feature/major refactor → Use PLANNING first, then IMPLEMENTATION
227
231
  - Bug fix/small code change → Use QUICK FIX template
228
232
  - Non-code task/question → Use GENERAL template
233
+
234
+ #### Heartbeat Checklist
235
+
236
+ The system reads your \`/workspace/HEARTBEAT.md\` every 30 minutes. If it has content (not just
237
+ comments or empty lines), it creates a \`heartbeat-checklist\` task for you containing:
238
+ 1. **Auto-generated system status** — task counts, stalled tasks, agent health, idle workers, unassigned work
239
+ 2. **Your standing orders** — whatever you wrote in HEARTBEAT.md
240
+
241
+ **How to configure:**
242
+ - **Edit the file directly:** Open \`/workspace/HEARTBEAT.md\` and write your standing orders. Changes sync to the database automatically on save.
243
+ - **Use \`update-profile\`:** Call \`update-profile\` with the \`heartbeatMd\` field set to your checklist content. This updates both the database and the file.
244
+
245
+ **What to put in HEARTBEAT.md** — a plain markdown list of actionable standing orders:
246
+ \`\`\`markdown
247
+ - Check Slack for unaddressed requests older than 1 hour
248
+ - Review active tasks for any that seem stuck or need follow-up
249
+ - If idle workers exist and unassigned tasks are available, investigate why
250
+ - Post a daily summary to #agent-status at 5pm
251
+ \`\`\`
252
+
253
+ **Key mechanics:**
254
+ - **Empty = disabled** — leave HEARTBEAT.md empty (or all HTML comments) to skip checks at zero LLM cost
255
+ - **System status is automatic** — don't gather it yourself, it's injected into every checklist task
256
+ - **Don't create checklist tasks yourself** — the system handles scheduling. Complete your current one and the next arrives in ~30 minutes
257
+ - **Boot triage** — after a server restart, you get a one-time higher-priority checklist task within 30 seconds
258
+
259
+ **When you receive a checklist task:**
260
+ 1. Review the system status for anything that needs attention
261
+ 2. Review your standing orders for any periodic checks or actions due
262
+ 3. If something needs action — do it now (create tasks, post to Slack, etc.)
263
+ 4. If everything is healthy — complete the task with a brief "All clear" summary
229
264
  `,
230
265
  variables: [],
231
266
  category: "system",
@@ -203,11 +203,11 @@ describe("Concurrency Integration Tests", () => {
203
203
  isLead: false,
204
204
  status: "idle",
205
205
  capabilities: [],
206
- maxTasks: 20, // Max allowed by schema
206
+ maxTasks: 100, // Max allowed by schema
207
207
  });
208
208
 
209
- expect(agent.maxTasks).toBe(20);
210
- expect(getRemainingCapacity(agent.id)).toBe(20);
209
+ expect(agent.maxTasks).toBe(100);
210
+ expect(getRemainingCapacity(agent.id)).toBe(100);
211
211
  });
212
212
  });
213
213
  });
@@ -0,0 +1,301 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { closeDb, createAgent, initDb } from "../be/db";
4
+ import {
5
+ handleCheckRun,
6
+ handleCheckSuite,
7
+ handleComment,
8
+ handleIssue,
9
+ handlePullRequest,
10
+ handlePullRequestReview,
11
+ handleWorkflowRun,
12
+ } from "../github/handlers";
13
+ import { GITHUB_BOT_NAME } from "../github/mentions";
14
+ import type {
15
+ CheckRunEvent,
16
+ CheckSuiteEvent,
17
+ CommentEvent,
18
+ IssueEvent,
19
+ PullRequestEvent,
20
+ PullRequestReviewEvent,
21
+ WorkflowRunEvent,
22
+ } from "../github/types";
23
+
24
+ const TEST_DB_PATH = "./test-github-event-filter.sqlite";
25
+
26
+ // ── Setup ──
27
+
28
+ beforeAll(async () => {
29
+ await unlink(TEST_DB_PATH).catch(() => {});
30
+ await unlink(`${TEST_DB_PATH}-wal`).catch(() => {});
31
+ await unlink(`${TEST_DB_PATH}-shm`).catch(() => {});
32
+ initDb(TEST_DB_PATH);
33
+ // Create a lead agent so handlers can assign tasks
34
+ createAgent({
35
+ id: "lead-gh-001",
36
+ name: "GitHubTestLead",
37
+ status: "idle",
38
+ isLead: true,
39
+ });
40
+ });
41
+
42
+ afterAll(async () => {
43
+ closeDb();
44
+ await unlink(TEST_DB_PATH).catch(() => {});
45
+ await unlink(`${TEST_DB_PATH}-wal`).catch(() => {});
46
+ await unlink(`${TEST_DB_PATH}-shm`).catch(() => {});
47
+ });
48
+
49
+ // ── Helpers ──
50
+
51
+ const BASE_REPO = { full_name: "test/repo", html_url: "https://github.com/test/repo" };
52
+ const BASE_SENDER = { login: "testuser" };
53
+ const BASE_PR = {
54
+ number: 1,
55
+ title: "Test PR",
56
+ body: null as string | null,
57
+ html_url: "https://github.com/test/repo/pull/1",
58
+ user: { login: "testuser" },
59
+ head: { ref: "feature", sha: "abc1234567890" },
60
+ base: { ref: "main" },
61
+ merged: false,
62
+ merged_by: undefined,
63
+ };
64
+
65
+ function makePREvent(overrides: Partial<PullRequestEvent> = {}): PullRequestEvent {
66
+ return {
67
+ action: "opened",
68
+ pull_request: BASE_PR,
69
+ repository: BASE_REPO,
70
+ sender: BASE_SENDER,
71
+ ...overrides,
72
+ };
73
+ }
74
+
75
+ function makeIssueEvent(overrides: Partial<IssueEvent> = {}): IssueEvent {
76
+ return {
77
+ action: "opened",
78
+ issue: {
79
+ number: 10,
80
+ title: "Test Issue",
81
+ body: null,
82
+ html_url: "https://github.com/test/repo/issues/10",
83
+ user: { login: "testuser" },
84
+ },
85
+ repository: BASE_REPO,
86
+ sender: BASE_SENDER,
87
+ ...overrides,
88
+ };
89
+ }
90
+
91
+ // ── Suppressed events ──
92
+
93
+ describe("suppressed cascade events", () => {
94
+ test("pull_request.closed returns created: false", async () => {
95
+ const result = await handlePullRequest(makePREvent({ action: "closed" }));
96
+ expect(result.created).toBe(false);
97
+ });
98
+
99
+ test("pull_request.synchronize returns created: false", async () => {
100
+ const result = await handlePullRequest(makePREvent({ action: "synchronize" }));
101
+ expect(result.created).toBe(false);
102
+ });
103
+
104
+ test("pull_request_review.submitted returns created: false", async () => {
105
+ const event: PullRequestReviewEvent = {
106
+ action: "submitted",
107
+ review: {
108
+ id: 1,
109
+ body: "Looks good",
110
+ state: "approved",
111
+ html_url: "https://github.com/test/repo/pull/1#pullrequestreview-1",
112
+ user: { login: "reviewer" },
113
+ submitted_at: "2026-01-01T00:00:00Z",
114
+ },
115
+ pull_request: {
116
+ number: 1,
117
+ title: "Test PR",
118
+ body: null,
119
+ html_url: "https://github.com/test/repo/pull/1",
120
+ user: { login: "testuser" },
121
+ head: { ref: "feature" },
122
+ base: { ref: "main" },
123
+ },
124
+ repository: BASE_REPO,
125
+ sender: { login: "reviewer" },
126
+ };
127
+ const result = await handlePullRequestReview(event);
128
+ expect(result.created).toBe(false);
129
+ });
130
+
131
+ test("check_run.completed with failure returns created: false", async () => {
132
+ const event: CheckRunEvent = {
133
+ action: "completed",
134
+ check_run: {
135
+ id: 1,
136
+ name: "ci/test",
137
+ status: "completed",
138
+ conclusion: "failure",
139
+ html_url: "https://github.com/test/repo/runs/1",
140
+ started_at: "2026-01-01T00:00:00Z",
141
+ completed_at: "2026-01-01T00:01:00Z",
142
+ output: { title: null, summary: null },
143
+ check_suite: { id: 1, head_sha: "abc1234" },
144
+ pull_requests: [{ number: 1, head: { sha: "abc1234" } }],
145
+ },
146
+ repository: BASE_REPO,
147
+ sender: BASE_SENDER,
148
+ };
149
+ const result = await handleCheckRun(event);
150
+ expect(result.created).toBe(false);
151
+ });
152
+
153
+ test("check_suite.completed with failure returns created: false", async () => {
154
+ const event: CheckSuiteEvent = {
155
+ action: "completed",
156
+ check_suite: {
157
+ id: 1,
158
+ status: "completed",
159
+ conclusion: "failure",
160
+ head_sha: "abc1234567890",
161
+ head_branch: "feature",
162
+ pull_requests: [{ number: 1, head: { sha: "abc1234" } }],
163
+ },
164
+ repository: BASE_REPO,
165
+ sender: BASE_SENDER,
166
+ };
167
+ const result = await handleCheckSuite(event);
168
+ expect(result.created).toBe(false);
169
+ });
170
+
171
+ test("workflow_run.completed with failure returns created: false", async () => {
172
+ const event: WorkflowRunEvent = {
173
+ action: "completed",
174
+ workflow_run: {
175
+ id: 1,
176
+ name: "CI",
177
+ head_branch: "feature",
178
+ head_sha: "abc1234567890",
179
+ status: "completed",
180
+ conclusion: "failure",
181
+ html_url: "https://github.com/test/repo/actions/runs/1",
182
+ run_number: 42,
183
+ event: "pull_request",
184
+ pull_requests: [{ number: 1, head: { sha: "abc1234" } }],
185
+ },
186
+ workflow: { id: 1, name: "CI", path: ".github/workflows/ci.yml" },
187
+ repository: BASE_REPO,
188
+ sender: BASE_SENDER,
189
+ };
190
+ const result = await handleWorkflowRun(event);
191
+ expect(result.created).toBe(false);
192
+ });
193
+ });
194
+
195
+ // ── Explicit actions still create tasks ──
196
+
197
+ describe("explicit actions create tasks", () => {
198
+ test("issue_comment with @mention creates task", async () => {
199
+ const event: CommentEvent = {
200
+ action: "created",
201
+ comment: {
202
+ id: 100,
203
+ body: `@${GITHUB_BOT_NAME} please review this`,
204
+ html_url: "https://github.com/test/repo/issues/10#issuecomment-100",
205
+ user: { login: "testuser" },
206
+ },
207
+ issue: {
208
+ number: 10,
209
+ title: "Test Issue",
210
+ html_url: "https://github.com/test/repo/issues/10",
211
+ },
212
+ repository: BASE_REPO,
213
+ sender: BASE_SENDER,
214
+ };
215
+ const result = await handleComment(event, "issue_comment");
216
+ expect(result.created).toBe(true);
217
+ expect(result.taskId).toBeDefined();
218
+ });
219
+
220
+ test("pull_request.review_requested with bot as reviewer creates task", async () => {
221
+ const event = makePREvent({
222
+ action: "review_requested",
223
+ requested_reviewer: { login: GITHUB_BOT_NAME, id: 1 },
224
+ });
225
+ const result = await handlePullRequest(event);
226
+ expect(result.created).toBe(true);
227
+ expect(result.taskId).toBeDefined();
228
+ });
229
+
230
+ test("pull_request.assigned with bot as assignee creates task", async () => {
231
+ const event = makePREvent({
232
+ action: "assigned",
233
+ pull_request: { ...BASE_PR, number: 2 },
234
+ assignee: { login: GITHUB_BOT_NAME, id: 1 },
235
+ });
236
+ const result = await handlePullRequest(event);
237
+ expect(result.created).toBe(true);
238
+ expect(result.taskId).toBeDefined();
239
+ });
240
+
241
+ test("pull_request.labeled with matching label creates task", async () => {
242
+ const event = makePREvent({
243
+ action: "labeled",
244
+ pull_request: { ...BASE_PR, number: 3 },
245
+ label: { id: 1, name: "swarm-review", color: "0075ca" },
246
+ });
247
+ const result = await handlePullRequest(event);
248
+ expect(result.created).toBe(true);
249
+ expect(result.taskId).toBeDefined();
250
+ });
251
+
252
+ test("pull_request.labeled with non-matching label does not create task", async () => {
253
+ const event = makePREvent({
254
+ action: "labeled",
255
+ pull_request: { ...BASE_PR, number: 4 },
256
+ label: { id: 2, name: "bug", color: "d73a4a" },
257
+ });
258
+ const result = await handlePullRequest(event);
259
+ expect(result.created).toBe(false);
260
+ });
261
+
262
+ test("issue.labeled with matching label creates task", async () => {
263
+ const event = makeIssueEvent({
264
+ action: "labeled",
265
+ label: { id: 1, name: "swarm-review", color: "0075ca" },
266
+ });
267
+ const result = await handleIssue(event);
268
+ expect(result.created).toBe(true);
269
+ expect(result.taskId).toBeDefined();
270
+ });
271
+
272
+ test("issue.labeled with non-matching label does not create task", async () => {
273
+ const event = makeIssueEvent({
274
+ action: "labeled",
275
+ issue: {
276
+ number: 11,
277
+ title: "Another Issue",
278
+ body: null,
279
+ html_url: "https://github.com/test/repo/issues/11",
280
+ user: { login: "testuser" },
281
+ },
282
+ label: { id: 2, name: "enhancement", color: "a2eeef" },
283
+ });
284
+ const result = await handleIssue(event);
285
+ expect(result.created).toBe(false);
286
+ });
287
+
288
+ test("pull_request.opened with @mention creates task", async () => {
289
+ const event = makePREvent({
290
+ action: "opened",
291
+ pull_request: {
292
+ ...BASE_PR,
293
+ number: 5,
294
+ title: `@${GITHUB_BOT_NAME} review this PR`,
295
+ },
296
+ });
297
+ const result = await handlePullRequest(event);
298
+ expect(result.created).toBe(true);
299
+ expect(result.taskId).toBeDefined();
300
+ });
301
+ });
@@ -0,0 +1,24 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { GITHUB_EVENT_LABELS, isSwarmLabel } from "../github/mentions";
3
+
4
+ describe("isSwarmLabel", () => {
5
+ test("default label 'swarm-review' matches", () => {
6
+ expect(isSwarmLabel("swarm-review")).toBe(true);
7
+ });
8
+
9
+ test("case-insensitive matching", () => {
10
+ expect(isSwarmLabel("Swarm-Review")).toBe(true);
11
+ expect(isSwarmLabel("SWARM-REVIEW")).toBe(true);
12
+ });
13
+
14
+ test("non-matching labels return false", () => {
15
+ expect(isSwarmLabel("bug")).toBe(false);
16
+ expect(isSwarmLabel("enhancement")).toBe(false);
17
+ expect(isSwarmLabel("")).toBe(false);
18
+ });
19
+
20
+ test("GITHUB_EVENT_LABELS is populated", () => {
21
+ expect(GITHUB_EVENT_LABELS.length).toBeGreaterThan(0);
22
+ expect(GITHUB_EVENT_LABELS).toContain("swarm-review");
23
+ });
24
+ });
package/src/types.ts CHANGED
@@ -179,7 +179,7 @@ export const AgentSchema = z.object({
179
179
  heartbeatMd: z.string().max(65536).optional(),
180
180
 
181
181
  // Concurrency limit (defaults to 1 for backwards compatibility)
182
- maxTasks: z.number().int().min(1).max(20).optional(),
182
+ maxTasks: z.number().int().min(1).max(100).optional(),
183
183
 
184
184
  // Polling limit tracking (consecutive empty polls)
185
185
  emptyPollCount: z.number().int().min(0).optional(),