@desplega.ai/agent-swarm 1.91.0 → 1.92.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +3 -2
  2. package/openapi.json +1005 -152
  3. package/package.json +6 -6
  4. package/plugin/skills/pages/SKILL.md +5 -2
  5. package/src/be/db.ts +662 -19
  6. package/src/be/memory/constants.ts +2 -1
  7. package/src/be/memory/providers/openai-embedding.ts +2 -5
  8. package/src/be/memory/providers/sqlite-store.ts +293 -76
  9. package/src/be/memory/types.ts +35 -0
  10. package/src/be/migrations/083_script_workflows.sql +51 -0
  11. package/src/be/migrations/084_script_run_journal_duration.sql +5 -0
  12. package/src/be/migrations/085_script_runs_kind.sql +9 -0
  13. package/src/be/migrations/086_pages_default_authed.sql +64 -0
  14. package/src/be/migrations/087_skill_files.sql +19 -0
  15. package/src/be/modelsdev-cache.json +42310 -38617
  16. package/src/be/scripts/typecheck.ts +49 -0
  17. package/src/be/seed-scripts/catalog/boot-triage.ts +221 -0
  18. package/src/be/seed-scripts/catalog/catalog-report.ts +457 -0
  19. package/src/be/seed-scripts/catalog/compound-insights.ts +310 -6
  20. package/src/be/seed-scripts/catalog/gh-pr-snapshot.ts +1 -1
  21. package/src/be/seed-scripts/catalog/memory-eval.ts +1059 -0
  22. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +506 -0
  23. package/src/be/seed-scripts/catalog/schedule-health.ts +78 -2
  24. package/src/be/seed-scripts/catalog/task-context-gathering.ts +92 -0
  25. package/src/be/seed-scripts/catalog/task-failure-audit.ts +48 -1
  26. package/src/be/seed-scripts/catalog/tool-usage.ts +6 -3
  27. package/src/be/seed-scripts/index.ts +51 -5
  28. package/src/be/seed-skills/index.ts +3 -3
  29. package/src/be/skill-sync.ts +91 -7
  30. package/src/be/swarm-config-guard.ts +17 -0
  31. package/src/commands/runner.ts +49 -4
  32. package/src/heartbeat/templates.ts +20 -16
  33. package/src/http/db-query.ts +20 -5
  34. package/src/http/index.ts +51 -7
  35. package/src/http/mcp-user.ts +23 -0
  36. package/src/http/mcp.ts +58 -0
  37. package/src/http/memory.ts +58 -0
  38. package/src/http/pages.ts +1 -1
  39. package/src/http/script-runs.ts +557 -0
  40. package/src/http/scripts.ts +39 -2
  41. package/src/http/skills.ts +225 -0
  42. package/src/prompts/session-templates.ts +24 -4
  43. package/src/providers/claude-adapter.ts +107 -28
  44. package/src/script-workflows/executor.ts +110 -0
  45. package/src/script-workflows/harness.ts +73 -0
  46. package/src/script-workflows/label-lint.ts +51 -0
  47. package/src/script-workflows/limits.ts +22 -0
  48. package/src/script-workflows/supervisor.ts +139 -0
  49. package/src/script-workflows/workflow-ctx.ts +209 -0
  50. package/src/scripts-runtime/sdk-allowlist.ts +4 -0
  51. package/src/scripts-runtime/swarm-sdk.ts +13 -0
  52. package/src/scripts-runtime/types/stdlib.d.ts +61 -0
  53. package/src/scripts-runtime/types/swarm-sdk.d.ts +61 -0
  54. package/src/server.ts +4 -0
  55. package/src/slack/handlers.ts +11 -4
  56. package/src/slack/message-text.ts +98 -0
  57. package/src/slack/thread-buffer.ts +5 -3
  58. package/src/tests/claude-adapter-binary.test.ts +271 -74
  59. package/src/tests/create-page-tool.test.ts +19 -2
  60. package/src/tests/db-query.test.ts +28 -0
  61. package/src/tests/error-tracker.test.ts +121 -0
  62. package/src/tests/harness-provider-resolution.test.ts +33 -0
  63. package/src/tests/heartbeat-checklist.test.ts +36 -0
  64. package/src/tests/mcp-tools.test.ts +6 -0
  65. package/src/tests/mcp-transport-gc.test.ts +58 -0
  66. package/src/tests/memory-health-endpoint.test.ts +78 -0
  67. package/src/tests/memory-store.test.ts +221 -1
  68. package/src/tests/pages-http.test.ts +20 -2
  69. package/src/tests/pages-storage.test.ts +26 -0
  70. package/src/tests/prompt-template-session.test.ts +34 -5
  71. package/src/tests/script-runs-http.test.ts +278 -0
  72. package/src/tests/script-workflows-label-lint.test.ts +43 -0
  73. package/src/tests/script-workflows-runtime-e2e.test.ts +170 -0
  74. package/src/tests/scripts-mcp-e2e.test.ts +102 -2
  75. package/src/tests/seed-scripts.test.ts +468 -3
  76. package/src/tests/skill-files-http.test.ts +171 -0
  77. package/src/tests/skill-files.test.ts +162 -0
  78. package/src/tests/skill-get-file-tool.test.ts +110 -0
  79. package/src/tests/skill-sync.test.ts +125 -6
  80. package/src/tests/slack-message-text.test.ts +250 -0
  81. package/src/tests/system-default-skills.test.ts +40 -0
  82. package/src/tools/create-page.ts +2 -2
  83. package/src/tools/db-query.ts +16 -6
  84. package/src/tools/script-runs.ts +123 -0
  85. package/src/tools/skills/index.ts +1 -0
  86. package/src/tools/skills/skill-get-file.ts +80 -0
  87. package/src/tools/slack-read.ts +12 -3
  88. package/src/tools/tool-config.ts +6 -2
  89. package/src/types.ts +72 -0
  90. package/src/utils/error-tracker.ts +40 -1
  91. package/src/utils/internal-ai/complete-structured.ts +10 -4
  92. package/src/workflows/executors/raw-llm.ts +76 -59
  93. package/templates/schedules/daily-blocker-digest/content.md +68 -54
  94. package/templates/schedules/daily-compounding-reflection/content.md +4 -4
  95. package/templates/schedules/daily-hn-briefing/content.md +5 -5
  96. package/templates/schedules/daily-workflow-health-audit/content.md +6 -6
  97. package/templates/schedules/gtm-weekly-review/content.md +9 -9
  98. package/templates/schedules/weekly-dependabot-triage/content.md +24 -20
  99. package/templates/skills/agentmail-sending/content.md +6 -7
  100. package/templates/skills/desloppify/content.md +8 -9
  101. package/templates/skills/jira-interaction/content.md +25 -33
  102. package/templates/skills/kapso-whatsapp/content.md +29 -30
  103. package/templates/skills/linear-interaction/content.md +8 -9
  104. package/templates/skills/pages/content.md +205 -55
  105. package/templates/skills/profile-corruption-escalation/content.md +44 -85
  106. package/templates/skills/script-workflows/config.json +14 -0
  107. package/templates/skills/script-workflows/content.md +68 -0
  108. package/templates/skills/sprite-cli/content.md +4 -5
  109. package/templates/skills/swarm-scripts/content.md +2 -3
  110. package/templates/skills/turso-interaction/content.md +14 -17
  111. package/templates/skills/workflow-iterate/content.md +38 -391
  112. package/templates/skills/x-api-interactions/content.md +4 -6
  113. package/templates/skills/scheduled-task-resilience/config.json +0 -14
  114. package/templates/skills/scheduled-task-resilience/content.md +0 -95
