@desplega.ai/agent-swarm 1.90.0 → 1.92.0

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 (96) hide show
  1. package/README.md +2 -1
  2. package/openapi.json +803 -150
  3. package/package.json +5 -5
  4. package/src/artifact-sdk/server.ts +2 -1
  5. package/src/be/db.ts +337 -1
  6. package/src/be/memory/providers/sqlite-store.ts +6 -1
  7. package/src/be/memory/types.ts +1 -0
  8. package/src/be/migrations/083_script_workflows.sql +51 -0
  9. package/src/be/modelsdev-cache.json +42352 -38595
  10. package/src/be/scripts/typecheck.ts +181 -1
  11. package/src/be/seed-scripts/catalog/compound-insights.ts +398 -0
  12. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +911 -0
  13. package/src/be/seed-scripts/catalog/schedule-health.ts +73 -0
  14. package/src/be/seed-scripts/catalog/smart-recall.ts +65 -0
  15. package/src/be/seed-scripts/catalog/task-context-gathering.ts +92 -0
  16. package/src/be/seed-scripts/catalog/tool-usage.ts +59 -0
  17. package/src/be/seed-scripts/index.ts +54 -0
  18. package/src/be/seed-skills/index.ts +7 -0
  19. package/src/be/swarm-config-guard.ts +17 -0
  20. package/src/commands/artifact.ts +3 -2
  21. package/src/commands/profile-sync.ts +310 -0
  22. package/src/commands/runner.ts +134 -3
  23. package/src/hooks/hook.ts +32 -9
  24. package/src/http/db-query.ts +20 -5
  25. package/src/http/index.ts +57 -0
  26. package/src/http/integrations.ts +6 -1
  27. package/src/http/mcp-bridge.ts +117 -0
  28. package/src/http/mcp-oauth.ts +97 -39
  29. package/src/http/memory.ts +5 -2
  30. package/src/http/openapi.ts +2 -2
  31. package/src/http/pages-public.ts +10 -11
  32. package/src/http/pages.ts +7 -11
  33. package/src/http/script-runs.ts +555 -0
  34. package/src/http/scripts.ts +24 -1
  35. package/src/http/utils.ts +11 -4
  36. package/src/jira/app.ts +2 -3
  37. package/src/jira/webhook-lifecycle.ts +2 -1
  38. package/src/linear/app.ts +2 -3
  39. package/src/prompts/session-templates.ts +24 -4
  40. package/src/providers/claude-adapter.ts +86 -13
  41. package/src/script-workflows/executor.ts +110 -0
  42. package/src/script-workflows/harness.ts +73 -0
  43. package/src/script-workflows/label-lint.ts +51 -0
  44. package/src/script-workflows/limits.ts +22 -0
  45. package/src/script-workflows/supervisor.ts +139 -0
  46. package/src/script-workflows/workflow-ctx.ts +205 -0
  47. package/src/scripts-runtime/executors/native.ts +1 -0
  48. package/src/scripts-runtime/sdk-allowlist.ts +124 -0
  49. package/src/scripts-runtime/swarm-sdk.ts +198 -3
  50. package/src/scripts-runtime/types/stdlib.d.ts +287 -0
  51. package/src/scripts-runtime/types/swarm-sdk.d.ts +287 -0
  52. package/src/server.ts +2 -0
  53. package/src/slack/handlers.ts +11 -4
  54. package/src/slack/message-text.ts +98 -0
  55. package/src/slack/thread-buffer.ts +5 -3
  56. package/src/tests/claude-adapter-binary.test.ts +147 -4
  57. package/src/tests/claude-adapter-otel.test.ts +85 -1
  58. package/src/tests/db-query.test.ts +28 -0
  59. package/src/tests/error-tracker.test.ts +121 -0
  60. package/src/tests/harness-provider-resolution.test.ts +33 -0
  61. package/src/tests/hook-registration-nudge.test.ts +69 -0
  62. package/src/tests/mcp-oauth-manual-client.test.ts +213 -0
  63. package/src/tests/mcp-tools.test.ts +6 -0
  64. package/src/tests/pages-public-html.test.ts +41 -0
  65. package/src/tests/pages-public-json-redirect.test.ts +37 -2
  66. package/src/tests/profile-sync.test.ts +282 -0
  67. package/src/tests/prompt-template-session.test.ts +34 -5
  68. package/src/tests/script-runs-http.test.ts +278 -0
  69. package/src/tests/script-workflows-label-lint.test.ts +43 -0
  70. package/src/tests/script-workflows-runtime-e2e.test.ts +170 -0
  71. package/src/tests/scripts-mcp-e2e.test.ts +49 -2
  72. package/src/tests/scripts-runtime.test.ts +33 -0
  73. package/src/tests/seed-scripts.test.ts +347 -2
  74. package/src/tests/slack-message-text.test.ts +250 -0
  75. package/src/tests/system-default-skills.test.ts +40 -0
  76. package/src/tools/create-metric.ts +2 -3
  77. package/src/tools/create-page.ts +3 -6
  78. package/src/tools/db-query.ts +16 -6
  79. package/src/tools/memory-rate.ts +2 -1
  80. package/src/tools/memory-search.ts +1 -0
  81. package/src/tools/register-kapso-number.ts +2 -4
  82. package/src/tools/request-human-input.ts +2 -1
  83. package/src/tools/script-common.ts +2 -4
  84. package/src/tools/script-run.ts +7 -0
  85. package/src/tools/script-runs.ts +123 -0
  86. package/src/tools/slack-read.ts +12 -3
  87. package/src/tools/tool-config.ts +4 -1
  88. package/src/types.ts +52 -0
  89. package/src/utils/constants.ts +58 -8
  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/skills/pages/content.md +205 -55
  94. package/templates/skills/script-workflows/config.json +14 -0
  95. package/templates/skills/script-workflows/content.md +68 -0
  96. package/templates/skills/swarm-scripts/content.md +45 -7
