@desplega.ai/agent-swarm 1.92.0 → 1.92.2

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 (90) hide show
  1. package/README.md +1 -1
  2. package/openapi.json +276 -3
  3. package/package.json +6 -6
  4. package/plugin/skills/pages/SKILL.md +5 -2
  5. package/src/be/db.ts +416 -20
  6. package/src/be/memory/boot-reembed.ts +85 -0
  7. package/src/be/memory/constants.ts +44 -2
  8. package/src/be/memory/providers/openai-embedding.ts +15 -5
  9. package/src/be/memory/providers/sqlite-store.ts +325 -76
  10. package/src/be/memory/reranker.ts +35 -17
  11. package/src/be/memory/types.ts +43 -0
  12. package/src/be/migrations/084_script_run_journal_duration.sql +5 -0
  13. package/src/be/migrations/085_script_runs_kind.sql +9 -0
  14. package/src/be/migrations/086_pages_default_authed.sql +64 -0
  15. package/src/be/migrations/087_skill_files.sql +19 -0
  16. package/src/be/modelsdev-cache.json +5622 -2543
  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 +465 -0
  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 +34 -439
  23. package/src/be/seed-scripts/catalog/schedule-health.ts +78 -2
  24. package/src/be/seed-scripts/catalog/task-failure-audit.ts +48 -1
  25. package/src/be/seed-scripts/index.ts +32 -4
  26. package/src/be/seed-skills/index.ts +0 -7
  27. package/src/be/skill-sync.ts +91 -7
  28. package/src/commands/runner.ts +6 -2
  29. package/src/heartbeat/templates.ts +20 -16
  30. package/src/http/index.ts +50 -7
  31. package/src/http/mcp-user.ts +23 -0
  32. package/src/http/mcp.ts +58 -0
  33. package/src/http/memory.ts +62 -0
  34. package/src/http/pages.ts +1 -1
  35. package/src/http/script-runs.ts +2 -0
  36. package/src/http/scripts.ts +39 -2
  37. package/src/http/skills.ts +225 -0
  38. package/src/providers/claude-adapter.ts +56 -24
  39. package/src/script-workflows/workflow-ctx.ts +7 -3
  40. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  41. package/src/scripts-runtime/swarm-sdk.ts +13 -0
  42. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  43. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  44. package/src/server.ts +2 -0
  45. package/src/tasks/worker-follow-up.ts +12 -0
  46. package/src/tests/claude-adapter-binary.test.ts +135 -81
  47. package/src/tests/create-page-tool.test.ts +19 -2
  48. package/src/tests/heartbeat-checklist.test.ts +36 -0
  49. package/src/tests/mcp-transport-gc.test.ts +58 -0
  50. package/src/tests/memory-e2e.test.ts +6 -6
  51. package/src/tests/memory-health-endpoint.test.ts +78 -0
  52. package/src/tests/memory-rater-e2e.test.ts +4 -5
  53. package/src/tests/memory-reranker.test.ts +135 -124
  54. package/src/tests/memory-store.test.ts +221 -1
  55. package/src/tests/memory.test.ts +13 -12
  56. package/src/tests/pages-http.test.ts +20 -2
  57. package/src/tests/pages-storage.test.ts +26 -0
  58. package/src/tests/scripts-mcp-e2e.test.ts +53 -0
  59. package/src/tests/seed-scripts.test.ts +328 -3
  60. package/src/tests/skill-files-http.test.ts +171 -0
  61. package/src/tests/skill-files.test.ts +162 -0
  62. package/src/tests/skill-get-file-tool.test.ts +110 -0
  63. package/src/tests/skill-sync.test.ts +125 -6
  64. package/src/tests/task-cascade-fail.test.ts +304 -0
  65. package/src/tools/create-page.ts +2 -2
  66. package/src/tools/skills/index.ts +1 -0
  67. package/src/tools/skills/skill-get-file.ts +80 -0
  68. package/src/tools/tool-config.ts +2 -1
  69. package/src/types.ts +20 -0
  70. package/src/utils/internal-ai/complete-structured.ts +2 -2
  71. package/templates/schedules/daily-blocker-digest/content.md +68 -54
  72. package/templates/schedules/daily-compounding-reflection/content.md +4 -4
  73. package/templates/schedules/daily-hn-briefing/content.md +5 -5
  74. package/templates/schedules/daily-workflow-health-audit/content.md +6 -6
  75. package/templates/schedules/gtm-weekly-review/content.md +9 -9
  76. package/templates/schedules/weekly-dependabot-triage/content.md +24 -20
  77. package/templates/skills/agentmail-sending/content.md +6 -7
  78. package/templates/skills/desloppify/content.md +8 -9
  79. package/templates/skills/jira-interaction/content.md +25 -33
  80. package/templates/skills/kapso-whatsapp/content.md +29 -30
  81. package/templates/skills/linear-interaction/content.md +8 -9
  82. package/templates/skills/profile-corruption-escalation/content.md +44 -85
  83. package/templates/skills/sprite-cli/content.md +4 -5
  84. package/templates/skills/turso-interaction/content.md +14 -17
  85. package/templates/skills/workflow-iterate/content.md +38 -391
  86. package/templates/skills/x-api-interactions/content.md +4 -6
  87. package/templates/workflows/llm-safe-release-context/config.json +13 -0
  88. package/templates/workflows/llm-safe-release-context/content.md +69 -0
  89. package/templates/skills/scheduled-task-resilience/config.json +0 -14
  90. package/templates/skills/scheduled-task-resilience/content.md +0 -95
