@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
@@ -0,0 +1,92 @@
1
+ import { z } from "zod";
2
+
3
+ export const argsSchema = z.object({
4
+ taskId: z.string().describe("Task ID to fetch details for"),
5
+ queries: z
6
+ .array(z.string())
7
+ .min(1)
8
+ .describe("Search queries from the task description — 2-4 recommended"),
9
+ scope: z
10
+ .enum(["all", "agent", "swarm"])
11
+ .optional()
12
+ .describe("Memory scope filter (default all)"),
13
+ memoryLimit: z
14
+ .number()
15
+ .int()
16
+ .positive()
17
+ .optional()
18
+ .describe("Max memories per query before dedup (default 8)"),
19
+ });
20
+
21
+ function slimTask(task: any) {
22
+ if (!task || typeof task !== "object") return null;
23
+ return {
24
+ id: task.id,
25
+ status: task.status,
26
+ description: task.task,
27
+ dependsOn: task.dependsOn,
28
+ slackChannelId: task.slackChannelId,
29
+ slackThreadTs: task.slackThreadTs,
30
+ createdAt: task.createdAt,
31
+ finishedAt: task.finishedAt,
32
+ agentId: task.agentId,
33
+ output: task.output,
34
+ failureReason: task.failureReason,
35
+ };
36
+ }
37
+
38
+ /** Fetch slim task details plus deduped multi-query memories for task onboarding. */
39
+ export default async function taskContextGathering(args: any, ctx: any) {
40
+ const parsed = argsSchema.safeParse(args);
41
+ if (!parsed.success) return { error: "invalid args: " + parsed.error.message };
42
+ const { taskId, queries, scope = "all", memoryLimit = 8 } = parsed.data;
43
+
44
+ const taskRes: any = await ctx.swarm.task_get({ taskId });
45
+ const taskPayload = taskRes?.data ?? taskRes;
46
+ if (taskPayload?.success === false) {
47
+ return { error: taskPayload.message ?? "task_get failed", taskId };
48
+ }
49
+
50
+ const allResults: any[] = [];
51
+ for (const query of queries) {
52
+ const res: any = await ctx.swarm.memory_search({ query, scope, limit: memoryLimit });
53
+ const payload = res?.data ?? res;
54
+ const results = payload?.results ?? [];
55
+ for (const memory of results) {
56
+ allResults.push({ ...memory, querySource: query });
57
+ }
58
+ }
59
+
60
+ const byId = new Map<string, any>();
61
+ const hitCounts = new Map<string, number>();
62
+ for (const memory of allResults) {
63
+ const id = typeof memory.id === "string" ? memory.id : JSON.stringify(memory);
64
+ hitCounts.set(id, (hitCounts.get(id) ?? 0) + 1);
65
+ const existing = byId.get(id);
66
+ const similarity = typeof memory.similarity === "number" ? memory.similarity : 0;
67
+ const existingSimilarity =
68
+ existing && typeof existing.similarity === "number" ? existing.similarity : -Infinity;
69
+ if (!existing || similarity > existingSimilarity) byId.set(id, memory);
70
+ }
71
+
72
+ const memories = Array.from(byId.entries()).map(([id, memory]) => {
73
+ const hits = hitCounts.get(id) ?? 1;
74
+ const similarity = typeof memory.similarity === "number" ? memory.similarity : 0;
75
+ return {
76
+ ...memory,
77
+ hits,
78
+ compositeScore: similarity + 0.05 * hits,
79
+ };
80
+ });
81
+ memories.sort((a: any, b: any) => b.compositeScore - a.compositeScore);
82
+
83
+ return {
84
+ task: slimTask(taskPayload?.task),
85
+ requestedBy: taskPayload?.requestedBy,
86
+ attachments: taskPayload?.attachments ?? [],
87
+ queriesRun: queries.length,
88
+ totalCandidates: allResults.length,
89
+ uniqueMemories: memories.length,
90
+ memories: memories.slice(0, memoryLimit * 2),
91
+ };
92
+ }
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { publishCatalogReportPage } from "./catalog-report";
2
3
 