@@ -0,0 +1,911 @@
1
+ import { z } from "zod";
2
+
3
+ export const argsSchema = z.object({
4
+ nowIso: z.string().optional().describe("Audit clock override (default: current time)"),
5
+ publishPage: z.boolean().optional().describe("Publish an authed HTML page (default true)"),
6
+ includeSamples: z
7
+ .boolean()
8
+ .optional()
9
+ .describe("Include small row samples for each finding cluster (default true)"),
10
+ staleScheduleDays: z
11
+ .number()
12
+ .int()
13
+ .positive()
14
+ .optional()
15
+ .describe("Flag enabled schedules with no run for this many days (default 14)"),
16
+ tempScheduleNames: z
17
+ .array(z.string())
18
+ .optional()
19
+ .describe("Known temporary schedule name fragments that should self-lift"),
20
+ });
21
+
22
+ const CODE_WORK_RE =
23
+ /\b(git|github|gh\b|gh-cli|docker|docker-compose|bun|npm|pnpm|yarn|tsc|eslint|lint|test|pr\b|pull request|branch|commit|repo|worktree|typescript|javascript)\b/i;
24
+ const CODE_AGENT_RE =
25
+ /\b(code|coder|coding|implement|implementation|engineer|software|typescript|javascript|repo|github|picateclas)\b/i;
26
+ const NON_CODE_AGENT_RE = /\b(content|reviewer|research|sales|gtm|support|ops|lead)\b/i;
27
+ const SMOKE_WORKFLOW_RE =
28
+ /\b(smoke|demo|litmus-smoke|one[- ]shot|validation|des-462-gate-validation|gsc-runtime-smoke)\b/i;
29
+ const GATE_WORKFLOW_RE = /\b(litmus|gate|eval|review|validation|quality|structured)\b/i;
30
+ const STALE_URL_RE =
31
+ /\b(localhost|127\.0\.0\.1|api\.example-swarm\.dev|app\.example-swarm\.dev|example-swarm\.dev|fly\.dev)\b/i;
32
+ const CONTRADICTORY_RE =
33
+ /\b(do not|don't|never)\b[\s\S]{0,240}\b(always|must|required)\b|\b(always|must|required)\b[\s\S]{0,240}\b(do not|don't|never)\b/i;
34
+
35
+ const CODE_REGISTRY_EVENTS =
36
+ "agentmail.email.followup agentmail.email.mapped_lead agentmail.email.mapped_worker agentmail.email.no_agent agentmail.email.unmapped common.command_suggestions.github_comment_issue common.command_suggestions.github_comment_pr common.command_suggestions.github_issue common.command_suggestions.github_pr common.command_suggestions.gitlab_issue common.command_suggestions.gitlab_mr common.delegation_instruction common.delegation_instruction.gitlab github.check_run.failed github.check_suite.failed github.comment.mentioned github.issue.assigned github.issue.labeled github.issue.mentioned github.pull_request.assigned github.pull_request.closed github.pull_request.labeled github.pull_request.mentioned github.pull_request.review_requested github.pull_request.review_submitted github.pull_request.synchronize github.workflow_run.failed gitlab.comment.mentioned gitlab.issue.assigned gitlab.merge_request.opened gitlab.pipeline.failed heartbeat.boot-triage heartbeat.checklist jira.issue.assigned jira.issue.commented jira.issue.followup kapso.message.received linear.issue.assigned linear.issue.followup linear.issue.reassigned slack.assistant.greeting slack.assistant.offline slack.assistant.suggested_prompts slack.message.thread_context system.agent.agent_fs system.agent.artifacts system.agent.code_quality system.agent.context_mode system.agent.filesystem system.agent.lead system.agent.register system.agent.role system.agent.seed_scripts system.agent.self_awareness system.agent.services system.agent.share_urls system.agent.slack system.agent.system system.agent.worker system.agent.worker.remote system.agent.worker.slack system.session.lead system.session.worker system.session.worker.pi system.session.worker.remote task.budget.refused task.requester.profile task.worker.completed task.worker.failed".split(
37
+ " ",
38
+ );
39
+
40
+ function rowsToObjects(res: any): any[] {
41
+ const p = res?.data ?? res;
42
+ const cols: string[] = p?.columns ?? [];
43
+ return (p?.rows ?? []).map((r: any) =>
44
+ Array.isArray(r) ? Object.fromEntries(cols.map((c, i) => [c, r[i]])) : r,
45
+ );
46
+ }
47
+
48
+ async function query(ctx: any, sql: string, params?: unknown[]): Promise<any[]> {
49
+ try {
50
+ return rowsToObjects(await ctx.swarm.db_query({ sql, params }));
51
+ } catch (error) {
52
+ return [{ unavailable: error instanceof Error ? error.message : String(error) }];
53
+ }
54
+ }
55
+
56
+ function safeJson(value: unknown, fallback: any = null): any {
57
+ if (typeof value !== "string") return value ?? fallback;
58
+ try {
59
+ return JSON.parse(value);
60
+ } catch {
61
+ return fallback;
62
+ }
63
+ }
64
+
65
+ function asBool(value: unknown): boolean {
66
+ return value === true || value === 1 || value === "1";
67
+ }
68
+
69
+ function asText(value: unknown): string {
70
+ return typeof value === "string" ? value : value == null ? "" : JSON.stringify(value);
71
+ }
72
+
73
+ function compactText(value: unknown, max = 180): string {
74
+ return asText(value).replace(/\s+/g, " ").trim().slice(0, max);
75
+ }
76
+
77
+ function daysSince(iso: string | null | undefined, nowMs: number): number | null {
78
+ if (!iso) return null;
79
+ const t = Date.parse(iso);
80
+ if (!Number.isFinite(t)) return null;
81
+ return Math.round((nowMs - t) / 86400000);
82
+ }
83
+
84
+ function severity(rank: number): "critical" | "high" | "medium" | "low" {
85
+ if (rank >= 4) return "critical";
86
+ if (rank === 3) return "high";
87
+ if (rank === 2) return "medium";
88
+ return "low";
89
+ }
90
+
91
+ function sample<T>(rows: T[], includeSamples: boolean, limit = 5): T[] {
92
+ return includeSamples ? rows.slice(0, limit) : [];
93
+ }
94
+
95
+ function isCodeCapable(agent: any): boolean {
96
+ if (!agent) return false;
97
+ const text = [
98
+ agent.name,
99
+ agent.role,
100
+ agent.description,
101
+ Array.isArray(agent.capabilities) ? agent.capabilities.join(" ") : agent.capabilities,
102
+ ]
103
+ .filter(Boolean)
104
+ .join(" ");
105
+ if (!CODE_AGENT_RE.test(text)) return false;
106
+ if (NON_CODE_AGENT_RE.test(text) && !/\bpicateclas\b/i.test(text)) return false;
107
+ return true;
108
+ }
109
+
110
+ function nodeList(definition: any): any[] {
111
+ return Array.isArray(definition?.nodes) ? definition.nodes : [];
112
+ }
113
+
114
+ function hasStructuredOutput(value: any): boolean {
115
+ return /"outputSchema"|"schema"|"jsonSchema"|"structured"/i.test(JSON.stringify(value ?? {}));
116
+ }
117
+
118
+ function htmlEscape(value: unknown): string {
119
+ return asText(value)
120
+ .replace(/&/g, "&amp;")
121
+ .replace(/</g, "&lt;")
122
+ .replace(/>/g, "&gt;")
123
+ .replace(/"/g, "&quot;");
124
+ }
125
+
126
+ function humanLabel(value: string): string {
127
+ return value
128
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
129
+ .replace(/[-_.]+/g, " ")
130
+ .replace(/\b\w/g, (char) => char.toUpperCase());
131
+ }
132
+
133
+ function formatMetric(value: unknown): string {
134
+ if (typeof value === "number") return new Intl.NumberFormat("en-US").format(value);
135
+ return asText(value);
136
+ }
137
+
138
+ function severityTone(value: string): string {
139
+ if (value === "critical") return "danger";
140
+ if (value === "high") return "warn";
141
+ if (value === "medium") return "note";
142
+ return "low";
143
+ }
144
+
145
+ function renderSampleValue(value: unknown): string {
146
+ if (Array.isArray(value)) {
147
+ return value
148
+ .map((item) => (typeof item === "object" && item !== null ? JSON.stringify(item) : asText(item)))
149
+ .join(", ");
150
+ }
151
+ if (value && typeof value === "object") return JSON.stringify(value);
152
+ return asText(value);
153
+ }
154
+
155
+ function renderSamples(samples: any[]): string {
156
+ if (!Array.isArray(samples) || samples.length === 0) return "";
157
+ const normalized = samples.map((sampleRow) =>
158
+ sampleRow && typeof sampleRow === "object" && !Array.isArray(sampleRow)
159
+ ? sampleRow
160
+ : { value: sampleRow },
161
+ );
162
+ const columns = Array.from(
163
+ new Set(normalized.flatMap((sampleRow) => Object.keys(sampleRow).slice(0, 6))),
164
+ ).slice(0, 6);
165
+ if (columns.length === 0) return "";
166
+ const rows = normalized
167
+ .map(
168
+ (sampleRow) =>
169
+ `<tr>${columns
170
+ .map((column) => `<td>${htmlEscape(renderSampleValue(sampleRow[column]))}</td>`)
171
+ .join("")}</tr>`,
172
+ )
173
+ .join("");
174
+ return `<div class="sample-table" aria-label="Sample rows">
175
+ <table>
176
+ <thead><tr>${columns.map((column) => `<th>${htmlEscape(humanLabel(column))}</th>`).join("")}</tr></thead>
177
+ <tbody>${rows}</tbody>
178
+ </table>
179
+ </div>`;
180
+ }
181
+
182
+ export function renderPage(result: any): string {
183
+ const metrics = [
184
+ ["Findings", result.summary.findingsTotal],
185
+ ["Schedules enabled", result.summary.schedulesEnabled],
186
+ ["Workflows enabled", result.summary.workflowsEnabled],
187
+ ["Prompt templates", result.summary.promptTemplates],
188
+ ];
189
+ const sections = ["schedules", "workflows", "promptsTemplates"]
190
+ .map((key) => {
191
+ const group = result.goals[key];
192
+ const findings = group.findings
193
+ .map(
194
+ (finding: any) => `<article class="finding ${htmlEscape(severityTone(finding.severity))}">
195
+ <div class="finding-head">
196
+ <div>
197
+ <p class="finding-id">${htmlEscape(finding.id)}</p>
198
+ <h3>${htmlEscape(finding.summary)}</h3>
199
+ </div>
200
+ <span class="pill ${htmlEscape(severityTone(finding.severity))}">${htmlEscape(
201
+ finding.severity,
202
+ )}</span>
203
+ </div>
204
+ <p class="action">${htmlEscape(finding.action)}</p>
205
+ ${renderSamples(finding.samples)}
206
+ </article>`,
207
+ )
208
+ .join("");
209
+ const checks = Object.entries(group.checks || {})
210
+ .map(
211
+ ([label, value]) =>
212
+ `<div class="check"><span>${htmlEscape(humanLabel(label))}</span><strong>${htmlEscape(
213
+ formatMetric(value),
214
+ )}</strong></div>`,
215
+ )
216
+ .join("");
217
+ return `<section class="section">
218
+ <div class="section-grid">
219
+ <aside class="checks">
220
+ <p class="section-kicker">${htmlEscape(humanLabel(key))}</p>
221
+ <div class="check-list">${checks}</div>
222
+ </aside>
223
+ <div>
224
+ <div class="section-head">
225
+ <h2>${htmlEscape(group.goal)}</h2>
226
+ <span>${htmlEscape(formatMetric(group.findingCount))} finding(s)</span>
227
+ </div>
228
+ <div class="findings">
229
+ ${findings || '<p class="empty">No actionable findings in this cluster.</p>'}
230
+ </div>
231
+ </div>
232
+ </div>
233
+ </section>`;
234
+ })
235
+ .join("\n");
236
+ return `<!doctype html>
237
+ <html lang="en">
238
+ <head>
239
+ <meta charset="utf-8">
240
+ <meta name="viewport" content="width=device-width, initial-scale=1">
241
+ <meta name="theme-color" content="#f5f2ea">
242
+ <title>Ops Catalog Audit</title>
243
+ <style>
244
+ :root {
245
+ color-scheme: light;
246
+ --bg: #f5f2ea;
247
+ --panel: #ffffff;
248
+ --ink: #18181b;
249
+ --muted: #5f6368;
250
+ --line: #ded8cb;
251
+ --accent: #255c99;
252
+ --danger: #b42318;
253
+ --danger-bg: #fff1f0;
254
+ --warn: #b54708;
255
+ --warn-bg: #fff7ed;
256
+ --note: #175cd3;
257
+ --note-bg: #eff6ff;
258
+ --low: #067647;
259
+ --low-bg: #ecfdf3;
260
+ --radius: 8px;
261
+ --shadow: 0 1px 2px rgba(24, 24, 27, 0.06), 0 14px 36px rgba(24, 24, 27, 0.07);
262
+ }
263
+ * { box-sizing: border-box; }
264
+ body {
265
+ margin: 0;
266
+ background: var(--bg);
267
+ color: var(--ink);
268
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
269
+ font-size: 16px;
270
+ line-height: 1.55;
271
+ }
272
+ main {
273
+ width: min(1120px, calc(100% - 32px));
274
+ margin: 0 auto;
275
+ padding: 48px 0 72px;
276
+ }
277
+ header { margin-bottom: 28px; }
278
+ .eyebrow {
279
+ margin: 0 0 8px;
280
+ color: var(--muted);
281
+ font-size: 13px;
282
+ font-weight: 750;
283
+ letter-spacing: 0.08em;
284
+ text-transform: uppercase;
285
+ }
286
+ h1 {
287
+ margin: 0;
288
+ max-width: 860px;
289
+ font-size: clamp(2rem, 4vw, 3rem);
290
+ line-height: 1.05;
291
+ letter-spacing: 0;
292
+ }
293
+ .lede {
294
+ max-width: 780px;
295
+ margin: 16px 0 0;
296
+ color: var(--muted);
297
+ font-size: 18px;
298
+ }
299
+ .metrics {
300
+ display: grid;
301
+ grid-template-columns: repeat(4, minmax(0, 1fr));
302
+ gap: 12px;
303
+ margin: 32px 0;
304
+ }
305
+ .metric, .section, details {
306
+ background: var(--panel);
307
+ border: 1px solid var(--line);
308
+ border-radius: var(--radius);
309
+ box-shadow: var(--shadow);
310
+ }
311
+ .metric { padding: 18px; }
312
+ .metric strong {
313
+ display: block;
314
+ font-size: 32px;
315
+ line-height: 1;
316
+ font-variant-numeric: tabular-nums;
317
+ }
318
+ .metric span {
319
+ display: block;
320
+ margin-top: 8px;
321
+ color: var(--muted);
322
+ font-size: 13px;
323
+ font-weight: 650;
324
+ }
325
+ .section {
326
+ margin-top: 18px;
327
+ padding: 24px;
328
+ }
329
+ .section-grid {
330
+ display: grid;
331
+ grid-template-columns: 260px minmax(0, 1fr);
332
+ gap: 28px;
333
+ }
334
+ .section-kicker {
335
+ margin: 0 0 12px;
336
+ color: var(--accent);
337
+ font-size: 13px;
338
+ font-weight: 800;
339
+ letter-spacing: 0.08em;
340
+ text-transform: uppercase;
341
+ }
342
+ .check-list {
343
+ display: grid;
344
+ gap: 8px;
345
+ }
346
+ .check {
347
+ display: flex;
348
+ align-items: baseline;
349
+ justify-content: space-between;
350
+ gap: 12px;
351
+ padding: 10px 0;
352
+ border-bottom: 1px solid var(--line);
353
+ }
354
+ .check span {
355
+ color: var(--muted);
356
+ font-size: 13px;
357
+ }
358
+ .check strong {
359
+ font-size: 18px;
360
+ font-variant-numeric: tabular-nums;
361
+ }
362
+ .section-head {
363
+ display: flex;
364
+ align-items: start;
365
+ justify-content: space-between;
366
+ gap: 16px;
367
+ margin-bottom: 16px;
368
+ }
369
+ .section-head h2 {
370
+ max-width: 680px;
371
+ margin: 0;
372
+ font-size: 24px;
373
+ line-height: 1.2;
374
+ letter-spacing: 0;
375
+ }
376
+ .section-head > span {
377
+ flex: 0 0 auto;
378
+ color: var(--muted);
379
+ font-size: 13px;
380
+ font-weight: 700;
381
+ white-space: nowrap;
382
+ }
383
+ .findings {
384
+ display: grid;
385
+ gap: 12px;
386
+ }
387
+ .finding {
388
+ border: 1px solid var(--line);
389
+ border-left: 4px solid var(--note);
390
+ border-radius: var(--radius);
391
+ padding: 16px;
392
+ background: #fffdf8;
393
+ }
394
+ .finding.danger { border-left-color: var(--danger); }
395
+ .finding.warn { border-left-color: var(--warn); }
396
+ .finding.low { border-left-color: var(--low); }
397
+ .finding-head {
398
+ display: flex;
399
+ align-items: start;
400
+ justify-content: space-between;
401
+ gap: 16px;
402
+ }
403
+ .finding-id {
404
+ margin: 0 0 4px;
405
+ color: var(--muted);
406
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
407
+ font-size: 12px;
408
+ }
409
+ h3 {
410
+ margin: 0;
411
+ font-size: 17px;
412
+ line-height: 1.3;
413
+ letter-spacing: 0;
414
+ }
415
+ .pill {
416
+ display: inline-flex;
417
+ align-items: center;
418
+ min-height: 26px;
419
+ padding: 4px 9px;
420
+ border-radius: 999px;
421
+ font-size: 12px;
422
+ font-weight: 800;
423
+ text-transform: uppercase;
424
+ white-space: nowrap;
425
+ }
426
+ .pill.danger { background: var(--danger-bg); color: var(--danger); }
427
+ .pill.warn { background: var(--warn-bg); color: var(--warn); }
428
+ .pill.note { background: var(--note-bg); color: var(--note); }
429
+ .pill.low { background: var(--low-bg); color: var(--low); }
430
+ .action {
431
+ margin: 10px 0 0;
432
+ color: var(--muted);
433
+ }
434
+ .sample-table {
435
+ margin-top: 14px;
436
+ overflow-x: auto;
437
+ border: 1px solid var(--line);
438
+ border-radius: var(--radius);
439
+ background: var(--panel);
440
+ }
441
+ table {
442
+ width: 100%;
443
+ min-width: 640px;
444
+ border-collapse: collapse;
445
+ }
446
+ th, td {
447
+ padding: 10px 12px;
448
+ border-bottom: 1px solid var(--line);
449
+ text-align: left;
450
+ vertical-align: top;
451
+ }
452
+ th {
453
+ color: var(--muted);
454
+ font-size: 12px;
455
+ font-weight: 800;
456
+ letter-spacing: 0.06em;
457
+ text-transform: uppercase;
458
+ }
459
+ td {
460
+ max-width: 360px;
461
+ color: #27272a;
462
+ font-size: 13px;
463
+ overflow-wrap: anywhere;
464
+ }
465
+ tr:last-child td { border-bottom: 0; }
466
+ .empty {
467
+ margin: 0;
468
+ color: var(--muted);
469
+ }
470
+ details {
471
+ margin-top: 24px;
472
+ padding: 18px;
473
+ }
474
+ summary {
475
+ cursor: pointer;
476
+ font-weight: 800;
477
+ }
478
+ pre {
479
+ margin: 16px 0 0;
480
+ max-height: 560px;
481
+ overflow: auto;
482
+ padding: 16px;
483
+ border-radius: var(--radius);
484
+ background: #111827;
485
+ color: #f9fafb;
486
+ font-size: 12px;
487
+ line-height: 1.45;
488
+ }
489
+ @media (max-width: 860px) {
490
+ main { width: min(100% - 24px, 1120px); padding-top: 32px; }
491
+ .metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
492
+ .section-grid { grid-template-columns: 1fr; gap: 18px; }
493
+ .section { padding: 18px; }
494
+ .section-head { display: block; }
495
+ .section-head > span { display: block; margin-top: 8px; }
496
+ }
497
+ @media (max-width: 520px) {
498
+ .metrics { grid-template-columns: 1fr; }
499
+ .lede { font-size: 16px; }
500
+ .finding-head { display: block; }
501
+ .pill { margin-top: 10px; }
502
+ }
503
+ </style>
504
+ </head>
505
+ <body>
506
+ <main>
507
+ <header>
508
+ <p class="eyebrow">Generated ${htmlEscape(result.generatedAt)}</p>
509
+ <h1>Ops Catalog Audit</h1>
510
+ <p class="lede">A re-runnable audit of schedules, workflows, and prompt/template catalogs. It found ${htmlEscape(
511
+ formatMetric(result.summary.findingsTotal),
512
+ )} actionable issue cluster(s), with the highest-risk items called out first inside each group.</p>
513
+ </header>
514
+ <section class="metrics" aria-label="Audit summary">
515
+ ${metrics
516
+ .map(
517
+ ([label, value]) =>
518
+ `<div class="metric"><strong>${htmlEscape(formatMetric(value))}</strong><span>${htmlEscape(
519
+ label,
520
+ )}</span></div>`,
521
+ )
522
+ .join("")}
523
+ </section>
524
+ ${sections}
525
+ <details>
526
+ <summary>Compressed JSON appendix</summary>
527
+ <pre>${htmlEscape(JSON.stringify(result, null, 2))}</pre>
528
+ </details>
529
+ </main>
530
+ </body>
531
+ </html>`;
532
+ }
533
+
534
+ async function publishAuditPage(result: any, ctx: any): Promise<any> {
535
+ const redacted = ctx.stdlib?.Redacted;
536
+ const value = (v: any) => (redacted?.value ? redacted.value(v) : String(v));
537
+ const response = await ctx.stdlib.fetch(`${value(ctx.swarm.config.mcpBaseUrl)}/api/pages`, {
538
+ method: "POST",
539
+ headers: {
540
+ Authorization: `Bearer ${value(ctx.swarm.config.apiKey)}`,
541
+ "Content-Type": "application/json",
542
+ "X-Agent-ID": value(ctx.swarm.config.agentId),
543
+ },
544
+ body: JSON.stringify({
545
+ title: "Ops Catalog Audit",
546
+ slug: "ops-catalog-audit",
547
+ description: "Clustered audit-as-code report for schedules, workflows, and prompts/templates.",
548
+ contentType: "text/html",
549
+ authMode: "authed",
550
+ body: renderPage(result),
551
+ }),
552
+ });
553
+ const body = await response.json().catch(async () => ({ error: await response.text() }));
554
+ if (!response.ok) return { error: body?.error || `page create failed: ${response.status}` };
555
+ return {
556
+ id: body?.id ?? null,
557
+ appUrl: body?.app_url ?? null,
558
+ apiUrl: body?.api_url ?? null,
559
+ version: body?.version ?? null,
560
+ };
561
+ }
562
+
563
+ /** Audit schedules, workflows, and prompt/template catalogs by goal, with optional authed page output. */
564
+ export default async function opsCatalogAudit(args: any, ctx: any) {
565
+ const parsed = argsSchema.safeParse(args || {});
566
+ if (!parsed.success) return { error: "invalid args: " + parsed.error.message };
567
+
568
+ const now = parsed.data.nowIso ? new Date(parsed.data.nowIso) : new Date();
569
+ const nowMs = now.getTime();
570
+ const publishPage = parsed.data.publishPage !== false;
571
+ const includeSamples = parsed.data.includeSamples !== false;
572
+ const staleScheduleDays = parsed.data.staleScheduleDays || 14;
573
+ const tempScheduleNames = parsed.data.tempScheduleNames || [
574
+ "memory-gate-597",
575
+ "swarm-postdeploy-memory",
576
+ ];
577
+
578
+ const scheduleRows = await query(
579
+ ctx,
580
+ `SELECT s.*, a.name as targetAgentName, a.role as targetAgentRole,
581
+ a.description as targetAgentDescription, a.capabilities as targetAgentCapabilities,
582
+ a.provider as targetAgentProvider, a.harness_provider as targetAgentHarnessProvider
583
+ FROM scheduled_tasks s LEFT JOIN agents a ON a.id = s.targetAgentId
584
+ WHERE s.enabled = 1 ORDER BY s.name ASC`,
585
+ );
586
+ const schedules = scheduleRows.filter((r) => !r.unavailable);
587
+ const agentsById = new Map<string, any>();
588
+ for (const s of schedules) {
589
+ if (!s.targetAgentId) continue;
590
+ agentsById.set(s.targetAgentId, {
591
+ id: s.targetAgentId,
592
+ name: s.targetAgentName,
593
+ role: s.targetAgentRole,
594
+ description: s.targetAgentDescription,
595
+ capabilities: safeJson(s.targetAgentCapabilities, []),
596
+ provider: s.targetAgentProvider,
597
+ harnessProvider: s.targetAgentHarnessProvider,
598
+ });
599
+ }
600
+
601
+ const duplicateCronGroups = Object.values(
602
+ schedules.reduce((acc: any, s: any) => {
603
+ const cron = s.cronExpression || (s.intervalMs ? `interval:${s.intervalMs}` : "");
604
+ if (!cron) return acc;
605
+ const key = `${cron}::${s.timezone || "UTC"}`;
606
+ acc[key] ??= { key, cron, timezone: s.timezone || "UTC", schedules: [] };
607
+ acc[key].schedules.push({ id: s.id, name: s.name });
608
+ return acc;
609
+ }, {}),
610
+ ).filter((g: any) => g.schedules.length > 1);
611
+
612
+ const deadSchedules = schedules.filter((s: any) => {
613
+ const noNext = !s.nextRunAt && s.scheduleType === "recurring";
614
+ const stale = (daysSince(s.lastRunAt, nowMs) ?? 0) >= staleScheduleDays;
615
+ return noNext || stale || Number(s.consecutiveErrors || 0) > 0;
616
+ });
617
+
618
+ const tempSchedules = schedules.filter((s: any) => {
619
+ const haystack = `${s.name || ""}\n${s.description || ""}\n${s.taskTemplate || ""}`.toLowerCase();
620
+ const known = tempScheduleNames.some((name) => haystack.includes(name.toLowerCase()));
621
+ const dateMatches = [
622
+ ...haystack.matchAll(
623
+ /\b(?:self[- ]lift|remove|disable|until|through|expires?)\D{0,40}(\d{4}-\d{2}-\d{2})/g,
624
+ ),
625
+ ];
626
+ const expired = dateMatches.some((m) => (m[1] ? Date.parse(m[1]) <= nowMs : false));
627
+ return known || expired;
628
+ });
629
+
630
+ const routingRisks = schedules
631
+ .filter((s: any) => CODE_WORK_RE.test(`${s.name || ""}\n${s.tags || ""}\n${s.taskTemplate || ""}`))
632
+ .map((s: any) => {
633
+ const target = s.targetAgentId ? agentsById.get(s.targetAgentId) : null;
634
+ const pool = !s.targetAgentId;
635
+ const opencode = target?.provider === "opencode" || target?.harnessProvider === "opencode";
636
+ const risky = pool || !isCodeCapable(target) || opencode;
637
+ return risky
638
+ ? {
639
+ id: s.id,
640
+ name: s.name,
641
+ targetAgentId: s.targetAgentId || null,
642
+ targetAgentName: target?.name || null,
643
+ reason: pool
644
+ ? "pool-targeted code work"
645
+ : opencode
646
+ ? "opencode target for code work"
647
+ : "target is not code-capable",
648
+ action: "Pin this schedule to a code-capable worker targetAgentId before it runs again.",
649
+ }
650
+ : null;
651
+ })
652
+ .filter(Boolean);
653
+
654
+ const workflowRows = await query(
655
+ ctx,
656
+ `SELECT id, name, description, enabled, definition, triggers, input, triggerSchema, createdAt, lastUpdatedAt
657
+ FROM workflows ORDER BY name ASC`,
658
+ );
659
+ const workflows = workflowRows.filter((r) => !r.unavailable);
660
+ const enabledWorkflows = workflows.filter((w: any) => asBool(w.enabled));
661
+ const smokeEnabled = enabledWorkflows
662
+ .filter((w: any) => SMOKE_WORKFLOW_RE.test(`${w.name || ""}\n${w.description || ""}`))
663
+ .map((w: any) => ({
664
+ id: w.id,
665
+ name: w.name,
666
+ action: "Disable or delete if this was only a smoke/eval fixture.",
667
+ }));
668
+ const gateCoverageGaps = enabledWorkflows
669
+ .map((w: any) => {
670
+ const definition = safeJson(w.definition, {});
671
+ const text = `${w.name || ""}\n${w.description || ""}\n${JSON.stringify(definition)}`;
672
+ const structured =
673
+ hasStructuredOutput(definition) ||
674
+ hasStructuredOutput(safeJson(w.input, {})) ||
675
+ hasStructuredOutput(safeJson(w.triggerSchema, {}));
676
+ if (!GATE_WORKFLOW_RE.test(text) || structured) return null;
677
+ return {
678
+ id: w.id,
679
+ name: w.name,
680
+ nodeCount: nodeList(definition).length,
681
+ action: "Add outputSchema/schema coverage to litmus/eval/gate nodes so downstream checks are deterministic.",
682
+ };
683
+ })
684
+ .filter(Boolean);
685
+ const workflowTypeRows = enabledWorkflows.map((w: any) => {
686
+ const definition = safeJson(w.definition, {});
687
+ const nodes = nodeList(definition);
688
+ const nodeTypes = Array.from(new Set(nodes.map((n: any) => n.type).filter(Boolean))).sort();
689
+ const loadBearing =
690
+ !SMOKE_WORKFLOW_RE.test(`${w.name || ""}\n${w.description || ""}`) && nodes.length > 1;
691
+ return {
692
+ id: w.id,
693
+ name: w.name,
694
+ nodeCount: nodes.length,
695
+ nodeTypes,
696
+ class: loadBearing ? "load-bearing" : "fixture-or-small",
697
+ };
698
+ });
699
+
700
+ const promptRows = await query(
701
+ ctx,
702
+ `SELECT id, eventType, scope, scopeId, state, body, isDefault, version, createdBy, updatedAt
703
+ FROM prompt_templates ORDER BY eventType ASC, scope ASC`,
704
+ );
705
+ const prompts = promptRows.filter((r) => !r.unavailable);
706
+ const expectedEvents = new Set(CODE_REGISTRY_EVENTS);
707
+ const defaultEvents = new Set(prompts.filter((p: any) => asBool(p.isDefault)).map((p: any) => p.eventType));
708
+ const liveEvents = new Set(prompts.map((p: any) => p.eventType));
709
+ const missingDefaultEvents = [...expectedEvents].filter((e) => !defaultEvents.has(e)).sort();
710
+ const dbOnlyEvents = [...liveEvents].filter((e) => !expectedEvents.has(e) && !e.startsWith("test.")).sort();
711
+
712
+ const bodyGroups: Record<string, any[]> = {};
713
+ for (const p of prompts) {
714
+ const key = compactText(p.body, 500);
715
+ if (!key) continue;
716
+ bodyGroups[key] ??= [];
717
+ bodyGroups[key].push({
718
+ id: p.id,
719
+ eventType: p.eventType,
720
+ scope: p.scope,
721
+ scopeId: p.scopeId,
722
+ });
723
+ }
724
+ const duplicatePromptBodies = Object.values(bodyGroups).filter((group) => group.length > 1);
725
+ const staleUrlPrompts = prompts
726
+ .filter((p: any) => STALE_URL_RE.test(p.body || ""))
727
+ .map((p: any) => ({
728
+ id: p.id,
729
+ eventType: p.eventType,
730
+ scope: p.scope,
731
+ match: (p.body.match(STALE_URL_RE) || [])[0],
732
+ }));
733
+ const contradictoryPrompts = prompts
734
+ .filter((p: any) => CONTRADICTORY_RE.test(p.body || ""))
735
+ .map((p: any) => ({ id: p.id, eventType: p.eventType, scope: p.scope }));
736
+ const skillDuplicateRows = await query(
737
+ ctx,
738
+ `SELECT name, count(*) as count,
739
+ group_concat(scope || ':' || coalesce(ownerAgentId, 'global'), ', ') as locations
740
+ FROM skills
741
+ WHERE isEnabled = 1 AND systemDefault = 1
742
+ GROUP BY name HAVING count(*) > 1 ORDER BY count DESC, name ASC`,
743
+ );
744
+ const systemDefaultSkillDuplicates = skillDuplicateRows.filter((r) => !r.unavailable);
745
+ const skillDuplicateUnavailable = skillDuplicateRows.find((r) => r.unavailable)?.unavailable;
746
+
747
+ const findings = {
748
+ schedules: [
749
+ duplicateCronGroups.length && {
750
+ id: "schedules.duplicate-crons",
751
+ severity: severity(duplicateCronGroups.length > 2 ? 3 : 2),
752
+ summary: `${duplicateCronGroups.length} duplicate enabled cron/interval group(s).`,
753
+ action: "Confirm whether each duplicate group is intentional; consolidate or retarget redundant schedules.",
754
+ samples: sample(duplicateCronGroups, includeSamples),
755
+ },
756
+ deadSchedules.length && {
757
+ id: "schedules.dead-or-stale",
758
+ severity: severity(deadSchedules.length > 3 ? 3 : 2),
759
+ summary: `${deadSchedules.length} enabled schedule(s) look dead, stale, or erroring.`,
760
+ action: `Disable dead schedules or repair nextRunAt/cron/errors. Stale threshold: ${staleScheduleDays} days.`,
761
+ samples: sample(
762
+ deadSchedules.map((s: any) => ({
763
+ id: s.id,
764
+ name: s.name,
765
+ nextRunAt: s.nextRunAt,
766
+ lastRunAt: s.lastRunAt,
767
+ consecutiveErrors: s.consecutiveErrors,
768
+ })),
769
+ includeSamples,
770
+ ),
771
+ },
772
+ tempSchedules.length && {
773
+ id: "schedules.temporary-self-lift",
774
+ severity: "high",
775
+ summary: `${tempSchedules.length} temporary monitor schedule(s) may be past self-lift.`,
776
+ action: "Review the named temporary monitors and disable/delete the ones whose guard window has expired.",
777
+ samples: sample(
778
+ tempSchedules.map((s: any) => ({ id: s.id, name: s.name })),
779
+ includeSamples,
780
+ ),
781
+ },
782
+ routingRisks.length && {
783
+ id: "schedules.rule-13-15-routing",
784
+ severity: "critical",
785
+ summary: `${routingRisks.length} enabled code-work schedule(s) are not pinned to a code-capable worker.`,
786
+ action: "Set targetAgentId to a code-capable worker; never leave git/docker/bun/gh work on the pool.",
787
+ samples: sample(routingRisks, includeSamples),
788
+ },
789
+ ].filter(Boolean),
790
+ workflows: [
791
+ smokeEnabled.length && {
792
+ id: "workflows.enabled-fixtures",
793
+ severity: severity(smokeEnabled.length > 2 ? 3 : 2),
794
+ summary: `${smokeEnabled.length} enabled workflow(s) look like smoke/demo/one-shot fixtures.`,
795
+ action: "Disable fixture workflows unless they are explicitly load-bearing production gates.",
796
+ samples: sample(smokeEnabled, includeSamples),
797
+ },
798
+ gateCoverageGaps.length && {
799
+ id: "workflows.structured-output-gaps",
800
+ severity: "high",
801
+ summary: `${gateCoverageGaps.length} litmus/eval/gate workflow(s) lack visible structured-output schema coverage.`,
802
+ action: "Add outputSchema/schema coverage so gate outputs can be regression-checked.",
803
+ samples: sample(gateCoverageGaps, includeSamples),
804
+ },
805
+ ].filter(Boolean),
806
+ promptsTemplates: [
807
+ (missingDefaultEvents.length || dbOnlyEvents.length) && {
808
+ id: "prompts.registry-drift",
809
+ severity: severity(missingDefaultEvents.length > 0 ? 3 : 2),
810
+ summary: `${missingDefaultEvents.length} code registry event(s) missing default DB rows; ${dbOnlyEvents.length} DB event(s) absent from code registry.`,
811
+ action: "Re-seed prompt templates or remove stale DB-only templates after confirming no runtime still emits them.",
812
+ samples: sample(
813
+ [
814
+ ...missingDefaultEvents.slice(0, 5).map((eventType) => ({
815
+ kind: "missing-default",
816
+ eventType,
817
+ })),
818
+ ...dbOnlyEvents.slice(0, 5).map((eventType) => ({ kind: "db-only", eventType })),
819
+ ],
820
+ includeSamples,
821
+ 10,
822
+ ),
823
+ },
824
+ duplicatePromptBodies.length && {
825
+ id: "prompts.redundant-bodies",
826
+ severity: "medium",
827
+ summary: `${duplicatePromptBodies.length} prompt body group(s) are duplicated across templates.`,
828
+ action: "Extract shared text into a template reference or remove redundant overrides.",
829
+ samples: sample(duplicatePromptBodies, includeSamples, 3),
830
+ },
831
+ staleUrlPrompts.length && {
832
+ id: "prompts.stale-urls-hosts",
833
+ severity: "high",
834
+ summary: `${staleUrlPrompts.length} prompt template(s) contain stale/local/example hosts.`,
835
+ action: "Replace hardcoded hosts with runtime env-var guidance or current public hosts.",
836
+ samples: sample(staleUrlPrompts, includeSamples),
837
+ },
838
+ contradictoryPrompts.length && {
839
+ id: "prompts.contradictory-instructions",
840
+ severity: "medium",
841
+ summary: `${contradictoryPrompts.length} prompt template(s) contain nearby must/never style conflicts worth review.`,
842
+ action: "Tighten redundant or conflicting instruction blocks so workers do not receive mixed routing guidance.",
843
+ samples: sample(contradictoryPrompts, includeSamples),
844
+ },
845
+ (systemDefaultSkillDuplicates.length || skillDuplicateUnavailable) && {
846
+ id: "prompts.system-default-skill-duplicates",
847
+ severity: systemDefaultSkillDuplicates.length ? "high" : "low",
848
+ summary: skillDuplicateUnavailable
849
+ ? `Could not query systemDefault skill duplicates: ${skillDuplicateUnavailable}`
850
+ : `${systemDefaultSkillDuplicates.length} systemDefault skill name(s) are duplicated.`,
851
+ action: "Deduplicate system-default skills so prompt skill seeding is stable and non-redundant.",
852
+ samples: sample(systemDefaultSkillDuplicates, includeSamples),
853
+ },
854
+ ].filter(Boolean),
855
+ };
856
+
857
+ const result: any = {
858
+ generatedAt: now.toISOString(),
859
+ script: "ops-catalog-audit",
860
+ summary: {
861
+ schedulesEnabled: schedules.length,
862
+ workflowsTotal: workflows.length,
863
+ workflowsEnabled: enabledWorkflows.length,
864
+ promptTemplates: prompts.length,
865
+ findingsTotal:
866
+ findings.schedules.length + findings.workflows.length + findings.promptsTemplates.length,
867
+ },
868
+ goals: {
869
+ schedules: {
870
+ goal: "Reduce schedule cost/context waste and prevent misrouted code work.",
871
+ findingCount: findings.schedules.length,
872
+ checks: {
873
+ duplicateCronGroups: duplicateCronGroups.length,
874
+ deadOrStaleSchedules: deadSchedules.length,
875
+ temporarySelfLiftSchedules: tempSchedules.length,
876
+ routingRisks: routingRisks.length,
877
+ },
878
+ findings: findings.schedules,
879
+ },
880
+ workflows: {
881
+ goal: "Separate load-bearing workflows from fixtures and enforce deterministic gate outputs.",
882
+ findingCount: findings.workflows.length,
883
+ checks: {
884
+ enabledFixtures: smokeEnabled.length,
885
+ structuredOutputGaps: gateCoverageGaps.length,
886
+ loadBearingCount: workflowTypeRows.filter((w) => w.class === "load-bearing").length,
887
+ fixtureOrSmallCount: workflowTypeRows.filter((w) => w.class === "fixture-or-small").length,
888
+ },
889
+ workflowClasses: sample(workflowTypeRows, includeSamples, 20),
890
+ findings: findings.workflows,
891
+ },
892
+ promptsTemplates: {
893
+ goal: "Keep prompt registry, runtime defaults, host guidance, and skill seed blocks aligned.",
894
+ findingCount: findings.promptsTemplates.length,
895
+ checks: {
896
+ codeRegistryEvents: CODE_REGISTRY_EVENTS.length,
897
+ dbOnlyEvents: dbOnlyEvents.length,
898
+ missingDefaultEvents: missingDefaultEvents.length,
899
+ duplicatePromptBodyGroups: duplicatePromptBodies.length,
900
+ staleUrlPrompts: staleUrlPrompts.length,
901
+ contradictoryPrompts: contradictoryPrompts.length,
902
+ systemDefaultSkillDuplicates: systemDefaultSkillDuplicates.length,
903
+ },
904
+ findings: findings.promptsTemplates,
905
+ },
906
+ },
907
+ };
908
+
909
+ if (publishPage) result.page = await publishAuditPage(result, ctx);
910
+ return result;
911
+ }