@desplega.ai/agent-swarm 1.123.1 → 1.124.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 (80) hide show
  1. package/README.md +2 -1
  2. package/dist/{actions-3ntn8s3f.js → actions-z3d0fk7z.js} +5 -5
  3. package/dist/{app-pej7g57x.js → app-q2rc9tdr.js} +3 -3
  4. package/dist/{assistant-pxa36zjn.js → assistant-3stjq7fz.js} +8 -8
  5. package/dist/{boot-reembed-wdwde4vr.js → boot-reembed-5867whnb.js} +3 -3
  6. package/dist/{boot-reembed-ybq960bh.js → boot-reembed-v6r1df32.js} +4 -4
  7. package/dist/{boot-scrub-logs-ndqxg6e3.js → boot-scrub-logs-8b8jegk3.js} +2 -2
  8. package/dist/{cli-xv09zc8e.js → cli-0925phzv.js} +1 -1
  9. package/dist/{cli-256kb8p5.js → cli-2yr6edr7.js} +4 -4
  10. package/dist/{cli-x2hk4m81.js → cli-3z4q09tn.js} +54 -9
  11. package/dist/{cli-7yn0n4fp.js → cli-5jn6wnd9.js} +8 -8
  12. package/dist/{cli-82eq5zf5.js → cli-92bqgbgy.js} +1 -1
  13. package/dist/{cli-r07fhvsk.js → cli-dq2ssyf0.js} +5 -5
  14. package/dist/{cli-kqgkfpz7.js → cli-gkvf8d83.js} +1 -1
  15. package/dist/{cli-7qvpf14x.js → cli-gzjsamwc.js} +1 -1
  16. package/dist/{cli-333x50bh.js → cli-h89g0qrd.js} +1 -1
  17. package/dist/{cli-g4bfr1e7.js → cli-jp4mdj9d.js} +2 -2
  18. package/dist/{cli-2e5t0yjj.js → cli-kk58tg33.js} +7 -7
  19. package/dist/{cli-mb3gz7qr.js → cli-mcez7z4p.js} +1 -1
  20. package/dist/{cli-ca32n52j.js → cli-mmk5vagg.js} +2 -2
  21. package/dist/{cli-1kv5sydm.js → cli-p0ec4vv6.js} +1 -1
  22. package/dist/{cli-zvbxe7pf.js → cli-p0zgv8pj.js} +1 -1
  23. package/dist/{cli-mntyk3cr.js → cli-perb20ma.js} +1 -1
  24. package/dist/{cli-7rk0bbfr.js → cli-qf7acvyr.js} +2 -2
  25. package/dist/{cli-1cah98w1.js → cli-qwsagsmg.js} +1 -1
  26. package/dist/{cli-pjvdm39a.js → cli-sqv6c9b8.js} +5 -5
  27. package/dist/{cli-vw0acwe7.js → cli-vm20sh8d.js} +3 -3
  28. package/dist/{cli-sj43m5z0.js → cli-wn3m6q54.js} +3 -3
  29. package/dist/{cli-hgkf9zmd.js → cli-x268say8.js} +125 -58
  30. package/dist/{cli-0pmkqg1d.js → cli-y43n987v.js} +1 -1
  31. package/dist/{cli-qrnhnhma.js → cli-ykthjw29.js} +4 -4
  32. package/dist/{cli-k3yfwe8g.js → cli-zxb72ctk.js} +2 -2
  33. package/dist/cli.js +9 -9
  34. package/dist/{commands-5p0erxye.js → commands-sda76yn8.js} +2 -2
  35. package/dist/{db-yp9e9nvs.js → db-vn9e4rqj.js} +6 -2
  36. package/dist/{handlers-3hqw1dsr.js → handlers-xfja18ac.js} +8 -8
  37. package/dist/{hook-5981d59r.js → hook-2medrp47.js} +1 -1
  38. package/dist/{http-ek9ctgth.js → http-937pyjfc.js} +49 -39
  39. package/dist/{index-33eb3q08.js → index-2bqd2q9v.js} +11 -11
  40. package/dist/{index-8j9b86c9.js → index-gw4en7jg.js} +9 -9
  41. package/dist/{index-chcty242.js → index-knqczmnm.js} +10 -10
  42. package/dist/{index-1cnvzmh4.js → index-vah60vxp.js} +8 -8
  43. package/dist/{keepalive-rrvxy1fy.js → keepalive-20rd6hxh.js} +5 -5
  44. package/dist/{lead-jvw8b413.js → lead-7w2k3a49.js} +20 -20
  45. package/dist/{maintenance-3zjnr2wa.js → maintenance-p3x918yw.js} +4 -4
  46. package/dist/{oauth-refresh-sweep-vfhvqmby.js → oauth-refresh-sweep-shm0nzm4.js} +4 -4
  47. package/dist/{onboard-kvntqz8h.js → onboard-py9dnf54.js} +2 -2
  48. package/dist/{otel-impl-61tsn4bx.js → otel-impl-7yjkfg68.js} +1 -1
  49. package/dist/{pricing-refresh-jcp6ntbk.js → pricing-refresh-3snb6370.js} +4 -4
  50. package/dist/{rbac-roles-xcbab18x.js → rbac-roles-5e72bbpk.js} +3 -3
  51. package/dist/{rbac-roles-r3j4z9vs.js → rbac-roles-gqxdp8st.js} +4 -4
  52. package/dist/{seed-pricing-bd669a7j.js → seed-pricing-bg42d7h2.js} +3 -3
  53. package/dist/{setup-pg03xran.js → setup-nzk3kbbq.js} +2 -2
  54. package/dist/{worker-q5n4qwvq.js → worker-0p1jdzkm.js} +20 -20
  55. package/openapi.json +23 -1
  56. package/package.json +1 -1
  57. package/src/be/db.ts +81 -4
  58. package/src/be/scripts/typecheck.ts +8 -4
  59. package/src/be/seed-scripts/catalog/task-context-gathering.ts +1 -1
  60. package/src/http/workflows.ts +16 -4
  61. package/src/prompts/session-templates.ts +6 -3
  62. package/src/scripts-runtime/swarm-sdk.ts +10 -0
  63. package/src/scripts-runtime/types/stdlib.d.ts +4 -0
  64. package/src/scripts-runtime/types/swarm-sdk.d.ts +4 -0
  65. package/src/telemetry.ts +10 -7
  66. package/src/tests/mcp-tools.test.ts +2 -2
  67. package/src/tests/prompt-template-session.test.ts +7 -0
  68. package/src/tests/scripts-mcp-e2e.test.ts +51 -2
  69. package/src/tests/scripts-typecheck.test.ts +17 -0
  70. package/src/tests/sdk-allowlist.test.ts +34 -1
  71. package/src/tests/seed-scripts.test.ts +43 -0
  72. package/src/tests/swarm-tool-result-gate.test.ts +35 -0
  73. package/src/tests/task-tools-ctx.test.ts +29 -0
  74. package/src/tests/telemetry-init.test.ts +21 -24
  75. package/src/tests/workflow-http-v2.test.ts +116 -0
  76. package/src/tools/get-tasks.ts +40 -1
  77. package/src/tools/script-run.ts +2 -2
  78. package/src/tools/script-upsert.ts +2 -2
  79. package/src/tools/utils.ts +14 -8
  80. package/src/tools/workflows/list-workflow-runs.ts +95 -24
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  PermissionVerbSchema
3
- } from "./cli-0pmkqg1d.js";
3
+ } from "./cli-y43n987v.js";
4
4
  import {
5
5
  getDb,
6
6
  init_db
7
- } from "./cli-x2hk4m81.js";
7
+ } from "./cli-3z4q09tn.js";
8
8
  import"./cli-z2zcxes1.js";