@@ -3,13 +3,18 @@ import { z } from "zod";
3
3
  import {
4
4
  createSkill,
5
5
  deleteSkill,
6
+ deleteSkillFile,
6
7
  getAgentSkills,
7
8
  getSkillById,
9
+ getSkillFile,
8
10
  installSkill,
11
+ listSkillFileManifest,
9
12
  listSkills,
10
13
  searchSkills,
11
14
  uninstallSkill,
12
15
  updateSkill,
16
+ upsertSkillFile,
17
+ upsertSkillFiles,
13
18
  } from "../be/db";
14
19
  import { parseSkillContent } from "../be/skill-parser";
15
20
  import { computeAgentSkillsSignature, syncSkillsToFilesystem } from "../be/skill-sync";
@@ -19,6 +24,24 @@ import { json, jsonError } from "./utils";
19
24
  const SYSTEM_DEFAULT_SKILL_LOCKED_MESSAGE =
20
25
  "This skill is system-managed and cannot be edited from the UI; it is re-seeded on each start. Fork it under a new name to customize.";
21
26
 
27
+ const skillFileBodySchema = z.object({
28
+ content: z.string(),
29
+ mimeType: z.string().optional(),
30
+ isBinary: z.boolean().optional(),
31
+ size: z.number().int().nonnegative().optional(),
32
+ });
33
+
34
+ const skillFileWithPathSchema = skillFileBodySchema.extend({
35
+ path: z.string().min(1),
36
+ });
37
+
38
+ function decodeSkillFilePath(pathSegments: string[]): string {
39
+ return pathSegments
40
+ .slice(4)
41
+ .map((segment) => decodeURIComponent(segment))
42
+ .join("/");
43
+ }
44
+
22
45
  // ─── Route Definitions ───────────────────────────────────────────────────────
23
46
 
