@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.
- package/README.md +3 -2
- package/openapi.json +1005 -152
- package/package.json +6 -6
- package/plugin/skills/pages/SKILL.md +5 -2
- package/src/be/db.ts +662 -19
- package/src/be/memory/constants.ts +2 -1
- package/src/be/memory/providers/openai-embedding.ts +2 -5
- package/src/be/memory/providers/sqlite-store.ts +293 -76
- package/src/be/memory/types.ts +35 -0
- package/src/be/migrations/083_script_workflows.sql +51 -0
- package/src/be/migrations/084_script_run_journal_duration.sql +5 -0
- package/src/be/migrations/085_script_runs_kind.sql +9 -0
- package/src/be/migrations/086_pages_default_authed.sql +64 -0
- package/src/be/migrations/087_skill_files.sql +19 -0
- package/src/be/modelsdev-cache.json +42310 -38617
- package/src/be/scripts/typecheck.ts +49 -0
- package/src/be/seed-scripts/catalog/boot-triage.ts +221 -0
- package/src/be/seed-scripts/catalog/catalog-report.ts +457 -0
- package/src/be/seed-scripts/catalog/compound-insights.ts +310 -6
- package/src/be/seed-scripts/catalog/gh-pr-snapshot.ts +1 -1
- package/src/be/seed-scripts/catalog/memory-eval.ts +1059 -0
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +506 -0
- package/src/be/seed-scripts/catalog/schedule-health.ts +78 -2
- package/src/be/seed-scripts/catalog/task-context-gathering.ts +92 -0
- package/src/be/seed-scripts/catalog/task-failure-audit.ts +48 -1
- package/src/be/seed-scripts/catalog/tool-usage.ts +6 -3
- package/src/be/seed-scripts/index.ts +51 -5
- package/src/be/seed-skills/index.ts +3 -3
- package/src/be/skill-sync.ts +91 -7
- package/src/be/swarm-config-guard.ts +17 -0
- package/src/commands/runner.ts +49 -4
- package/src/heartbeat/templates.ts +20 -16
- package/src/http/db-query.ts +20 -5
- package/src/http/index.ts +51 -7
- package/src/http/mcp-user.ts +23 -0
- package/src/http/mcp.ts +58 -0
- package/src/http/memory.ts +58 -0
- package/src/http/pages.ts +1 -1
- package/src/http/script-runs.ts +557 -0
- package/src/http/scripts.ts +39 -2
- package/src/http/skills.ts +225 -0
- package/src/prompts/session-templates.ts +24 -4
- package/src/providers/claude-adapter.ts +107 -28
- package/src/script-workflows/executor.ts +110 -0
- package/src/script-workflows/harness.ts +73 -0
- package/src/script-workflows/label-lint.ts +51 -0
- package/src/script-workflows/limits.ts +22 -0
- package/src/script-workflows/supervisor.ts +139 -0
- package/src/script-workflows/workflow-ctx.ts +209 -0
- package/src/scripts-runtime/sdk-allowlist.ts +4 -0
- package/src/scripts-runtime/swarm-sdk.ts +13 -0
- package/src/scripts-runtime/types/stdlib.d.ts +61 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +61 -0
- package/src/server.ts +4 -0
- package/src/slack/handlers.ts +11 -4
- package/src/slack/message-text.ts +98 -0
- package/src/slack/thread-buffer.ts +5 -3
- package/src/tests/claude-adapter-binary.test.ts +271 -74
- package/src/tests/create-page-tool.test.ts +19 -2
- package/src/tests/db-query.test.ts +28 -0
- package/src/tests/error-tracker.test.ts +121 -0
- package/src/tests/harness-provider-resolution.test.ts +33 -0
- package/src/tests/heartbeat-checklist.test.ts +36 -0
- package/src/tests/mcp-tools.test.ts +6 -0
- package/src/tests/mcp-transport-gc.test.ts +58 -0
- package/src/tests/memory-health-endpoint.test.ts +78 -0
- package/src/tests/memory-store.test.ts +221 -1
- package/src/tests/pages-http.test.ts +20 -2
- package/src/tests/pages-storage.test.ts +26 -0
- package/src/tests/prompt-template-session.test.ts +34 -5
- package/src/tests/script-runs-http.test.ts +278 -0
- package/src/tests/script-workflows-label-lint.test.ts +43 -0
- package/src/tests/script-workflows-runtime-e2e.test.ts +170 -0
- package/src/tests/scripts-mcp-e2e.test.ts +102 -2
- package/src/tests/seed-scripts.test.ts +468 -3
- package/src/tests/skill-files-http.test.ts +171 -0
- package/src/tests/skill-files.test.ts +162 -0
- package/src/tests/skill-get-file-tool.test.ts +110 -0
- package/src/tests/skill-sync.test.ts +125 -6
- package/src/tests/slack-message-text.test.ts +250 -0
- package/src/tests/system-default-skills.test.ts +40 -0
- package/src/tools/create-page.ts +2 -2
- package/src/tools/db-query.ts +16 -6
- package/src/tools/script-runs.ts +123 -0
- package/src/tools/skills/index.ts +1 -0
- package/src/tools/skills/skill-get-file.ts +80 -0
- package/src/tools/slack-read.ts +12 -3
- package/src/tools/tool-config.ts +6 -2
- package/src/types.ts +72 -0
- package/src/utils/error-tracker.ts +40 -1
- package/src/utils/internal-ai/complete-structured.ts +10 -4
- package/src/workflows/executors/raw-llm.ts +76 -59
- package/templates/schedules/daily-blocker-digest/content.md +68 -54
- package/templates/schedules/daily-compounding-reflection/content.md +4 -4
- package/templates/schedules/daily-hn-briefing/content.md +5 -5
- package/templates/schedules/daily-workflow-health-audit/content.md +6 -6
- package/templates/schedules/gtm-weekly-review/content.md +9 -9
- package/templates/schedules/weekly-dependabot-triage/content.md +24 -20
- package/templates/skills/agentmail-sending/content.md +6 -7
- package/templates/skills/desloppify/content.md +8 -9
- package/templates/skills/jira-interaction/content.md +25 -33
- package/templates/skills/kapso-whatsapp/content.md +29 -30
- package/templates/skills/linear-interaction/content.md +8 -9
- package/templates/skills/pages/content.md +205 -55
- package/templates/skills/profile-corruption-escalation/content.md +44 -85
- package/templates/skills/script-workflows/config.json +14 -0
- package/templates/skills/script-workflows/content.md +68 -0
- package/templates/skills/sprite-cli/content.md +4 -5
- package/templates/skills/swarm-scripts/content.md +2 -3
- package/templates/skills/turso-interaction/content.md +14 -17
- package/templates/skills/workflow-iterate/content.md +38 -391
- package/templates/skills/x-api-interactions/content.md +4 -6
- package/templates/skills/scheduled-task-resilience/config.json +0 -14
- package/templates/skills/scheduled-task-resilience/content.md +0 -95
package/src/http/skills.ts
CHANGED
|
@@ -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
|
|
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"
|
|
76
|
-
*
|
|
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
|
|
118
|
-
*
|
|
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
|
|
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.
|
|
801
|
-
//
|
|
802
|
-
//
|
|
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/.../
|
|
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
|
|
815
|
-
|
|
816
|
-
|
|
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
|
|
896
|
+
`\x1b[2m[${config.role}]\x1b[0m Resolved claude binary: ${effectiveClaudeBinaryArgv.join(" ")} (useClaudeBridge: ${useClaudeBridge}, legacyBridgeCompat: ${isLegacyBridgeCompat})`,
|
|
820
897
|
);
|
|
821
898
|
|
|
822
|
-
// Fail fast:
|
|
823
|
-
//
|
|
824
|
-
|
|
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
|
-
|
|
908
|
+
`${label} requires 'tmux' on PATH (install via apt/brew). See runbooks/harness-providers.md.`,
|
|
827
909
|
);
|
|
828
910
|
}
|
|
829
911
|
|
|
830
|
-
//
|
|
831
|
-
//
|
|
832
|
-
|
|
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
|
-
|
|
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();
|