9
- import"./cli-zvbxe7pf.js";
9
+ import"./cli-p0zgv8pj.js";
10
10
  import"./cli-4j2mrzag.js";
11
11
  import"./cli-bw80ck94.js";
12
12
  import"./cli-xz9aq0rf.js";
@@ -7,11 +7,11 @@ import {
7
7
  getUserGrant,
8
8
  listUserRoles,
9
9
  runRbacCliCommand
10
- } from "./cli-k3yfwe8g.js";
11
- import"./cli-0pmkqg1d.js";
12
- import"./cli-x2hk4m81.js";
10
+ } from "./cli-zxb72ctk.js";
11
+ import"./cli-y43n987v.js";
12
+ import"./cli-3z4q09tn.js";
13
13
  import"./cli-z2zcxes1.js";
14
- import"./cli-zvbxe7pf.js";
14
+ import"./cli-p0zgv8pj.js";
15
15
  import"./cli-4j2mrzag.js";
16
16
  import"./cli-bw80ck94.js";
17
17
  import"./cli-xz9aq0rf.js";
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  buildModelsDevSeedRows,
3
3
  seedPricingFromModelsDev
4
- } from "./cli-7qvpf14x.js";
5
- import"./cli-x2hk4m81.js";
4
+ } from "./cli-gzjsamwc.js";
5
+ import"./cli-3z4q09tn.js";
6
6
  import"./cli-z2zcxes1.js";
7
- import"./cli-zvbxe7pf.js";
7
+ import"./cli-p0zgv8pj.js";
8
8
  import"./cli-4j2mrzag.js";
9
9
  import"./cli-bw80ck94.js";
10
10
  import"./cli-xz9aq0rf.js";
@@ -4,14 +4,14 @@ import {
4
4
  createDefaultMcpJson,
5
5
  createDefaultSettingsLocal,
6
6
  createHooksConfig
7
- } from "./cli-mntyk3cr.js";
7
+ } from "./cli-perb20ma.js";
8
8
  import {
9
9
  require_jsx_dev_runtime
10
10
  } from "./cli-wx82tw1k.js";
