@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
|
@@ -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
|
-
|
|
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
|
|
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({
|
|
39
|
+
const res: any = await ctx.swarm.db_query({ sql });
|
|
40
40
|
const payload = res?.data ?? res;
|
|
41
|
-
const
|
|
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
|
-
"
|
|
159
|
-
source: asText(
|
|
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
|
|
28
|
+
import scriptWorkflowsConfig from "../../../templates/skills/script-workflows/config.json" with {
|
|
29
29
|
type: "text",
|
|
30
30
|
};
|
|
31
|
-
import
|
|
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:
|
|
74
|
+
{ config: scriptWorkflowsConfig, body: scriptWorkflowsContent },
|
|
75
75
|
{ config: swarmScriptsConfig, body: swarmScriptsContent },
|
|
76
76
|
{ config: workflowIterateConfig, body: workflowIterateContent },
|
|
77
77
|
{ config: workflowStructuredOutputConfig, body: workflowStructuredOutputContent },
|
package/src/be/skill-sync.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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 {
|
package/src/commands/runner.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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 —
|