24
47
  const listSkillsRoute = route({
@@ -58,6 +81,86 @@ const getSkillRoute = route({
58
81
  },
59
82
  });
60
83
 
84
+ const listSkillFilesRoute = route({
85
+ method: "get",
86
+ path: "/api/skills/{id}/files",
87
+ pattern: ["api", "skills", null, "files"],
88
+ summary: "List bundled files for a skill",
89
+ description: "Returns a manifest of bundled skill files without file content.",
90
+ tags: ["Skills"],
91
+ auth: { apiKey: true },
92
+ params: z.object({ id: z.string() }),
93
+ responses: {
94
+ 200: { description: "Skill file manifest" },
95
+ 404: { description: "Skill not found" },
96
+ },
97
+ });
98
+
99
+ const bulkUpsertSkillFilesRoute = route({
100
+ method: "post",
101
+ path: "/api/skills/{id}/files",
102
+ pattern: ["api", "skills", null, "files"],
103
+ summary: "Bulk upsert bundled files for a skill",
104
+ tags: ["Skills"],
105
+ auth: { apiKey: true },
106
+ params: z.object({ id: z.string() }),
107
+ body: z.object({
108
+ files: z.array(skillFileWithPathSchema).max(100),
109
+ }),
110
+ responses: {
111
+ 200: { description: "Skill files upserted" },
112
+ 400: { description: "Validation error" },
113
+ 404: { description: "Skill not found" },
114
+ },
115
+ });
116
+
117
+ const getSkillFileRoute = route({
118
+ method: "get",
119
+ path: "/api/skills/{id}/files/{path}",
120
+ pattern: ["api", "skills", null, "files", null],
121
+ exact: false,
122
+ summary: "Get a bundled skill file",
123
+ tags: ["Skills"],
124
+ auth: { apiKey: true },
125
+ params: z.object({ id: z.string(), path: z.string() }),
126
+ responses: {
127
+ 200: { description: "Skill file" },
128
+ 404: { description: "Skill or file not found" },
129
+ },
130
+ });
131
+
132
+ const upsertSkillFileRoute = route({
133
+ method: "put",
134
+ path: "/api/skills/{id}/files/{path}",
135
+ pattern: ["api", "skills", null, "files", null],
136
+ exact: false,
137
+ summary: "Upsert a bundled skill file",
138
+ tags: ["Skills"],
139
+ auth: { apiKey: true },
140
+ params: z.object({ id: z.string(), path: z.string() }),
141
+ body: skillFileBodySchema,
142
+ responses: {
143
+ 200: { description: "Skill file upserted" },
144
+ 400: { description: "Validation error" },
145
+ 404: { description: "Skill not found" },
146
+ },
147
+ });
148
+
149
+ const deleteSkillFileRoute = route({
150
+ method: "delete",
151
+ path: "/api/skills/{id}/files/{path}",
152
+ pattern: ["api", "skills", null, "files", null],
153
+ exact: false,
154
+ summary: "Delete a bundled skill file",
155
+ tags: ["Skills"],
156
+ auth: { apiKey: true },
157
+ params: z.object({ id: z.string(), path: z.string() }),
158
+ responses: {
159
+ 200: { description: "Skill file deleted" },
160
+ 404: { description: "Skill or file not found" },
161
+ },
162
+ });
163
+
61
164
  const createSkillRoute = route({
62
165
  method: "post",
63
166
  path: "/api/skills",
@@ -423,6 +526,128 @@ export async function handleSkills(
423
526
  return true;
424
527
  }
425
528
 
529
+ // GET /api/skills/:id/files
530
+ if (listSkillFilesRoute.match(req.method, pathSegments)) {
531
+ const parsed = await listSkillFilesRoute.parse(req, res, pathSegments, queryParams);
532
+ if (!parsed) return true;
533
+
534
+ const skill = getSkillById(parsed.params.id);
535
+ if (!skill) {
536
+ jsonError(res, "Skill not found", 404);
537
+ return true;
538
+ }
539
+
540
+ const files = listSkillFileManifest(parsed.params.id);
541
+ json(res, { files, total: files.length });
542
+ return true;
543
+ }
544
+
545
+ // POST /api/skills/:id/files
546
+ if (bulkUpsertSkillFilesRoute.match(req.method, pathSegments)) {
547
+ const parsed = await bulkUpsertSkillFilesRoute.parse(req, res, pathSegments, queryParams);
548
+ if (!parsed) return true;
549
+
550
+ const skill = getSkillById(parsed.params.id);
551
+ if (!skill) {
552
+ jsonError(res, "Skill not found", 404);
553
+ return true;
554
+ }
555
+ if (skill.systemDefault) {
556
+ jsonError(res, SYSTEM_DEFAULT_SKILL_LOCKED_MESSAGE, 403);
557
+ return true;
558
+ }
559
+
560
+ try {
561
+ const files = upsertSkillFiles(parsed.params.id, parsed.body.files);
562
+ const updatedSkill = getSkillById(parsed.params.id);
563
+ json(res, { files, total: files.length, skill: updatedSkill });
564
+ } catch (err) {
565
+ jsonError(res, err instanceof Error ? err.message : "Failed to upsert files", 400);
566
+ }
567
+ return true;
568
+ }
569
+
570
+ // GET /api/skills/:id/files/:path
571
+ if (getSkillFileRoute.match(req.method, pathSegments)) {
572
+ const parsed = await getSkillFileRoute.parse(req, res, pathSegments, queryParams);
573
+ if (!parsed) return true;
574
+
575
+ const skill = getSkillById(parsed.params.id);
576
+ if (!skill) {
577
+ jsonError(res, "Skill not found", 404);
578
+ return true;
579
+ }
580
+
581
+ try {
582
+ const file = getSkillFile(parsed.params.id, decodeSkillFilePath(pathSegments));
583
+ if (!file) {
584
+ jsonError(res, "Skill file not found", 404);
585
+ return true;
586
+ }
587
+ json(res, { file });
588
+ } catch (err) {
589
+ jsonError(res, err instanceof Error ? err.message : "Invalid file path", 400);
590
+ }
591
+ return true;
592
+ }
593
+
594
+ // PUT /api/skills/:id/files/:path
595
+ if (upsertSkillFileRoute.match(req.method, pathSegments)) {
596
+ const parsed = await upsertSkillFileRoute.parse(req, res, pathSegments, queryParams);
597
+ if (!parsed) return true;
598
+
599
+ const skill = getSkillById(parsed.params.id);
600
+ if (!skill) {
601
+ jsonError(res, "Skill not found", 404);
602
+ return true;
603
+ }
604
+ if (skill.systemDefault) {
605
+ jsonError(res, SYSTEM_DEFAULT_SKILL_LOCKED_MESSAGE, 403);
606
+ return true;
607
+ }
608
+
609
+ try {
610
+ const file = upsertSkillFile(parsed.params.id, {
611
+ path: decodeSkillFilePath(pathSegments),
612
+ ...parsed.body,
613
+ });
614
+ const updatedSkill = getSkillById(parsed.params.id);
615
+ json(res, { file, skill: updatedSkill });
616
+ } catch (err) {
617
+ jsonError(res, err instanceof Error ? err.message : "Failed to upsert file", 400);
618
+ }
619
+ return true;
620
+ }
621
+
622
+ // DELETE /api/skills/:id/files/:path
623
+ if (deleteSkillFileRoute.match(req.method, pathSegments)) {
624
+ const parsed = await deleteSkillFileRoute.parse(req, res, pathSegments, queryParams);
625
+ if (!parsed) return true;
626
+
627
+ const skill = getSkillById(parsed.params.id);
628
+ if (!skill) {
629
+ jsonError(res, "Skill not found", 404);
630
+ return true;
631
+ }
632
+ if (skill.systemDefault) {
633
+ jsonError(res, SYSTEM_DEFAULT_SKILL_LOCKED_MESSAGE, 403);
634
+ return true;
635
+ }
636
+
637
+ try {
638
+ const deleted = deleteSkillFile(parsed.params.id, decodeSkillFilePath(pathSegments));
639
+ if (!deleted) {
640
+ jsonError(res, "Skill file not found", 404);
641
+ return true;
642
+ }
643
+ const updatedSkill = getSkillById(parsed.params.id);
644
+ json(res, { success: true, skill: updatedSkill });
645
+ } catch (err) {
646
+ jsonError(res, err instanceof Error ? err.message : "Invalid file path", 400);
647
+ }
648
+ return true;
649
+ }
650
+
426
651
  // GET /api/skills/:id
427
652
  if (getSkillRoute.match(req.method, pathSegments)) {
428
653
  const parsed = await getSkillRoute.parse(req, res, pathSegments, queryParams);
@@ -378,11 +378,9 @@ registerTemplate({
378
378
 
379
379
  You have access to the \`context-mode\` MCP tools (\`batch_execute\`, \`execute\`, \`execute_file\`, \`search\`, \`fetch_and_index\`, \`index\`) which compress tool output to save context window space. For data-heavy operations (web fetches, large file reads, CLI output processing), prefer these over raw Bash/WebFetch to avoid flooding your context window with raw output.
380
380
 
381
- When a tool returns more than a few dozen lines — JSON payloads, log tails, search results, API responses — route it through \`ctx_execute\` or \`ctx_batch_execute\` so only the derived answer enters your conversation. This is especially important for tasks that make many Bash/Read/MCP calls in sequence; each raw response compounds context pressure.
382
-
383
381
  ### Agent Scripts — for bulk, repetitive, or data-heavy work
384
382
 
385
- Use **scripts** (\`script-upsert\` + \`script-run\`) when a task involves repetitive SDK calls, large data processing, or deterministic multi-step pipelines. Scripts run out-of-process and return only their final result — none of the intermediate output floods your context window.
383
+ Use **scripts** (\`script-upsert\` + \`script-run\`) when a task involves repetitive SDK calls, large data processing, or deterministic multi-step pipelines. Scripts run out-of-process and return only their final result.
386
384
 
387
385
  **Decision rubric — when to use scripts vs. other approaches:**
388
386
 
@@ -394,7 +392,6 @@ Use **scripts** (\`script-upsert\` + \`script-run\`) when a task involves repeti
394
392
  | Single expensive web fetch | \`ctx_fetch_and_index\` (context-mode) |
395
393
  | Multi-agent fan-out, parallel work, deterministic pipeline | **Workflow** |
396
394
  | One-off bash/TS with no reuse needed | \`code-mode run\` (Bash) |
397
- | Same logic needed across sessions/agents | **Named script** (\`script-upsert\` + reuse) |
398
395
 
399
396
  The 5 script tools (\`script-search\`, \`script-run\`, \`script-upsert\`, \`script-delete\`, \`script-query-types\`) are deferred tools. Call ToolSearch to load \`script-upsert\`, \`script-run\`, and \`script-query-types\` before using them.
400
397
 
@@ -407,6 +404,27 @@ The 5 script tools (\`script-search\`, \`script-run\`, \`script-upsert\`, \`scri
407
404
  category: "system",
408
405
  });
409
406
 
407
+ registerTemplate({
408
+ eventType: "system.agent.seed_scripts",
409
+ header: "",
410
+ defaultBody: `
411
+ ### Pre-built Seed Scripts
412
+
413
+ The swarm ships named scripts at global scope — each replaces a multi-step tool chain.
414
+ See the \`swarm-scripts\` skill for the full catalog, signatures, and usage patterns.
415
+
416
+ **At every task start (do this FIRST):** Run \`task-context-gathering\` with the task ID and 2-4 queries from the task description. Returns a slimmed task projection + deduped memories in one call — replaces task_get + memory_search loops.
417
+
418
+ **At task start when you only need memory:** Run \`smart-recall\` with 2-4 search angles from the task description.
419
+
420
+ **For any other repetitive multi-tool pattern:** Call \`script-search\` with a natural-language description. If a script exists, use it.
421
+
422
+ Script tools are deferred — load via \`ToolSearch("select:script-search,script-run")\` before first use.
423
+ `,
424
+ variables: [],
425
+ category: "system",
426
+ });
427
+
410
428
  // system.agent.guidelines removed — its content (communication etiquette, todos)
411
429
  // is covered by worker/lead templates and filesystem template respectively.
412
430
 
@@ -602,6 +620,7 @@ registerTemplate({
602
620
  {{@template[system.agent.filesystem]}}
603
621
  {{@template[system.agent.self_awareness]}}
604
622
  {{@template[system.agent.context_mode]}}
623
+ {{@template[system.agent.seed_scripts]}}
605
624
 
606
625
  {{@template[system.agent.system]}}
607
626
  {{@template[system.agent.share_urls]}}
@@ -623,6 +642,7 @@ registerTemplate({
623
642
  {{@template[system.agent.filesystem]}}
624
643
  {{@template[system.agent.self_awareness]}}
625
644
  {{@template[system.agent.context_mode]}}
645
+ {{@template[system.agent.seed_scripts]}}
626
646
 
627
647
  {{@template[system.agent.system]}}
628
648
  {{@template[system.agent.share_urls]}}
@@ -72,9 +72,8 @@ async function cleanupTaskFile(pid: number): Promise<void> {
72
72
  /**
73
73
  * Parse `CLAUDE_BINARY` into argv prefix tokens.
74
74
  *
75
- * Accepts a single binary name (`"claude"`, `"shannon"`), an absolute path,
76
- * or a whitespace-separated command string (`"bunx @dexh/shannon"`,
77
- * `"npx -y @dexh/shannon"`). Trim + split on `/\s+/`. No shell parsing, no
75
+ * Accepts a single binary name (`"claude"`), an absolute path, or a
76
+ * whitespace-separated command string. Trim + split on `/\s+/`. No shell parsing, no
78
77
  * quote handling — keep it tiny and predictable. Empty / missing → `["claude"]`.
79
78
  *
80
79
  * Exported for unit testing.
@@ -108,14 +107,81 @@ export function resolveClaudeBinary(
108
107
  return candidate || "claude";
109
108
  }
110
109
 
110
+ const CLAUDE_BRIDGE_BINARY = "claude-bridge";
111
+ const CLAUDE_BRIDGE_LOCAL_AUTH_ARG = "--desplega-local-auth";
112
+ const LEGACY_CLAUDE_BRIDGE_COMPAT_BINARY = "shan" + "non";
113
+ const CLAUDE_BRIDGE_LOCAL_AUTH_ENV_VARS = [
114
+ "ANTHROPIC_API_KEY",
115
+ "ANTHROPIC_AUTH_TOKEN",
116
+ "ANTHROPIC_BASE_URL",
117
+ "ANTHROPIC_CUSTOM_HEADERS",
118
+ "ANTHROPIC_MODEL",
119
+ ] as const;
120
+
121
+ /**
122
+ * Parse a boolean env toggle. Only true/1 enable and false/0 disable; unset
123
+ * and invalid values are treated as disabled.
124
+ *
125
+ * Exported for unit testing.
126
+ */
127
+ export function parseClaudeBridgeEnabled(raw: string | undefined): boolean {
128
+ const normalized = raw?.trim().toLowerCase();
129
+ return normalized === "true" || normalized === "1";
130
+ }
131
+
132
+ /**
133
+ * Resolve the reloadable claude-bridge toggle from the same resolved-env
134
+ * overlay used for `CLAUDE_BINARY`.
135
+ *
136
+ * Exported for unit testing.
137
+ */
138
+ export function resolveClaudeBridgeEnabled(
139
+ resolvedEnv: Record<string, string | undefined>,
140
+ fallbackEnv: Record<string, string | undefined> = process.env,
141
+ ): boolean {
142
+ const candidate =
143
+ resolvedEnv.SWARM_USE_CLAUDE_BRIDGE?.trim() || fallbackEnv.SWARM_USE_CLAUDE_BRIDGE?.trim();
144
+ return parseClaudeBridgeEnabled(candidate);
145
+ }
146
+
147
+ function resolveClaudeBinaryArgv(
148
+ resolvedEnv: Record<string, string | undefined>,
149
+ fallbackEnv: Record<string, string | undefined> = process.env,
150
+ ): { raw: string; argv: string[]; useClaudeBridge: boolean } {
151
+ const useClaudeBridge = resolveClaudeBridgeEnabled(resolvedEnv, fallbackEnv);
152
+ const raw = useClaudeBridge
153
+ ? CLAUDE_BRIDGE_BINARY
154
+ : resolveClaudeBinary(resolvedEnv, fallbackEnv);
155
+ return { raw, argv: parseClaudeBinary(raw), useClaudeBridge };
156
+ }
157
+
158
+ function isLegacyClaudeBridgeCompatBinary(raw: string): boolean {
159
+ return raw.toLowerCase().includes(LEGACY_CLAUDE_BRIDGE_COMPAT_BINARY);
160
+ }
161
+
162
+ function withClaudeBridgeAuthArgs(
163
+ argv: readonly string[],
164
+ sourceEnv: Record<string, string | undefined>,
165
+ ): string[] {
166
+ if (sourceEnv.CLAUDE_CODE_OAUTH_TOKEN) {
167
+ return [...argv];
168
+ }
169
+
170
+ if (CLAUDE_BRIDGE_LOCAL_AUTH_ENV_VARS.some((name) => sourceEnv[name])) {
171
+ return [...argv, CLAUDE_BRIDGE_LOCAL_AUTH_ARG];
172
+ }
173
+
174
+ return [...argv];
175
+ }
176
+
111
177
  /**
112
178
  * Pre-seed `~/.claude.json` so the per-project trust-dialog ("Quick safety
113
179
  * check: Is this a project you trust?") doesn't block on first run.
114
180
  *
115
181
  * Mirrors the onboarding-skip hack in `Dockerfile.worker` (which writes
116
182
  * `hasCompletedOnboarding` and `bypassPermissionsModeAccepted`). When the
117
- * resolved binary contains "shannon", claude runs inside tmux and shannon
118
- * does NOT auto-accept the dialog, so the pane hangs forever. Writing
183
+ * resolved binary runs interactive claude inside tmux, claude does NOT
184
+ * reliably auto-accept the dialog, so the pane can hang forever. Writing
119
185
  * `projects[cwd].hasTrustDialogAccepted = true` (and `hasCompletedProjectOnboarding`)
120
186
  * tells claude-code the cwd is pre-trusted.
121
187
  *
@@ -793,46 +859,59 @@ export class ClaudeAdapter implements ProviderAdapter {
793
859
 
794
860
  const model = config.model || "opus";
795
861
 
796
- const credType = validateClaudeCredentials(config.env || process.env);
862
+ const sourceEnv = config.env || process.env;
863
+ const credType = validateClaudeCredentials(sourceEnv);
797
864
  console.log(`\x1b[2m[claude]\x1b[0m Using credential: ${credType}`);
798
865
 
799
866
  // Resolve the argv prefix. Same flags (`-p`, `--model`, ...) work across
800
- // alternates; only argv[0..n] changes. `CLAUDE_BINARY` accepts a single
801
- // binary (`"shannon"`, `"/usr/local/bin/shannon"`) or a whitespace-separated
802
- // command string (`"bunx @dexh/shannon"`, `"npx -y @dexh/shannon"`).
803
- // Setting it to anything containing `shannon` opts into the dexhorthy/shannon
804
- // variant, which drives `claude` interactively in tmux to stay on the
805
- // subscription credit pool after the 2026-06-15 programmatic-credit split.
867
+ // alternates; only argv[0..n] changes. Prefer SWARM_USE_CLAUDE_BRIDGE=true
868
+ // for the Desplega-owned bridge. CLAUDE_BINARY remains as the low-level
869
+ // override for custom binaries and the legacy third-party bridge path.
806
870
  //
807
871
  // `config.env` carries the swarm_config overlay (resolved repo > agent > global
808
872
  // by `fetchResolvedEnv` in src/commands/runner.ts), so operators can flip
809
873
  // a worker's binary via `set-config CLAUDE_BINARY=...` without a restart.
810
874
  // Falls back to process.env, then "claude". See `resolveClaudeBinary` above.
811
875
  //
812
- // See `docs-site/.../shannon-experimental.mdx` for the user-facing guide
876
+ // See `docs-site/.../claude-bridge-experimental.mdx` for the user-facing guide
813
877
  // and `runbooks/harness-providers.md` for engineering notes.
814
- const claudeBinaryRaw = resolveClaudeBinary(config.env || process.env);
815
- const claudeBinaryArgv = parseClaudeBinary(claudeBinaryRaw);
816
- const isShannon = claudeBinaryRaw.toLowerCase().includes("shannon");
878
+ const {
879
+ raw: claudeBinaryRaw,
880
+ argv: claudeBinaryArgv,
881
+ useClaudeBridge,
882
+ } = resolveClaudeBinaryArgv(sourceEnv);
883
+ const isLegacyBridgeCompat = isLegacyClaudeBridgeCompatBinary(claudeBinaryRaw);
884
+ const effectiveClaudeBinaryArgv = useClaudeBridge
885
+ ? withClaudeBridgeAuthArgs(claudeBinaryArgv, sourceEnv)
886
+ : claudeBinaryArgv;
887
+ const isInteractiveTmuxClaude = isLegacyBridgeCompat || useClaudeBridge;
888
+ const configuredClaudeBinaryRaw = resolveClaudeBinary(sourceEnv);
889
+ if (isLegacyClaudeBridgeCompatBinary(configuredClaudeBinaryRaw)) {
890
+ console.warn(
891
+ `\x1b[33m[claude]\x1b[0m CLAUDE_BINARY=${LEGACY_CLAUDE_BRIDGE_COMPAT_BINARY} is deprecated; set SWARM_USE_CLAUDE_BRIDGE=true to use @desplega.ai/claude-bridge.`,
892
+ );
893
+ }
817
894
 
818
895
  console.log(
819
- `\x1b[2m[${config.role}]\x1b[0m Resolved CLAUDE_BINARY: ${claudeBinaryArgv.join(" ")} (isShannon: ${isShannon})`,
896
+ `\x1b[2m[${config.role}]\x1b[0m Resolved claude binary: ${effectiveClaudeBinaryArgv.join(" ")} (useClaudeBridge: ${useClaudeBridge}, legacyBridgeCompat: ${isLegacyBridgeCompat})`,
820
897
  );
821
898
 
822
- // Fail fast: shannon shells out to tmux. If it's missing, surface a
823
- // clear error here rather than letting the spawn fail opaquely.
824
- if (isShannon && !Bun.which("tmux")) {
899
+ // Fail fast: claude-bridge and its legacy compatibility path both shell
900
+ // out to tmux. If it's
901
+ // missing, surface a clear error here rather than letting startup fail
902
+ // opaquely.
903
+ if (isInteractiveTmuxClaude && !Bun.which("tmux")) {
904
+ const label = useClaudeBridge
905
+ ? "SWARM_USE_CLAUDE_BRIDGE=true"
906
+ : `CLAUDE_BINARY=${LEGACY_CLAUDE_BRIDGE_COMPAT_BINARY}`;
825
907
  throw new Error(
826
- "CLAUDE_BINARY=shannon requires 'tmux' on PATH (install via apt/brew). See runbooks/harness-providers.md.",
908
+ `${label} requires 'tmux' on PATH (install via apt/brew). See runbooks/harness-providers.md.`,
827
909
  );
828
910
  }
829
911
 
830
- // Shannon drives `claude` in tmux claude's per-project trust dialog
831
- // (first-run "Is this a project you trust?") hangs the pane because shannon
832
- // doesn't auto-accept it. Pre-seed `~/.claude.json` so the dialog never
833
- // prompts. Idempotent; no-op when already trusted. Engineering rationale:
834
- // `runbooks/harness-providers.md` § "Trust-dialog pre-seed".
835
- if (isShannon) {
912
+ // Claude Bridge and its legacy compatibility path drive interactive
913
+ // `claude` in tmux, where the first-run trust dialog can block startup.
914
+ if (isInteractiveTmuxClaude) {
836
915
  try {
837
916
  await preseedClaudeTrustDialog(config.cwd);
838
917
  } catch (err) {
@@ -897,7 +976,7 @@ export class ClaudeAdapter implements ProviderAdapter {
897
976
  taskFilePath,
898
977
  taskFilePid,
899
978
  sessionMcpConfig,
900
- claudeBinaryArgv,
979
+ effectiveClaudeBinaryArgv,
901
980
  systemPromptFile,
902
981
  );
903
982
  }
@@ -0,0 +1,110 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, rm } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import type { ScriptRun } from "../types";
5
+
6
+ export type ScriptExecutionResult = {
7
+ exitCode: number | null;
8
+ stderr: string;
9
+ };
10
+
11
+ export type ScriptExecutionHandle = {
12
+ pid: number | null;
13
+ tmpdir: string;
14
+ startedAtMs: number;
15
+ exited: Promise<ScriptExecutionResult>;
16
+ terminate(signal?: NodeJS.Signals): void;
17
+ cleanup(): Promise<void>;
18
+ };
19
+
20
+ export type StartScriptExecutionInput = {
21
+ run: ScriptRun;
22
+ baseUrl: string;
23
+ apiKey: string;
24
+ };
25
+
26
+ export interface ScriptExecutor {
27
+ start(input: StartScriptExecutionInput): Promise<ScriptExecutionHandle>;
28
+ isRunning(pid: number): boolean;
29
+ terminatePid(pid: number, signal?: NodeJS.Signals): void;
30
+ }
31
+
32
+ export function getScriptWorkflowHarnessPath(): string {
33
+ const runtimeDir = process.env.SCRIPT_WORKFLOW_RUNTIME_DIR;
34
+ if (!runtimeDir) return new URL("./harness.ts", import.meta.url).pathname;
35
+
36
+ const bundledHarness = `${resolve(runtimeDir)}/harness.bundle.js`;
37
+ if (!existsSync(bundledHarness)) {
38
+ throw new Error(
39
+ `Script workflow harness bundle not found at ${bundledHarness}. ` +
40
+ "Build/copy harness.bundle.js and set SCRIPT_WORKFLOW_RUNTIME_DIR to its directory.",
41
+ );
42
+ }
43
+ return bundledHarness;
44
+ }
45
+
46
+ export class LocalProcessScriptExecutor implements ScriptExecutor {
47
+ async start(input: StartScriptExecutionInput): Promise<ScriptExecutionHandle> {
48
+ const { run, baseUrl, apiKey } = input;
49
+ const tmpdir = `${process.env.TMPDIR ?? "/tmp"}/script-workflow-${run.id}`;
50
+ await mkdir(tmpdir, { recursive: true });
51
+ const sourceFile = `${tmpdir}/source.ts`;
52
+ const argsFile = `${tmpdir}/args.json`;
53
+ await Bun.write(sourceFile, run.source);
54
+ await Bun.write(argsFile, JSON.stringify(run.args ?? null));
55
+
56
+ const proc = Bun.spawn(["bun", "run", getScriptWorkflowHarnessPath()], {
57
+ cwd: tmpdir,
58
+ stdin: "ignore",
59
+ stdout: "ignore",
60
+ stderr: "pipe",
61
+ env: {
62
+ PATH: process.env.PATH ?? "/usr/bin:/bin",
63
+ HOME: process.env.HOME ?? "/tmp",
64
+ LANG: process.env.LANG ?? "C.UTF-8",
65
+ LC_ALL: process.env.LC_ALL ?? "C.UTF-8",
66
+ TMPDIR: tmpdir,
67
+ AGENT_SWARM_API_KEY: apiKey,
68
+ MCP_BASE_URL: baseUrl,
69
+ SCRIPT_RUN_ID: run.id,
70
+ SCRIPT_RUN_AGENT_ID: run.agentId,
71
+ SCRIPT_RUN_TMPDIR: tmpdir,
72
+ SCRIPT_RUN_SOURCE_FILE: sourceFile,
73
+ SCRIPT_RUN_ARGS_FILE: argsFile,
74
+ },
75
+ });
76
+
77
+ const stderrPromise = new Response(proc.stderr).text().catch(() => "");
78
+
79
+ return {
80
+ pid: proc.pid,
81
+ tmpdir,
82
+ startedAtMs: Date.now(),
83
+ exited: proc.exited.then(async (exitCode) => ({
84
+ exitCode,
85
+ stderr: await stderrPromise,
86
+ })),
87
+ terminate: (signal = "SIGTERM") => {
88
+ proc.kill(signal);
89
+ },
90
+ cleanup: async () => {
91
+ await rm(tmpdir, { recursive: true, force: true });
92
+ },
93
+ };
94
+ }
95
+
96
+ isRunning(pid: number): boolean {
97
+ try {
98
+ process.kill(pid, 0);
99
+ return true;
100
+ } catch {
101
+ return false;
102
+ }
103
+ }
104
+
105
+ terminatePid(pid: number, signal: NodeJS.Signals = "SIGTERM"): void {
106
+ process.kill(pid, signal);
107
+ }
108
+ }
109
+
110
+ export const localProcessScriptExecutor = new LocalProcessScriptExecutor();