11
11
  import {
12
12
  getApiKey
13
13
  } from "./cli-f14fvzag.js";
14
- import"./cli-zvbxe7pf.js";
14
+ import"./cli-p0zgv8pj.js";
15
15
  import {
16
16
  Box_default,
17
17
  Spinner,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runAgent
3
- } from "./cli-qrnhnhma.js";
3
+ } from "./cli-ykthjw29.js";
4
4
  import"./cli-exfs8aym.js";
5
5
  import"./cli-jj1arm18.js";
6
6
  import"./cli-07er7wrs.js";
@@ -14,7 +14,7 @@ import"./cli-d6n0thxw.js";
14
14
  import"./cli-v1mwc5v2.js";
15
15
  import {
16
16
  getEnabledCapabilities
17
- } from "./cli-hgkf9zmd.js";
17
+ } from "./cli-x268say8.js";
18
18
  import"./cli-1z7048dy.js";
19
19
  import"./cli-gz9sbyk3.js";
20
20
  import"./cli-ftyf58xj.js";
@@ -25,36 +25,36 @@ import"./cli-tng3131n.js";
25
25
  import"./cli-zm7njwzd.js";
26
26
  import"./cli-cw70hymf.js";
27
27
  import"./cli-e61fs2hp.js";
28
- import"./cli-256kb8p5.js";
29
- import"./cli-7rk0bbfr.js";
30
- import"./cli-7qvpf14x.js";
31
- import"./cli-k3yfwe8g.js";
32
- import"./cli-0pmkqg1d.js";
33
- import"./cli-kqgkfpz7.js";
34
- import"./cli-2e5t0yjj.js";
28
+ import"./cli-2yr6edr7.js";
29
+ import"./cli-qf7acvyr.js";
30
+ import"./cli-gzjsamwc.js";
31
+ import"./cli-zxb72ctk.js";
32
+ import"./cli-y43n987v.js";
33
+ import"./cli-gkvf8d83.js";
34
+ import"./cli-kk58tg33.js";
35
35
  import"./cli-f14fvzag.js";
36
36
  import"./cli-3dpj7atn.js";
37
- import"./cli-333x50bh.js";
38
- import"./cli-sj43m5z0.js";
37
+ import"./cli-h89g0qrd.js";
38
+ import"./cli-wn3m6q54.js";
39
39
  import"./cli-jkhwmmgg.js";
40
- import"./cli-ca32n52j.js";
41
- import"./cli-1kv5sydm.js";
40
+ import"./cli-mmk5vagg.js";
41
+ import"./cli-p0ec4vv6.js";
42
42
  import"./cli-q25ejbyn.js";
43
- import"./cli-mb3gz7qr.js";
43
+ import"./cli-mcez7z4p.js";
44
44
  import"./cli-fad8m16k.js";
45
45
  import"./cli-7zd05xb4.js";
46
- import"./cli-82eq5zf5.js";
47
- import"./cli-xv09zc8e.js";
48
- import"./cli-1cah98w1.js";
49
- import"./cli-r07fhvsk.js";
46
+ import"./cli-92bqgbgy.js";
47
+ import"./cli-0925phzv.js";
48
+ import"./cli-qwsagsmg.js";
49
+ import"./cli-dq2ssyf0.js";
50
50
  import"./cli-rkndnn90.js";
51
51
  import"./cli-b0p7rfnd.js";
52
52
  import"./cli-5ncdb7ff.js";
53
53
  import"./cli-wspgs9bt.js";
54
54
  import"./cli-dh55d5fg.js";
55
- import"./cli-x2hk4m81.js";
55
+ import"./cli-3z4q09tn.js";
56
56
  import"./cli-z2zcxes1.js";
57
- import"./cli-zvbxe7pf.js";
57
+ import"./cli-p0zgv8pj.js";
58
58
  import"./cli-4j2mrzag.js";
59
59
  import"./cli-bw80ck94.js";
60
60
  import"./cli-xz9aq0rf.js";
package/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Agent Swarm API",
5
- "version": "1.123.1",
5
+ "version": "1.124.0",
6
6
  "description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
7
7
  },
8
8
  "servers": [
@@ -19728,6 +19728,28 @@
19728
19728
  "required": false,
19729
19729
  "name": "status",
19730
19730
  "in": "query"
19731
+ },
19732
+ {
19733
+ "schema": {
19734
+ "type": "integer",
19735
+ "minimum": 1,
19736
+ "maximum": 100
19737
+ },
19738
+ "required": false,
19739
+ "name": "limit",
19740
+ "in": "query"
19741
+ },
19742
+ {
19743
+ "schema": {
19744
+ "type": [
19745
+ "integer",
19746
+ "null"
19747
+ ],
19748
+ "minimum": 0
19749
+ },
19750
+ "required": false,
19751
+ "name": "offset",
19752
+ "in": "query"
19731
19753
  }
19732
19754
  ],