@@ -1,4 +1,9 @@
1
1
  import { z } from "zod";
2
+ import {
3
+ type CatalogReport,
4
+ publishCatalogReportPage,
5
+ renderCatalogReportPage,
6
+ } from "./catalog-report";
2
7
 
3
8
  export const argsSchema = z.object({
4
9
  nowIso: z.string().optional().describe("Audit clock override (default: current time)"),
@@ -22,7 +27,7 @@ export const argsSchema = z.object({
22
27
  const CODE_WORK_RE =
23
28
  /\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
29
  const CODE_AGENT_RE =
25
- /\b(code|coder|coding|implement|implementation|engineer|software|typescript|javascript|repo|github|picateclas)\b/i;
30
+ /\b(code|coder|coding|implement|implementation|engineer|software|typescript|javascript|repo|github)\b/i;
26
31
  const NON_CODE_AGENT_RE = /\b(content|reviewer|research|sales|gtm|support|ops|lead)\b/i;
27
32
  const SMOKE_WORKFLOW_RE =
28
33
  /\b(smoke|demo|litmus-smoke|one[- ]shot|validation|des-462-gate-validation|gsc-runtime-smoke)\b/i;
@@ -74,6 +79,11 @@ function compactText(value: unknown, max = 180): string {
74
79
  return asText(value).replace(/\s+/g, " ").trim().slice(0, max);
75
80
  }
76
81
 
82
+ function formatMetric(value: unknown): string {
83
+ if (typeof value === "number") return new Intl.NumberFormat("en-US").format(value);
84
+ return asText(value);
85
+ }
86
+
77
87
  function daysSince(iso: string | null | undefined, nowMs: number): number | null {
78
88
  if (!iso) return null;
79
89
  const t = Date.parse(iso);
@@ -115,449 +125,34 @@ function hasStructuredOutput(value: any): boolean {
115
125
  return /"outputSchema"|"schema"|"jsonSchema"|"structured"/i.test(JSON.stringify(value ?? {}));
116
126
  }
117
127
 
118
- function htmlEscape(value: unknown): string {
119
- return asText(value)
120
- .replace(/&/g, "&")
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 = [
128
+ export function buildReport(result: any): CatalogReport {
129
+ return {
130
+ title: "Ops Catalog Audit",
131
+ slug: "ops-catalog-audit",
132
+ description: "Clustered audit-as-code report for schedules, workflows, and prompts/templates.",
133
+ generatedAt: result.generatedAt,
134
+ lede: `A re-runnable audit of schedules, workflows, and prompt/template catalogs. It found ${formatMetric(
135
+ result.summary.findingsTotal,
136
+ )} actionable issue cluster(s), with the highest-risk items called out first inside each group.`,
137
+ metrics: [
184
138
  ["Findings", result.summary.findingsTotal],
185
139
  ["Schedules enabled", result.summary.schedulesEnabled],
186
140
  ["Workflows enabled", result.summary.workflowsEnabled],
187
141
  ["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>`;
142
+ ],
143
+ sections: ["schedules", "workflows", "promptsTemplates"].map((key) => ({
144
+ key,
145
+ goal: result.goals[key].goal,
146
+ findingCount: result.goals[key].findingCount,
147
+ checks: result.goals[key].checks,
148
+ findings: result.goals[key].findings,
149
+ })),
150
+ appendix: result,
151
+ };
532
152
  }
533
153
 
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
- };
154
+ export function renderPage(result: any): string {
155
+ return renderCatalogReportPage(buildReport(result));
561
156
  }
562
157
 
563
158
  /** Audit schedules, workflows, and prompt/template catalogs by goal, with optional authed page output. */
@@ -906,6 +501,6 @@ export default async function opsCatalogAudit(args: any, ctx: any) {
906
501
  },
907
502
  };
908
503
 
909
- if (publishPage) result.page = await publishAuditPage(result, ctx);
504
+ if (publishPage) result.page = await publishCatalogReportPage(buildReport(result), ctx);
910
505
  return result;
911
506
  }
@@ -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
@@ -11,6 +12,7 @@ export const argsSchema = z.object({
11
12
  .number()
12
13
  .optional()
13
14
  .describe("Flag schedules with failure rate above this (0-1, default 0.2)"),
15
+ publishPage: z.boolean().optional().describe("Publish an authed HTML page (default true)"),
14
16
  });
15
17
 
16
18
  /** Per-schedule health check: failure rates and flagging unhealthy schedules. */
@@ -19,6 +21,7 @@ export default async function scheduleHealth(args: any, ctx: any) {
19
21
  if (!parsed.success) return { error: "invalid args: " + parsed.error.message };
20
22
  const days = parsed.data.days || 7;
21
23
  const threshold = parsed.data.failureThreshold ?? 0.2;
24
+ const publishPage = parsed.data.publishPage !== false;
22
25
  const since = new Date(Date.now() - days * 86400000).toISOString();
23
26
 
24
27
  // Get schedules
@@ -26,7 +29,38 @@ export default async function scheduleHealth(args: any, ctx: any) {
26
29
  const schedPayload = schedRes?.data ?? schedRes;
27
30
  const schedules: any[] = schedPayload?.schedules ?? [];
28
31
 
29
- if (!schedules.length) return { days, schedules: [], flagged: [] };
32
+ if (!schedules.length) {
33
+ const result: any = { days, threshold, totalSchedules: 0, flaggedCount: 0, schedules: [], flagged: [] };
34
+ if (publishPage) {
35
+ result.page = await publishCatalogReportPage(
36
+ {
37
+ title: "Schedule Health Audit",
38
+ slug: "schedule-health",
39
+ description: "Per-schedule failure rate audit.",
40
+ generatedAt: new Date().toISOString(),
41
+ lede: "No schedules were returned for the selected window.",
42
+ metrics: [
43
+ ["Schedules", 0],
44
+ ["Flagged", 0],
45
+ ["Days", days],
46
+ ["Threshold", threshold],
47
+ ],
48
+ sections: [
49
+ {
50
+ key: "schedule-health",
51
+ goal: "Keep recurring schedules healthy and visible.",
52
+ findingCount: 0,
53
+ checks: { schedules: 0, flagged: 0 },
54
+ findings: [],
55
+ },
56
+ ],
57
+ appendix: result,
58
+ },
59
+ ctx,
60
+ );
61
+ }
62
+ return result;
63
+ }
30
64
 
31
65
  // Get recent tasks to correlate with schedules
32
66
  const taskRes: any = await ctx.swarm.task_list({ createdAfter: since, limit: 2000 });
@@ -62,7 +96,7 @@ export default async function scheduleHealth(args: any, ctx: any) {
62
96
 
63
97
  const flagged = results.filter((r: any) => r.flagged);
64
98
 
65
- return {
99
+ const result: any = {
66
100
  days,
67
101
  threshold,
68
102
  totalSchedules: schedules.length,
@@ -70,4 +104,46 @@ export default async function scheduleHealth(args: any, ctx: any) {
70
104
  schedules: results.sort((a: any, b: any) => b.failureRate - a.failureRate),
71
105
  flagged,
72
106
  };
107
+
108
+ if (publishPage) {
109
+ result.page = await publishCatalogReportPage(
110
+ {
111
+ title: "Schedule Health Audit",
112
+ slug: "schedule-health",
113
+ description: "Per-schedule failure rate audit.",
114
+ generatedAt: new Date().toISOString(),
115
+ lede: `Checked ${schedules.length} schedule(s) over ${days} day(s); ${flagged.length} exceeded the configured failure threshold.`,
116
+ metrics: [
117
+ ["Schedules", schedules.length],
118
+ ["Flagged", flagged.length],
119
+ ["Days", days],
120
+ ["Threshold", threshold],
121
+ ],
122
+ sections: [
123
+ {
124
+ key: "schedule-health",
125
+ goal: "Keep recurring schedules healthy and visible.",
126
+ findingCount: flagged.length,
127
+ checks: {
128
+ totalSchedules: schedules.length,
129
+ flaggedSchedules: flagged.length,
130
+ threshold,
131
+ scannedTasks: tasks.length,
132
+ },
133
+ findings: flagged.map((schedule: any) => ({
134
+ id: `schedule.${schedule.id}`,
135
+ severity: schedule.failureRate >= 0.5 ? "high" : "medium",
136
+ summary: `${schedule.name} failed ${schedule.failed}/${schedule.runs} recent run(s).`,
137
+ action: "Review the latest failed tasks and disable, repair, or retarget this schedule.",
138
+ samples: [schedule],
139
+ })),
140
+ },
141
+ ],
142
+ appendix: result,
143
+ },
144
+ ctx,
145
+ );
146
+ }
147
+
148
+ return result;
73
149
  }
@@ -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
  }