3
4
  export const argsSchema = z.object({
4
5
  days: z
@@ -17,6 +18,7 @@ export const argsSchema = z.object({
17
18
  .positive()
18
19
  .optional()
19
20
  .describe("Max failed tasks to scan (default 500)"),
21
+ publishPage: z.boolean().optional().describe("Publish an authed HTML page (default true)"),
20
22
  });
21
23
 
22
24
  const REASON_PATTERNS: any[] = [
@@ -46,6 +48,7 @@ export default async function taskFailureAudit(args: any, ctx: any) {
46
48
  const days = parsed.data.days || 7;
47
49
  const groupBy = parsed.data.groupBy || "reason";
48
50
  const limit = parsed.data.limit || 500;
51
+ const publishPage = parsed.data.publishPage !== false;
49
52
 
50
53
  const since = new Date(Date.now() - days * 86400000).toISOString();
51
54
  const res: any = await ctx.swarm.task_list({
@@ -77,11 +80,55 @@ export default async function taskFailureAudit(args: any, ctx: any) {
77
80
  .map((k: string) => groups[k])
78
81
  .sort((a: any, b: any) => b.count - a.count);
79
82
 
80
- return {
83
+ const result: any = {
81
84
  days,
82
85
  groupBy,
83
86
  totalFailed: tasks.length,
84
87
  clusterCount: rows.length,
85
88
  groups: rows,
86
89
  };
90
+
91
+ if (publishPage) {
92
+ result.page = await publishCatalogReportPage(
93
+ {
94
+ title: "Task Failure Audit",
95
+ slug: "task-failure-audit",
96
+ description: "Clustered audit of recently failed swarm tasks.",
97
+ generatedAt: new Date().toISOString(),
98
+ lede: `Clustered ${tasks.length} failed task(s) over ${days} day(s) by ${groupBy}.`,
99
+ metrics: [
100
+ ["Failed tasks", tasks.length],
101
+ ["Clusters", rows.length],
102
+ ["Days", days],
103
+ ["Limit", limit],
104
+ ],
105
+ sections: [
106
+ {
107
+ key: "failure-clusters",
108
+ goal: "Surface repeated failure modes before they become operational drift.",
109
+ findingCount: rows.length,
110
+ checks: { totalFailed: tasks.length, clusterCount: rows.length, groupBy },
111
+ findings: rows.map((group: any) => ({
112
+ id: `failure.${group.key}`,
113
+ severity: group.count >= 5 ? "high" : group.count >= 2 ? "medium" : "low",
114
+ summary: `${group.count} failed task(s) in ${group.key}.`,
115
+ action: "Inspect the sample task IDs and decide whether this needs a fix, retry, or HEARTBEAT watch item.",
116
+ samples: [
117
+ {
118
+ key: group.key,
119
+ count: group.count,
120
+ taskIds: group.taskIds,
121
+ sampleReason: group.sampleReason,
122
+ },
123
+ ],
124
+ })),
125
+ },
126
+ ],
127
+ appendix: result,
128
+ },
129
+ ctx,
130
+ );
131
+ }
132
+
133
+ return result;
87
134
  }
@@ -25,7 +25,7 @@ export default async function toolUsage(args: any, ctx: any) {
25
25
  const agentId = parsed.data.agentId;
26
26
 
27
27
  const agentFilter = agentId ? `AND agent_id = '${agentId}'` : "";
28
- const query = `
28
+ const sql = `
29
29
  SELECT tool_name, count(*) as calls
30
30
  FROM session_logs
31
31
  WHERE tool_name IS NOT NULL
@@ -36,9 +36,12 @@ export default async function toolUsage(args: any, ctx: any) {
36
36
  LIMIT ${limit}
37
37
  `;
38
38
 
39
- const res: any = await ctx.swarm.db_query({ query });
39
+ const res: any = await ctx.swarm.db_query({ sql });
40
40
  const payload = res?.data ?? res;
41
- const rows: any[] = payload?.rows ?? [];
41
+ const columns: string[] = payload?.columns ?? [];
42
+ const rows: any[] = (payload?.rows ?? []).map((row: any[]) =>
43
+ Object.fromEntries(columns.map((column, index) => [column, row[index]])),
44
+ );
42
45
 
43
46
  const totalCalls = rows.reduce((sum: number, r: any) => sum + (r.calls ?? 0), 0);
44
47
 
@@ -22,6 +22,9 @@ import { getScript, upsertScriptByName } from "../scripts/db";
22
22
  import { extractArgsJsonSchema } from "../scripts/extract-schema";
23
23
  import { typecheckScript } from "../scripts/typecheck";
24
24
  import type { Seeder, SeedItem } from "../seed/types";
25
+ import bootTriageSrc from "./catalog/boot-triage.ts" with { type: "text" };
26
+ // @ts-expect-error Bun text imports return the raw source string for bundling standalone scripts.
27
+ import catalogReportSrc from "./catalog/catalog-report.ts" with { type: "text" };
25
28
  import compoundInsightsSrc from "./catalog/compound-insights.ts" with { type: "text" };
26
29
  import dateResolveSrc from "./catalog/date-resolve.ts" with { type: "text" };
27
30
  import fetchReadableSrc from "./catalog/fetch-readable.ts" with { type: "text" };
@@ -30,9 +33,12 @@ import groupCountSrc from "./catalog/group-count.ts" with { type: "text" };
30
33
  import jsonQuerySrc from "./catalog/json-query.ts" with { type: "text" };
31
34
  import linearIssueSrc from "./catalog/linear-issue.ts" with { type: "text" };
32
35
  import memoryDedupCheckSrc from "./catalog/memory-dedup-check.ts" with { type: "text" };
36
+ import memoryEvalSrc from "./catalog/memory-eval.ts" with { type: "text" };
37
+ import opsCatalogAuditSrc from "./catalog/ops-catalog-audit.ts" with { type: "text" };
33
38
  import scheduleHealthSrc from "./catalog/schedule-health.ts" with { type: "text" };
34
39
  import slackThreadFlattenSrc from "./catalog/slack-thread-flatten.ts" with { type: "text" };
35
40
  import smartRecallSrc from "./catalog/smart-recall.ts" with { type: "text" };
41
+ import taskContextGatheringSrc from "./catalog/task-context-gathering.ts" with { type: "text" };
36
42
  import taskFailureAuditSrc from "./catalog/task-failure-audit.ts" with { type: "text" };
37
43
  import textDiffSrc from "./catalog/text-diff.ts" with { type: "text" };
38
44
  import toolUsageSrc from "./catalog/tool-usage.ts" with { type: "text" };
@@ -48,6 +54,14 @@ export type SeedScript = {
48
54
  // module's default export, so the cast restores the real shape.
49
55
  const asText = (s: unknown): string => s as string;
50
56
 
57
+ const CATALOG_REPORT_IMPORT_RE = /^import\s+\{[^}]*\}\s+from "\.\/catalog-report";\n\n?/m;
58
+
59
+ function bundleCatalogReport(source: string): string {
60
+ const helper = asText(catalogReportSrc);
61
+ if (!CATALOG_REPORT_IMPORT_RE.test(source)) return source;
62
+ return `${helper}\n\n${source.replace(CATALOG_REPORT_IMPORT_RE, "")}`;
63
+ }
64
+
51
65
  export const SEED_SCRIPTS: SeedScript[] = [
52
66
  {
53
67
  name: "gh-pr-snapshot",
@@ -101,7 +115,7 @@ export const SEED_SCRIPTS: SeedScript[] = [
101
115
  "Scan recently failed swarm tasks and cluster them by failure reason, agent or schedule to surface recurring problems.",
102
116
  intent:
103
117
  "Find patterns in swarm task failures — which agent, schedule or error keeps breaking — for a reliability review.",
104
- source: asText(taskFailureAuditSrc),
118
+ source: bundleCatalogReport(asText(taskFailureAuditSrc)),
105
119
  },
106
120
  {
107
121
  name: "memory-dedup-check",
@@ -134,13 +148,21 @@ export const SEED_SCRIPTS: SeedScript[] = [
134
148
  "Recall relevant memories using multiple search angles — better coverage than a single query. Use for task onboarding, context gathering, or before writing new memories.",
135
149
  source: asText(smartRecallSrc),
136
150
  },
151
+ {
152
+ name: "task-context-gathering",
153
+ description:
154
+ "Get task details and recall relevant memories in one call — returns a slimmed task projection plus deduped and reranked memories from multi-query fan-out.",
155
+ intent:
156
+ "Task onboarding: one call instead of task_get plus multiple memory_search calls. Pass the task description split into 2-4 natural-language queries.",
157
+ source: asText(taskContextGatheringSrc),
158
+ },
137
159
  {
138
160
  name: "schedule-health",
139
161
  description:
140
162
  "Per-schedule failure rate check over recent tasks — flags schedules with failure rates above a configurable threshold.",
141
163
  intent:
142
164
  "Find unhealthy schedules that keep failing — for daily compounding, reliability reviews, or ops triage.",
143
- source: asText(scheduleHealthSrc),
165
+ source: bundleCatalogReport(asText(scheduleHealthSrc)),
144
166
  },
145
167
  {
146
168
  name: "tool-usage",
@@ -153,10 +175,34 @@ export const SEED_SCRIPTS: SeedScript[] = [
153
175
  {
154
176
  name: "compound-insights",
155
177
  description:
156
- "All-in-one swarm-wide daily ops snapshot: task completion/failure summary, real failure clusters (excludes superseded/cancelled bookkeeping), schedule health flags, tool usage top-25, memory health stats, and a per-agent breakdown. Aggregates across ALL agents via direct read-only SQL.",
178
+ "All-in-one swarm-wide daily ops snapshot: task completion/failure summary, real failure clusters (excludes superseded/cancelled bookkeeping), schedule health flags, tool usage top-25, memory health/pollution stats, seed-script candidate tool triplets, and a per-agent breakdown. Aggregates across ALL agents via direct read-only SQL.",
179
+ intent:
180
+ "Single-call daily compounding Phase 0 helper — replaces ~25 raw tool roundtrips with one compressed JSON result covering every agent. For daily evolution, self-scripting candidates, ops reviews, or heartbeat context.",
181
+ source: bundleCatalogReport(asText(compoundInsightsSrc)),
182
+ },
183
+ {
184
+ name: "memory-eval",
185
+ description:
186
+ "3-axis memory quality evaluation: carry-forward context (do follow-up tasks retrieve useful memories from prior tasks?), follow preferences (are CLAUDE.md/IDENTITY.md/SOUL.md/TOOLS.md memories retrieved and useful?), and stay current (what fraction of retrieved memories are fresh vs stale?). Outputs a baseline report to agent-fs + a swarm Page.",
187
+ intent:
188
+ "Measure memory system health across OpenAI Dreaming-inspired axes — before/after baseline for architecture changes, blog-post numbers, daily quality monitoring.",
189
+ source: asText(memoryEvalSrc),
190
+ },
191
+ {
192
+ name: "ops-catalog-audit",
193
+ description:
194
+ "Audit-as-code catalog check for schedules, workflows, and prompt/template drift. Clusters actionable findings by goal and can publish an authed HTML report page.",
195
+ intent:
196
+ "Re-run the ops inventory audit in one call: duplicate/dead schedules, code-work routing risks, enabled workflow fixtures, structured-output gate gaps, prompt registry drift, stale hosts, and systemDefault skill duplicates.",
197
+ source: bundleCatalogReport(asText(opsCatalogAuditSrc)),
198
+ },
199
+ {
200
+ name: "boot-triage",
201
+ description:
202
+ "Post-restart heartbeat triage snapshot: deploy restart PR context, recent real failures, stuck offline-agent work, orphaned tasks, and superseded tasks missing resume children.",
157
203
  intent:
158
- "Single-call daily compounding Phase 0 helper replaces ~25 raw tool roundtrips with one compressed JSON result covering every agent. For daily evolution, ops reviews, or heartbeat context.",
159
- source: asText(compoundInsightsSrc),
204
+ "Run immediately after a swarm restart to gather deterministic boot triage data in one read-only call before the Lead decides what to retry, cancel, or escalate.",
205
+ source: asText(bootTriageSrc),
160
206
  },
161
207
  ];
162
208
 
@@ -25,10 +25,10 @@ import kvStorageContent from "../../../templates/skills/kv-storage/content.md" w
25
25
  };
26
26
  import pagesConfig from "../../../templates/skills/pages/config.json" with { type: "text" };
27
27
  import pagesContent from "../../../templates/skills/pages/content.md" with { type: "text" };
28
- import scheduledTaskResilienceConfig from "../../../templates/skills/scheduled-task-resilience/config.json" with {
28
+ import scriptWorkflowsConfig from "../../../templates/skills/script-workflows/config.json" with {
29
29
  type: "text",
30
30
  };
31
- import scheduledTaskResilienceContent from "../../../templates/skills/scheduled-task-resilience/content.md" with {
31
+ import scriptWorkflowsContent from "../../../templates/skills/script-workflows/content.md" with {
32
32
  type: "text",
33
33
  };
34
34
  import swarmScriptsConfig from "../../../templates/skills/swarm-scripts/config.json" with {
@@ -71,7 +71,7 @@ const BUILT_IN_SKILL_SOURCES = [
71
71
  { config: artifactsConfig, body: artifactsContent },
72
72
  { config: kvStorageConfig, body: kvStorageContent },
73
73
  { config: pagesConfig, body: pagesContent },
74
- { config: scheduledTaskResilienceConfig, body: scheduledTaskResilienceContent },
74
+ { config: scriptWorkflowsConfig, body: scriptWorkflowsContent },
75
75
  { config: swarmScriptsConfig, body: swarmScriptsContent },
76
76
  { config: workflowIterateConfig, body: workflowIterateContent },
77
77
  { config: workflowStructuredOutputConfig, body: workflowStructuredOutputContent },
@@ -8,10 +8,11 @@
8
8
  * This runs on the API side — workers call it via POST /api/skills/sync-filesystem.
9
9
  */
10
10
 
11
+ import type { Dirent } from "node:fs";
11
12
  import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
12
13
  import { homedir } from "node:os";
13
- import { join } from "node:path";
14
- import { getAgentSkills } from "./db";
14
+ import { dirname, join } from "node:path";
15
+ import { getAgentSkills, getSkillFiles } from "./db";
15
16
 
16
17
  export interface SkillSyncResult {
17
18
  synced: number;
@@ -29,11 +30,68 @@ export interface SkillSyncResult {
29
30
  */
30
31
  const SWARM_MARKER_FILE = ".swarm-managed";
31
32
 
33
+ function reconcileManagedSkillFiles(skillDir: string, currentRelativeFiles: Set<string>): number {
34
+ if (!existsSync(join(skillDir, SWARM_MARKER_FILE))) return 0;
35
+
36
+ let removed = 0;
37
+
38
+ const walk = (dir: string, relativeDir = ""): boolean => {
39
+ let entries: Dirent[];
40
+ try {
41
+ entries = readdirSync(dir, { withFileTypes: true });
42
+ } catch {
43
+ return false;
44
+ }
45
+
46
+ let hasEntries = false;
47
+ for (const entry of entries) {
48
+ const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
49
+ const fullPath = join(dir, entry.name);
50
+
51
+ if (entry.isDirectory()) {
52
+ const childHasEntries = walk(fullPath, relativePath);
53
+ if (!childHasEntries) {
54
+ try {
55
+ rmSync(fullPath, { recursive: true, force: true });
56
+ } catch {
57
+ hasEntries = true;
58
+ }
59
+ } else {
60
+ hasEntries = true;
61
+ }
62
+ continue;
63
+ }
64
+
65
+ if (
66
+ relativePath === "SKILL.md" ||
67
+ relativePath === SWARM_MARKER_FILE ||
68
+ currentRelativeFiles.has(relativePath)
69
+ ) {
70
+ hasEntries = true;
71
+ continue;
72
+ }
73
+
74
+ try {
75
+ rmSync(fullPath, { force: true });
76
+ removed++;
77
+ } catch {
78
+ hasEntries = true;
79
+ }
80
+ }
81
+
82
+ return hasEntries;
83
+ };
84
+
85
+ walk(skillDir);
86
+ return removed;
87
+ }
88
+
32
89
  /**
33
90
  * Sync agent's installed skills to the filesystem.
34
91
  *
35
92
  * For simple skills (content in DB): writes SKILL.md to ~/.claude/skills/<name>/
36
- * For complex skills (isComplex=true): skipped here (handled by npx in entrypoint)
93
+ * For DB-backed complex skills: writes SKILL.md plus bundled skill_files rows.
94
+ * Legacy complex skills without skill_files remain handled by npx in entrypoint.
37
95
  */
38
96
  export function syncSkillsToFilesystem(
39
97
  agentId: string,
@@ -44,6 +102,7 @@ export function syncSkillsToFilesystem(
44
102
  const home = homeOverride ?? homedir();
45
103
  const errors: string[] = [];
46
104
  let synced = 0;
105
+ let removed = 0;
47
106
 
48
107
  // Directories to write to
49
108
  const skillDirs: string[] = [];
@@ -67,7 +126,8 @@ export function syncSkillsToFilesystem(
67
126
 
68
127
  for (const skill of skills) {
69
128
  if (!skill.isActive || !skill.isEnabled) continue;
70
- if (skill.isComplex) continue; // Complex skills handled by npx
129
+ const bundledFiles = skill.isComplex ? getSkillFiles(skill.id) : [];
130
+ if (skill.isComplex && bundledFiles.length === 0) continue; // Legacy complex skills handled by npx
71
131
  if (!skill.content) continue;
72
132
 
73
133
  // Sanitize skill name to prevent path traversal (strip /, .., and non-safe chars)
@@ -75,6 +135,9 @@ export function syncSkillsToFilesystem(
75
135
  if (!safeName) continue;
76
136
 
77
137
  writtenNames.add(safeName);
138
+ const currentBundledFilePaths = new Set(
139
+ bundledFiles.filter((file) => !file.isBinary).map((file) => file.path),
140
+ );
78
141
 
79
142
  for (const baseDir of skillDirs) {
80
143
  const skillDir = join(baseDir, safeName);
@@ -83,14 +146,36 @@ export function syncSkillsToFilesystem(
83
146
 
84
147
  try {
85
148
  mkdirSync(skillDir, { recursive: true });
149
+ removed += reconcileManagedSkillFiles(skillDir, currentBundledFilePaths);
86
150
  writeFileSync(skillFile, skill.content, "utf-8");
87
151
  writeFileSync(markerFile, "", "utf-8");
88
152
  synced++;
89
153
  } catch (err) {
90
- errors.push(
91
- `${skill.name} -> ${skillDir}: ${err instanceof Error ? err.message : "Unknown error"}`,
154
+ const msg = err instanceof Error ? err.message : "Unknown error";
155
+ errors.push(`${skill.name} -> ${skillDir}: ${msg}`);
156
+ console.error(
157
+ `[skill-sync] Failed to write SKILL.md for ${skill.name} to ${skillDir}: ${msg}`,
92
158
  );
93
159
  }
160
+
161
+ for (const file of bundledFiles) {
162
+ if (file.isBinary) {
163
+ console.log(`[skill-sync] Skipping binary skill file ${skill.name}/${file.path}`);
164
+ continue;
165
+ }
166
+
167
+ const targetPath = join(skillDir, file.path);
168
+ try {
169
+ mkdirSync(dirname(targetPath), { recursive: true });
170
+ writeFileSync(targetPath, file.content, "utf-8");
171
+ } catch (err) {
172
+ const msg = err instanceof Error ? err.message : "Unknown error";
173
+ errors.push(`${skill.name}/${file.path} -> ${targetPath}: ${msg}`);
174
+ console.error(
175
+ `[skill-sync] Failed to write bundled file ${skill.name}/${file.path} to ${targetPath}: ${msg}`,
176
+ );
177
+ }
178
+ }
94
179
  }
95
180
  }
96
181
 
@@ -98,7 +183,6 @@ export function syncSkillsToFilesystem(
98
183
  // present). Leaves user-installed personal skills alone — important on
99
184
  // local dev where ~/.codex/skills holds skills the user installed
100
185
  // outside the swarm.
101
- let removed = 0;
102
186
  for (const baseDir of skillDirs) {
103
187
  if (!existsSync(baseDir)) continue;
104
188
 
@@ -41,6 +41,23 @@ const VALIDATED_KEYS: Record<string, (value: unknown) => string | null> = {
41
41
  if (parsed.success) return null;
42
42
  return `Invalid HARNESS_PROVIDER value (must be one of: ${ProviderNameSchema.options.join(", ")})`;
43
43
  },
44
+ // Codex credits-exhausted cooldown (ms). Permissive on range here (positive
45
+ // integer) — the worker clamps to [5m, 7d] via resolveCodexCreditsExhaustedCooldownMs.
46
+ CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS: (value) => {
47
+ const str = String(value).trim();
48
+ if (!/^\d+$/.test(str) || Number(str) <= 0) {
49
+ return "Invalid CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS (must be a positive integer of milliseconds)";
50
+ }
51
+ return null;
52
+ },
53
+ SWARM_USE_CLAUDE_BRIDGE: (value) => {
54
+ if (typeof value !== "string") {
55
+ return "Invalid SWARM_USE_CLAUDE_BRIDGE value (must be one of: true, false, 1, 0)";
56
+ }
57
+ const normalized = value.trim().toLowerCase();
58
+ if (["true", "false", "1", "0"].includes(normalized)) return null;
59
+ return "Invalid SWARM_USE_CLAUDE_BRIDGE value (must be one of: true, false, 1, 0)";
60
+ },
44
61
  };
45
62
 
46
63
  export function validateConfigValue(key: string, value: unknown): string | null {
@@ -40,9 +40,11 @@ import { getMcpBaseUrl } from "../utils/constants.ts";
40
40
  import { getContextWindowSize } from "../utils/context-window.ts";
41
41
  import { type CredentialSelection, resolveCredentialPools } from "../utils/credentials.ts";
42
42
  import {
43
+ isCodexCreditsExhaustedMessage,
43
44
  isRateLimitMessage,
44
45
  MAX_RATE_LIMIT_RESET_MS,
45
46
  parseRateLimitResetTime,
47
+ resolveCodexCreditsExhaustedCooldownMs,
46
48
  } from "../utils/error-tracker.ts";
47
49
  import { resolveHarnessProvider } from "../utils/harness-provider.ts";
48
50
  import { prettyPrintLine, prettyPrintStderr } from "../utils/pretty-print.ts";
@@ -424,6 +426,7 @@ async function fetchResolvedEnv(
424
426
  const RELOADABLE_ENV_KEYS: ReadonlySet<string> = new Set([
425
427
  "MODEL_OVERRIDE",
426
428
  "AGENT_FS_SHARED_ORG_ID",
429
+ "SWARM_USE_CLAUDE_BRIDGE",
427
430
  ]);
428
431
 
429
432
  /**
@@ -1483,6 +1486,13 @@ interface RunnerState {
1483
1486
  * (per-task live re-resolution) will mutate this between tasks.
1484
1487
  */
1485
1488
  harnessProvider: ProviderName;
1489
+ /**
1490
+ * Effective Codex credits-exhausted cooldown (ms), resolved from `swarm_config`
1491
+ * (key `CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS`) > default 2h constant, clamped to
1492
+ * [5m, 7d]. Reconciled live by `applySwarmConfigDrift` — read at the cooldown
1493
+ * application site so a fresh value applies to the next credits-exhausted failure.
1494
+ */
1495
+ codexCreditsExhaustedCooldownMs: number;
1486
1496
  }
1487
1497
 
1488
1498
  /** Buffer for session logs */
@@ -3117,6 +3127,7 @@ async function checkCompletedProcesses(
3117
3127
  state: RunnerState,
3118
3128
  role: string,
3119
3129
  apiConfig?: ApiConfig,
3130
+ cancelledSignaled?: Set<string>,
3120
3131
  ): Promise<void> {
3121
3132
  const completedTasks: Array<{
3122
3133
  taskId: string;
@@ -3151,6 +3162,9 @@ async function checkCompletedProcesses(
3151
3162
  // Remove completed tasks from the map and ensure they're marked as finished
3152
3163
  for (const { taskId, result, cursorUpdates, workingDir, credentialInfo } of completedTasks) {
3153
3164
  state.activeTasks.delete(taskId);
3165
+ vcsDetectedTasks.delete(taskId);
3166
+ vcsCheckTimestamps.delete(taskId);
3167
+ cancelledSignaled?.delete(taskId);
3154
3168
 
3155
3169
  if (apiConfig) {
3156
3170
  removeActiveSession(apiConfig, taskId);
@@ -3209,6 +3223,12 @@ async function checkCompletedProcesses(
3209
3223
  console.log(
3210
3224
  `[credentials] Parsed rate limit reset time from error: ${rateLimitedUntil}`,
3211
3225
  );
3226
+ } else if (isCodexCreditsExhaustedMessage(failureReason)) {
3227
+ const cooldownMs = state.codexCreditsExhaustedCooldownMs;
3228
+ rateLimitedUntil = new Date(Date.now() + cooldownMs).toISOString();
3229
+ console.log(
3230
+ `[credentials] Codex credits exhausted — applying cooldown (${cooldownMs}ms): ${rateLimitedUntil}`,
3231
+ );
3212
3232
  } else {
3213
3233
  rateLimitedUntil = new Date(Date.now() + 5 * 60 * 1000).toISOString();
3214
3234
  }
@@ -3403,10 +3423,22 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
3403
3423
  // Failures (network, API down, malformed value) fall back to env then "claude"
3404
3424
  // so a swarm_config outage cannot wedge boot.
3405
3425
  let bootProvider: ProviderName;
3426
+ // Codex credits-exhausted cooldown is sourced solely from the global swarm_config
3427
+ // (key `CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS`). Initialize to the default constant
3428
+ // for the case where it is unset, then apply the swarm_config value below; on a
3429
+ // boot-fetch failure it stays at the default. Reconciled live thereafter by
3430
+ // `applySwarmConfigDrift`.
3431
+ let bootCooldownMs = resolveCodexCreditsExhaustedCooldownMs(undefined);
3406
3432
  try {
3407
- bootProvider = (await fetchResolvedEnv(apiUrl, apiKey, agentId)).resolvedProvider;
3433
+ const bootEnv = await fetchResolvedEnv(apiUrl, apiKey, agentId);
3434
+ bootProvider = bootEnv.resolvedProvider;
3435
+ bootCooldownMs = resolveCodexCreditsExhaustedCooldownMs(
3436
+ bootEnv.env.CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS,
3437
+ );
3408
3438
  } catch (err) {
3409
- console.warn(`[runner] fetchResolvedEnv failed at boot, falling back to env: ${err}`);
3439
+ console.warn(
3440
+ `[runner] fetchResolvedEnv failed at boot, falling back to env for provider and the default cooldown: ${err}`,
3441
+ );
3410
3442
  bootProvider = resolveHarnessProvider({}, process.env);
3411
3443
  }
3412
3444
  console.log(`[runner] Resolved HARNESS_PROVIDER: ${bootProvider}`);
@@ -3610,6 +3642,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
3610
3642
  activeTasks: new Map(),
3611
3643
  maxConcurrent,
3612
3644
  harnessProvider: bootProvider,
3645
+ codexCreditsExhaustedCooldownMs: bootCooldownMs,
3613
3646
  };
3614
3647
 
3615
3648
  // Track tasks already signaled for cancellation to avoid repeated SIGTERM
@@ -3690,6 +3723,18 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
3690
3723
  agentVisibleChanged = true;
3691
3724
  }
3692
3725
 
3726
+ // (2b) Codex credits-exhausted cooldown — operator-tunable live. Not
3727
+ // agent-visible (doesn't change provider/maxConcurrent → no re-register).
3728
+ const nextCooldown = resolveCodexCreditsExhaustedCooldownMs(
3729
+ freshEnv.CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS,
3730
+ );
3731
+ if (nextCooldown !== state.codexCreditsExhaustedCooldownMs) {
3732
+ console.log(
3733
+ `[${role}] [config] codexCreditsExhaustedCooldownMs: ${state.codexCreditsExhaustedCooldownMs} → ${nextCooldown}`,
3734
+ );
3735
+ state.codexCreditsExhaustedCooldownMs = nextCooldown;
3736
+ }
3737
+
3693
3738
  // (3) Apply the small allowlist of safe-to-mutate env keys to process.env.
3694
3739
  const changedKeys = applyResolvedEnvToProcessEnv(freshEnv);
3695
3740
  if (changedKeys.length > 0) {
@@ -4081,7 +4126,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
4081
4126
 
4082
4127
  // Wait if at capacity (though unlikely on fresh startup)
4083
4128
  while (state.activeTasks.size >= state.maxConcurrent) {
4084
- await checkCompletedProcesses(state, role, apiConfig);
4129
+ await checkCompletedProcesses(state, role, apiConfig, cancelledSignaled);
4085
4130
  await Bun.sleep(1000);
4086
4131
  }
4087
4132
 
@@ -4300,7 +4345,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
4300
4345
  await pingServer(apiConfig, role);
4301
4346
 
4302
4347
  // Check for completed processes first and ensure tasks are marked as finished
4303
- await checkCompletedProcesses(state, role, apiConfig);
4348
+ await checkCompletedProcesses(state, role, apiConfig, cancelledSignaled);
4304
4349
 
4305
4350
  // Live HARNESS_PROVIDER reconciliation. Re-fetches `swarm_config` (overlaid
4306
4351
  // on env) and swaps the adapter if the resolved provider changed —