19733
19755
  "responses": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.123.1",
3
+ "version": "1.124.0",
4
4
  "description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
5
5
  "license": "MIT",
6
6
  "author": "desplega.sh <contact@desplega.sh>",
package/src/be/db.ts CHANGED
@@ -8679,15 +8679,92 @@ export function updateWorkflowRun(
8679
8679
  return run;
8680
8680
  }
8681
8681
 
8682
- export function listWorkflowRuns(workflowId: string): WorkflowRun[] {
8682
+ export type WorkflowRunListOptions = {
8683
+ status?: WorkflowRunStatus;
8684
+ limit?: number;
8685
+ offset?: number;
8686
+ };
8687
+
8688
+ export type WorkflowRunPage = {
8689
+ runs: WorkflowRun[];
8690
+ page: {
8691
+ limit: number;
8692
+ offset: number;
8693
+ total: number;
8694
+ hasMore: boolean;
8695
+ nextOffset?: number;
8696
+ };
8697
+ };
8698
+
8699
+ export function listWorkflowRuns(
8700
+ workflowId: string,
8701
+ options: WorkflowRunListOptions = {},
8702
+ ): WorkflowRun[] {
8703
+ const conditions = ["workflowId = ?"];
8704
+ const params: Array<string | number> = [workflowId];
8705
+ if (options.status) {
8706
+ conditions.push("status = ?");
8707
+ params.push(options.status);
8708
+ }
8709
+
8710
+ let pagination = "";
8711
+ if (options.limit !== undefined) {
8712
+ pagination = " LIMIT ? OFFSET ?";
8713
+ params.push(options.limit, options.offset ?? 0);
8714
+ } else if (options.offset !== undefined) {
8715
+ pagination = " LIMIT -1 OFFSET ?";
8716
+ params.push(options.offset);
8717
+ }
8718
+
8683
8719
  return getDb()
8684
- .prepare<WorkflowRunRow, [string]>(
8685
- "SELECT * FROM workflow_runs WHERE workflowId = ? ORDER BY startedAt DESC",
8720
+ .prepare<WorkflowRunRow, Array<string | number>>(
8721
+ `SELECT * FROM workflow_runs
8722
+ WHERE ${conditions.join(" AND ")}
8723
+ ORDER BY startedAt DESC, id DESC${pagination}`,
8686
8724
  )
8687
- .all(workflowId)
8725
+ .all(...params)
8688
8726
  .map(rowToWorkflowRun);
8689
8727
  }
8690
8728
 
8729
+ export function countWorkflowRuns(
8730
+ workflowId: string,
8731
+ options: Pick<WorkflowRunListOptions, "status"> = {},
8732
+ ): number {
8733
+ const conditions = ["workflowId = ?"];
8734
+ const params: string[] = [workflowId];
8735
+ if (options.status) {
8736
+ conditions.push("status = ?");
8737
+ params.push(options.status);
8738
+ }
8739
+ const row = getDb()
8740
+ .prepare<{ count: number }, string[]>(
8741
+ `SELECT COUNT(*) AS count FROM workflow_runs WHERE ${conditions.join(" AND ")}`,
8742
+ )
8743
+ .get(...params);
8744
+ return row?.count ?? 0;
8745
+ }
8746
+
8747
+ export function listWorkflowRunsPage(
8748
+ workflowId: string,
8749
+ options: Required<Pick<WorkflowRunListOptions, "limit" | "offset">> &
8750
+ Pick<WorkflowRunListOptions, "status">,
8751
+ ): WorkflowRunPage {
8752
+ const runs = listWorkflowRuns(workflowId, options);
8753
+ const total = countWorkflowRuns(workflowId, { status: options.status });
8754
+ const nextOffset = options.offset + runs.length;
8755
+ const hasMore = nextOffset < total;
8756
+ return {
8757
+ runs,
8758
+ page: {
8759
+ limit: options.limit,
8760
+ offset: options.offset,
8761
+ total,
8762
+ hasMore,
8763
+ ...(hasMore ? { nextOffset } : {}),
8764
+ },
8765
+ };
8766
+ }
8767
+
8691
8768
  // ============================================================================
8692
8769
  // Workflow Run Step CRUD
8693
8770
  // ============================================================================
@@ -106,7 +106,7 @@ export interface SwarmSdk {
106
106
  // --- workflows ---
107
107
  workflow_list(args?: { enabled?: boolean; includeFull?: boolean; consecutiveErrorsMin?: number; lastRunStatus?: "running" | "waiting" | "completed" | "failed" | "skipped" | "cancelled" }): Promise<unknown>;
108
108
  workflow_get(args: { id: string }): Promise<unknown>;
109
- workflow_listRuns(args: { workflowId: string; status?: "running" | "waiting" | "completed" | "failed" | "skipped" | "cancelled" }): Promise<unknown>;
109
+ workflow_listRuns(args: { workflowId: string; status?: "running" | "waiting" | "completed" | "failed" | "skipped" | "cancelled"; limit?: number; offset?: number }): Promise<unknown>;
110
110
  workflow_getRun(args: { id: string }): Promise<unknown>;
111
111
  // --- prompt templates ---
112
112
  prompt_list(args?: { eventType?: string; scope?: "global" | "agent" | "repo"; scopeId?: string; isDefault?: boolean }): Promise<unknown>;
@@ -299,6 +299,8 @@ export interface ScriptContext {
299
299
  * A swarm script's default export. \`args\` comes FIRST, \`ctx\` second — never swap them.
300
300
  *
301
301
  * @example
302
+ * import type { ScriptContext } from "swarm-sdk";
303
+ *
302
304
  * export default async function (args: { name: string }, ctx: ScriptContext) {
303
305
  * await ctx.logger.log(\`hello \${args.name}\`);
304
306
  * return { ok: true };
@@ -887,15 +889,17 @@ void _scriptMain;
887
889
 
888
890
  if (diagnostics.length === 0) return { ok: true };
889
891
 
890
- const formatted = ts.formatDiagnosticsWithColorAndContext(diagnostics, {
892
+ const formatHost: ts.FormatDiagnosticsHost = {
891
893
  getCanonicalFileName: (fileName) => fileName,
892
894
  getCurrentDirectory: () => "/virtual",
893
895
  getNewLine: () => "\n",
894
- });
896
+ };
895
897
 
896
898
  return {
897
899
  ok: false,
898
- diagnostics: formatted.split("\n\n").filter(Boolean),
900
+ diagnostics: diagnostics.map((diagnostic) =>
901
+ ts.formatDiagnosticsWithColorAndContext([diagnostic], formatHost).trimEnd(),
902
+ ),
899
903
  structured: diagnostics.map(toStructured),
900
904
  };
901
905
  }
@@ -81,7 +81,7 @@ export default async function taskContextGathering(args: any, ctx: any) {
81
81
  memories.sort((a: any, b: any) => b.compositeScore - a.compositeScore);
82
82
 
83
83
  return {
84
- task: slimTask(taskPayload?.task),
84
+ task: slimTask(taskPayload),
85
85
  requestedBy: taskPayload?.requestedBy,
86
86
  attachments: taskPayload?.attachments ?? [],
87
87
  queriesRun: queries.length,
@@ -11,6 +11,7 @@ import {
11
11
  getWorkflowVersion,
12
12
  getWorkflowVersions,
13
13
  listWorkflowRuns,
14
+ listWorkflowRunsPage,
14
15
  listWorkflows,
15
16
  updateWorkflow,
16
17
  withFavoriteFlags,
@@ -209,6 +210,8 @@ const listWorkflowRunsRoute = route({
209
210
  params: z.object({ id: z.string() }),
210
211
  query: z.object({
211
212
  status: WorkflowRunStatusSchema.optional(),
213
+ limit: z.coerce.number().int().min(1).max(100).optional(),
214
+ offset: z.coerce.number().int().min(0).optional(),
212
215
  }),
213
216
  responses: {
214
217
  200: { description: "Workflow run list" },
@@ -754,11 +757,20 @@ export async function handleWorkflows(
754
757
  if (listWorkflowRunsRoute.match(req.method, pathSegments)) {
755
758
  const parsed = await listWorkflowRunsRoute.parse(req, res, pathSegments, queryParams);
756
759
  if (!parsed) return true;
757
- let runs = listWorkflowRuns(parsed.params.id);
758
- // Apply optional status filter
759
- if (parsed.query?.status) {
760
- runs = runs.filter((r) => r.status === parsed.query.status);
760
+ const paginationRequested =
761
+ parsed.query?.limit !== undefined || parsed.query?.offset !== undefined;
762
+ if (paginationRequested) {
763
+ const page = listWorkflowRunsPage(parsed.params.id, {
764
+ status: parsed.query?.status,
765
+ limit: parsed.query?.limit ?? 20,
766
+ offset: parsed.query?.offset ?? 0,
767
+ });
768
+ json(res, page);
769
+ return true;
761
770
  }
771
+ // Preserve the pre-pagination response for the UI when limit/offset are
772
+ // omitted: a bare array containing every matching run.
773
+ const runs = listWorkflowRuns(parsed.params.id, { status: parsed.query?.status });
762
774
  json(res, runs);
763
775
  return true;
764
776
  }
@@ -450,20 +450,23 @@ registerTemplate({
450
450
  defaultBody: `
451
451
  ### Script Authoring Contract (read BEFORE writing any script)
452
452
 
453
- **Entry point — \`args\` FIRST, \`ctx\` SECOND.** A one-parameter \`function (ctx)\` still typechecks, but at runtime that parameter receives \`args\`, so every \`ctx.*\` access throws. This is the single most common cause of failed script runs.
453
+ **Entry point — \`args\` FIRST, \`ctx\` SECOND.** A one-parameter \`function (ctx)\` can execute through \`script-run\`, but at runtime that parameter receives \`args\`, so every \`ctx.*\` access throws. \`script-upsert\` also rejects an untyped parameter under strict typechecking. This is the single most common cause of failed script runs.
454
454
 
455
455
  \`\`\`ts
456
+ import type { ScriptContext } from "swarm-sdk";
456
457
  import * as z from "zod";
457
458
 
458
459
  export const argsSchema = z.object({ taskId: z.string(), limit: z.number().optional() });
459
460
 
460
- export default async function (args: z.infer<typeof argsSchema>, ctx) {
461
+ export default async function (args: z.infer<typeof argsSchema>, ctx: ScriptContext) {
461
462
  const res = await ctx.swarm.task_get({ taskId: args.taskId });
462
- const task = res?.data ?? res;
463
+ const task = ((res as { data?: unknown }).data ?? res) as { title?: string };
463
464
  return { title: task?.title };
464
465
  }
465
466
  \`\`\`
466
467
 
468
+ **Typechecking:** inline source passed to \`script-run\` executes without a compile-time typecheck. \`script-upsert\` typechecks before saving, so import \`ScriptContext\` from \`"swarm-sdk"\` as shown above to make inline code promotion-safe. Use \`script-query-types\` for the authoritative SDK and stdlib declarations.
469
+
467
470
  **What \`ctx\` actually holds for inline/named scripts (\`script-run\` / \`script-upsert\`) — nothing else:**
468
471
  - \`ctx.swarm.*\` — the swarm SDK: \`task_get\`, \`task_send\`, \`task_storeProgress\`, \`task_action\`, \`task_list\`, \`message_post\`, \`message_read\`, \`slack_reply\`, \`memory_search\`, \`kv_get\`/\`kv_set\`/\`kv_del\`/\`kv_incr\`/\`kv_list\`, \`swarm_get\`, \`agent_info\`, and more. Responses are usually wrapped — prefer \`res?.data ?? res\`.
469
472
  - \`ctx.swarm.config\` — \`apiKey\`, \`agentId\`, \`mcpBaseUrl\`, plus \`ctx.swarm.config.get("KEY")\` for user values. All are \`Redacted\` wrappers: they stringify to \`<redacted>\`, and you must never unwrap them into a return value, log line, or request body you build by hand.
@@ -270,10 +270,20 @@ function bridgeRequestFor(name: string, args: unknown): BridgeRequest | null {
270
270
  case "workflow_listRuns": {
271
271
  const wfId = typeof body.workflowId === "string" ? body.workflowId : undefined;
272
272
  if (!wfId) throw new Error("workflow_listRuns requires string `workflowId`");
273
+ const limit = body.limit === undefined ? 20 : body.limit;
274
+ const offset = body.offset === undefined ? 0 : body.offset;
275
+ if (!Number.isInteger(limit) || (limit as number) < 1 || (limit as number) > 100) {
276
+ throw new Error("workflow_listRuns `limit` must be an integer between 1 and 100");
277
+ }
278
+ if (!Number.isInteger(offset) || (offset as number) < 0) {
279
+ throw new Error("workflow_listRuns `offset` must be a non-negative integer");
280
+ }
273
281
  return {
274
282
  method: "GET",
275
283
  path: appendQuery(`/api/workflows/${encodeURIComponent(wfId)}/runs`, {
276
284
  status: body.status,
285
+ limit,
286
+ offset,
277
287
  }),
278
288
  };
279
289
  }
@@ -182,6 +182,8 @@ declare module "swarm-sdk" {
182
182
  workflow_listRuns(args: {
183
183
  workflowId: string;
184
184
  status?: "running" | "waiting" | "completed" | "failed" | "skipped" | "cancelled";
185
+ limit?: number;
186
+ offset?: number;
185
187
  }): Promise<unknown>;
186
188
  workflow_getRun(args: { id: string }): Promise<unknown>;
187
189
  // --- prompt templates ---
@@ -442,6 +444,8 @@ declare module "swarm-sdk" {
442
444
  * A swarm script's default export. `args` comes FIRST, `ctx` second — never swap them.
443
445
  *
444
446
  * @example
447
+ * import type { ScriptContext } from "swarm-sdk";
448
+ *
445
449
  * export default async function (args: { name: string }, ctx: ScriptContext) {
446
450
  * await ctx.logger.log(`hello ${args.name}`);
447
451
  * return { ok: true };
@@ -164,6 +164,8 @@ declare module "swarm-sdk" {
164
164
  workflow_listRuns(args: {
165
165
  workflowId: string;
166
166
  status?: "running" | "waiting" | "completed" | "failed" | "skipped" | "cancelled";
167
+ limit?: number;
168
+ offset?: number;
167
169
  }): Promise<unknown>;
168
170
  workflow_getRun(args: { id: string }): Promise<unknown>;
169
171
  // --- prompt templates ---
@@ -424,6 +426,8 @@ declare module "swarm-sdk" {
424
426
  * A swarm script's default export. `args` comes FIRST, `ctx` second — never swap them.
425
427
  *
426
428
  * @example
429
+ * import type { ScriptContext } from "swarm-sdk";
430
+ *
427
431
  * export default async function (args: { name: string }, ctx: ScriptContext) {
428
432
  * await ctx.logger.log(`hello ${args.name}`);
429
433
  * return { ok: true };
package/src/telemetry.ts CHANGED
@@ -218,16 +218,19 @@ export async function initTelemetry(
218
218
  const existing = await getConfig("telemetry_installation_id");
219
219
  if (existing) {
220
220
  installationId = existing;
221
- // Backfill for installs that minted an ID before `telemetry_installed_at`
222
- // existed. This reflects the upgrade date, not the true original install
223
- // date, but it's the earliest timestamp we can attribute with certainty
224
- // only api-server (generateIfMissing) backfills, same authority rule
225
- // as the installation ID itself.
221
+ // A pre-existing installation ID with no stored anchor means this
222
+ // install predates `telemetry_installed_at` tracking. Do NOT mint
223
+ // now() as a stand-in "install date" here that back-fills a date
224
+ // that's wrong by however long the install has actually existed
225
+ // (seen: 110 days on our own production install after a routine
226
+ // upgrade). Leave installedAt null so the field is omitted from the
227
+ // payload: absence unambiguously means "pre-existing install, anchor
228
+ // unknown", and consumers can fall back to min(occurred_at) per
229
+ // installation_id in ClickHouse for the real anchor. Only a
230
+ // genuinely new installationId (the branch below) mints one.
226
231
  const existingInstalledAt = await getConfig("telemetry_installed_at");
227
232
  if (existingInstalledAt) {
228
233
  installedAt = existingInstalledAt;
229
- } else if (generateIfMissing) {
230
- await tryPersistInstalledAt(setConfig);
231
234
  }
232
235
  } else if (generateIfMissing) {
233
236
  const candidateId = `install_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
@@ -48,9 +48,9 @@ describe("script MCP tools", () => {
48
48
  "script-search":
49
49
  "Semantic search over swarm-shared TypeScript scripts (catalog persisted in the agent-swarm DB; callable from agents and workflows). For ephemeral throwaway TS on your local machine, use code-mode instead.",
50
50
  "script-run":
51
- "Run a named swarm-shared script (callable across agents and from workflow `swarm-script` nodes), OR inline source (auto-saved as scratch to the catalog). Use for swarm-visible, durable scripts. For local-only throwaway TS, use code-mode `run`.",
51
+ "Run a named swarm-shared script (callable across agents and from workflow `swarm-script` nodes), OR inline source (auto-saved as scratch to the catalog). Inline source executes without the `script-upsert` compile-time typecheck. Use for swarm-visible, durable scripts. For local-only throwaway TS, use code-mode `run`.",
52
52
  "script-upsert":
53
- "Persist a TypeScript script to the swarm catalog under your agent scope (or global if you're a lead). Other agents and workflow nodes will be able to find and run it. For local-only scripts, use code-mode `save`.",
53
+ 'Typecheck and persist a TypeScript script to the swarm catalog under your agent scope (or global if you\'re a lead). Import `ScriptContext` from "swarm-sdk" for a real context type. Other agents and workflow nodes will be able to find and run it. For local-only scripts, use code-mode `save`.',
54
54
  "script-delete":
55
55
  "Remove a swarm-shared script from the catalog. Versions table preserves history.",
56
56
  "script-query-types":
@@ -220,8 +220,15 @@ describe("Session templates — individual resolution", () => {
220
220
  const result = resolveTemplate("system.agent.script_authoring_contract", {});
221
221
  expect(result.skipped).toBe(false);
222
222
  expect(result.text).toContain("`args` FIRST, `ctx` SECOND");
223
+ expect(result.text).toContain('import type { ScriptContext } from "swarm-sdk"');
223
224
  expect(result.text).toContain("export default async function (args");
225
+ expect(result.text).toContain("ctx: ScriptContext");
224
226
  expect(result.text).toContain("export const argsSchema");
227
+ expect(result.text).toContain(
228
+ "inline source passed to `script-run` executes without a compile-time typecheck",
229
+ );
230
+ expect(result.text).toContain("`script-upsert` typechecks before saving");
231
+ expect(result.text).toContain("`script-query-types`");
225
232
  expect(result.text).toContain("ctx.swarm.config");
226
233
  expect(result.text).toContain("ctx.api.<slug>");
227
234
  expect(result.text).toContain("ctx.stdlib");
@@ -9,6 +9,7 @@ import { handleCore } from "../http/core";
9
9
  import { handleScriptRuns } from "../http/script-runs";
10
10
  import { handleScripts } from "../http/scripts";
11
11
  import { getPathSegments, parseQueryParams } from "../http/utils";
12
+ import { getTemplateDefinition } from "../prompts/registry";
12
13
  import { registerScriptDeleteTool } from "../tools/script-delete";
13
14
  import { registerScriptRunTool } from "../tools/script-run";
14
15
  import { registerScriptRunsTools } from "../tools/script-runs";
@@ -16,6 +17,8 @@ import { registerScriptSearchTool } from "../tools/script-search";
16
17
  import { registerScriptUpsertTool } from "../tools/script-upsert";
17
18
  import { refreshSecretScrubberCache } from "../utils/secret-scrubber";
18
19
 
20
+ import "../prompts/session-templates";
21
+
19
22
  const TEST_DB_PATH = "./test-scripts-mcp-e2e.sqlite";
20
23
  const API_KEY = "test-scripts-mcp-key-1234567890";
21
24
 
@@ -207,6 +210,27 @@ beforeEach(() => {
207
210
  });
208
211
 
209
212
  describe("script_ MCP HTTP proxy tools", () => {
213
+ test("upserts the canonical script authoring contract example verbatim", async () => {
214
+ const tools = buildToolServer();
215
+ const contract = getTemplateDefinition("system.agent.script_authoring_contract");
216
+ const source = contract?.defaultBody.match(/```ts\n([\s\S]*?)\n```/)?.[1];
217
+ expect(source).toBeTruthy();
218
+
219
+ const upsert = (await tools.upsert.handler(
220
+ {
221
+ name: "canonical-authoring-contract",
222
+ source,
223
+ description: "Canonical authoring contract fixture",
224
+ intent: "verify documented source typechecks",
225
+ },
226
+ meta(workerId),
227
+ )) as StructuredResult<{ name: string }>;
228
+
229
+ expect(upsert.isError).toBeFalsy();
230
+ expect(upsert.structuredContent.success).toBe(true);
231
+ expect(upsert.structuredContent.data?.name).toBe("canonical-authoring-contract");
232
+ });
233
+
210
234
  test("exercise script-upsert -> script-search -> script-run -> script-delete", async () => {
211
235
  const tools = buildToolServer();
212
236
  const source = `export default async (args: { value: number }) => ({ result: args.value * 7 });`;
@@ -462,8 +486,33 @@ describe("script_ MCP HTTP proxy tools", () => {
462
486
  // summarizes ... details carries the payload the model actually needs").
463
487
  expect(bad.isError).toBe(true);
464
488
  expect(bad.structuredContent.success).toBe(false);
465
- expect(bad.structuredContent.message).toContain("Typecheck failed:");
489
+ expect(bad.structuredContent.message).toMatch(/^Typecheck failed:/);
490
+ expect(bad.structuredContent.message).not.toContain("(+1 more)");
466
491
  expect(bad.structuredContent.details).toBeTruthy();
467
- expect(bad.content[0]?.text).toContain("Typecheck failed:");
492
+ expect(bad.content[0]?.text).toContain(bad.structuredContent.message);
493
+ });
494
+
495
+ test("reports only remaining typecheck diagnostics in the summary", async () => {
496
+ const tools = buildToolServer();
497
+ const bad = (await tools.upsert.handler(
498
+ {
499
+ name: "two-type-errors",
500
+ source: `
501
+ export default async () => {
502
+ const count: number = "one";
503
+ const enabled: boolean = 1;
504
+ return { count, enabled };
505
+ };
506
+ `,
507
+ description: "Two diagnostic fixture",
508
+ intent: "verify diagnostic summary cardinality",
509
+ },
510
+ meta(workerId),
511
+ )) as StructuredResult<{ diagnostics: string[] }>;
512
+
513
+ expect(bad.isError).toBe(true);
514
+ expect(bad.structuredContent.success).toBe(false);
515
+ expect(bad.structuredContent.message).toMatch(/\(\+1 more\)$/);
516
+ expect(bad.structuredContent.message).not.toContain("(+2 more)");
468
517
  });
469
518
  });
@@ -123,6 +123,23 @@ describe("typecheckScript", () => {
123
123
  expect(d.message).toContain("number");
124
124
  });
125
125
 
126
+ test("returns exactly one formatted entry per compiler diagnostic", () => {
127
+ const result = typecheckScript(`
128
+ export default async () => {
129
+ const count: number = "one";
130
+ const enabled: boolean = 1;
131
+ return { count, enabled };
132
+ };
133
+ `);
134
+ expect(result.ok).toBe(false);
135
+ if (result.ok) return;
136
+
137
+ expect(result.structured).toHaveLength(2);
138
+ expect(result.diagnostics).toHaveLength(result.structured.length);
139
+ expect(result.diagnostics[0]).toContain("Type 'string' is not assignable to type 'number'");
140
+ expect(result.diagnostics[1]).toContain("Type 'number' is not assignable to type 'boolean'");
141
+ });
142
+
126
143
  test("captures the offending identifier on TS2304 (Cannot find name)", () => {
127
144
  const result = typecheckScript(`
128
145
  export default async () => {