@desplega.ai/agent-swarm 2.2.0 → 2.3.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 (107) hide show
  1. package/dist/{acp-adapter-k0vwfyvn.js → acp-adapter-v85a2bqy.js} +1 -1
  2. package/dist/{actions-w4pnb8vz.js → actions-zgcxfspj.js} +15 -15
  3. package/dist/{amp-adapter-0cmxb2t5.js → amp-adapter-jw9xfz42.js} +2 -2
  4. package/dist/{app-zneem7g9.js → app-4e1s4675.js} +3 -3
  5. package/dist/{assistant-wbzd2rnr.js → assistant-v238n9dk.js} +16 -16
  6. package/dist/{boot-reembed-4kx69j3g.js → boot-reembed-p666cw5n.js} +1 -1
  7. package/dist/{boot-reembed-db0wwgy1.js → boot-reembed-w6pm64j4.js} +1 -1
  8. package/dist/{boot-scrub-sweep-tfv4d3mm.js → boot-scrub-sweep-jcyjagn3.js} +2 -2
  9. package/dist/{boot-seeds-w2hmkxn5.js → boot-seeds-gbedmamp.js} +4 -4
  10. package/dist/{claude-adapter-5qrc2paq.js → claude-adapter-q7wspzt8.js} +2 -2
  11. package/dist/{claude-managed-setup-7ra32em4.js → claude-managed-setup-wza784fb.js} +1 -1
  12. package/dist/{claude-sdk-session-cd02gc76.js → claude-sdk-session-yf400ptr.js} +2 -2
  13. package/dist/{cli-p46xdr1m.js → cli-0rh20bp3.js} +1 -1
  14. package/dist/{cli-e5y285rg.js → cli-1v8w782f.js} +1 -1
  15. package/dist/{cli-8c1m9mhy.js → cli-30m5q8mc.js} +5 -5
  16. package/dist/{cli-xnp2vkx7.js → cli-4hmaheb6.js} +6 -6
  17. package/dist/{cli-xhgtay7w.js → cli-4s1kqrqg.js} +1 -1
  18. package/dist/{cli-2jm992rm.js → cli-5d1m1qca.js} +2 -2
  19. package/dist/{cli-mvsfbbgq.js → cli-65q70v2v.js} +3 -3
  20. package/dist/{cli-th8krqbf.js → cli-6p7ddg72.js} +2 -2
  21. package/dist/{cli-6q7pyd3k.js → cli-6vcyw6v7.js} +1 -1
  22. package/dist/{cli-bddxamg7.js → cli-8qsx817b.js} +23 -22
  23. package/dist/{cli-ztcxv5nv.js → cli-9e9kjqsj.js} +1 -1
  24. package/dist/{cli-jrk8mj0t.js → cli-a17tzmn0.js} +1 -1
  25. package/dist/{cli-raejy0mp.js → cli-d43xqzbe.js} +1 -1
  26. package/dist/{cli-tba8fxf9.js → cli-dxwh4j32.js} +11 -11
  27. package/dist/{cli-c9vrwcpb.js → cli-ea7chd26.js} +19 -19
  28. package/dist/{cli-vmbakbq0.js → cli-fhvqt2fp.js} +1 -1
  29. package/dist/{cli-z3dk189k.js → cli-fp4xbw07.js} +6 -6
  30. package/dist/{cli-xx13gqf3.js → cli-g6p7c2my.js} +1 -1
  31. package/dist/{cli-c0dpzwnx.js → cli-hn3pftj2.js} +2 -2
  32. package/dist/{cli-h23mr2s6.js → cli-hz64bcnj.js} +2 -2
  33. package/dist/{cli-dw0njd9q.js → cli-jhmjwqcg.js} +5 -5
  34. package/dist/{cli-0k9rcwfy.js → cli-ka01m5rb.js} +28 -12
  35. package/dist/{cli-jhhb2v9n.js → cli-kqypty34.js} +2 -2
  36. package/dist/{cli-hwc4a9xm.js → cli-q1812jbw.js} +4 -4
  37. package/dist/{cli-8w65vwee.js → cli-qrwp40st.js} +1 -1
  38. package/dist/{cli-4c7k75rq.js → cli-re4wkbzx.js} +54 -41
  39. package/dist/{cli-z1hh99j0.js → cli-tr24eeny.js} +2 -2
  40. package/dist/{cli-q6gw45wj.js → cli-wtg6edvd.js} +1 -1
  41. package/dist/{cli-mnry16kh.js → cli-xgn4rwyv.js} +4 -4
  42. package/dist/{cli-5ks6x36q.js → cli-y3dvz6ge.js} +3 -3
  43. package/dist/{cli-ksrphveb.js → cli-yv0vgscc.js} +1 -0
  44. package/dist/{cli-ak4c12mh.js → cli-yv3n83x8.js} +1 -1
  45. package/dist/{cli-9ac0mkg5.js → cli-zes4tsbb.js} +1 -1
  46. package/dist/cli.js +14 -14
  47. package/dist/{codex-adapter-m0fx3v3s.js → codex-adapter-nhjj2veb.js} +1 -1
  48. package/dist/{codex-session-runner-j9m2gfyy.js → codex-session-runner-6hggrjbx.js} +1 -1
  49. package/dist/{commands-ktnqwrv4.js → commands-mrw1ph2w.js} +2 -2
  50. package/dist/{controller-nex2he8v.js → controller-pt1zam5w.js} +2 -2
  51. package/dist/{controllers-connect-6yhwqv9v.js → controllers-connect-ymw5541d.js} +16 -16
  52. package/dist/{cursor-adapter-02s5ekva.js → cursor-adapter-cf4zh5x0.js} +1 -1
  53. package/dist/{db-19tw9r5s.js → db-hhpy6m4x.js} +2 -2
  54. package/dist/{db-jvr86v7f.js → db-hy80219q.js} +1 -1
  55. package/dist/{dispatcher-d3mnsg2y.js → dispatcher-6pdjtkr1.js} +14 -14
  56. package/dist/{dsh-adapter-63rvej88.js → dsh-adapter-fbx06re2.js} +1 -1
  57. package/dist/{external-channel-44cwer32.js → external-channel-j6vb6ya1.js} +2 -2
  58. package/dist/{grok-adapter-ysp907f6.js → grok-adapter-1193sprp.js} +2 -2
  59. package/dist/{handlers-t5pbabwf.js → handlers-tb3xgjnm.js} +17 -17
  60. package/dist/{heartbeat-2etacsqq.js → heartbeat-h1w1ktce.js} +17 -17
  61. package/dist/{hook-3kvmax6d.js → hook-ejja4bpe.js} +1 -1
  62. package/dist/{hook-mzxxs5n2.js → hook-skrbja18.js} +1 -1
  63. package/dist/{http-fn978m0g.js → http-mzh41mfj.js} +108 -52
  64. package/dist/{index-039x2sag.js → index-6rmnn89d.js} +40 -12
  65. package/dist/{index-w2wr89xd.js → index-epr2gnbw.js} +1 -1
  66. package/dist/{index-qyjbb7br.js → index-nyfcdpky.js} +17 -17
  67. package/dist/{index-e4tsa1h7.js → index-vjn3wbzf.js} +14 -14
  68. package/dist/{index-1r8hw4vd.js → index-xmmkyx82.js} +14 -14
  69. package/dist/{keepalive-d4nzs76x.js → keepalive-4djvbm2j.js} +3 -3
  70. package/dist/{lead-8h5n9k5f.js → lead-61kqp604.js} +23 -23
  71. package/dist/{maintenance-ea6jfw42.js → maintenance-f3xhnjsm.js} +1 -1
  72. package/dist/{oauth-refresh-sweep-3f25djq3.js → oauth-refresh-sweep-fpd6x6rw.js} +2 -2
  73. package/dist/{onboard-pvk7hdb2.js → onboard-196tjgyn.js} +2 -2
  74. package/dist/{otel-impl-qb7nhgdp.js → otel-impl-g8fh4c21.js} +1 -1
  75. package/dist/{pricing-refresh-6ehwkcmp.js → pricing-refresh-sdcf5yaz.js} +3 -3
  76. package/dist/{rbac-roles-9648nh00.js → rbac-roles-2x4g1wb5.js} +2 -2
  77. package/dist/{rbac-roles-km0f9cmm.js → rbac-roles-bsp5657v.js} +1 -1
  78. package/dist/{render-v2-xkze1wef.js → render-v2-0pyb39bb.js} +3 -3
  79. package/dist/{resume-tq0bbvt5.js → resume-a86tkj6r.js} +5 -5
  80. package/dist/{setup-rnrazxvd.js → setup-ypbzcapd.js} +1 -1
  81. package/dist/{work-objects-e75q2rqs.js → work-objects-yjp8de12.js} +17 -17
  82. package/dist/{worker-4ww4hjef.js → worker-4ptnn1ge.js} +23 -23
  83. package/openapi.json +168 -6
  84. package/package.json +2 -2
  85. package/src/be/asset-key-audit.ts +4 -13
  86. package/src/be/db.ts +57 -33
  87. package/src/be/migrations/227_inbox_item_state_last_read_at.sql +1 -0
  88. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +22 -4
  89. package/src/heartbeat/heartbeat.ts +25 -7
  90. package/src/http/assets.ts +26 -4
  91. package/src/http/inbox-state.ts +58 -8
  92. package/src/kv-reserved-namespaces.ts +1 -0
  93. package/src/providers/amp-adapter.ts +1 -1
  94. package/src/tests/asset-key-api.test.ts +128 -0
  95. package/src/tests/asset-key-invariants.test.ts +179 -3
  96. package/src/tests/asset-key-migration.test.ts +35 -0
  97. package/src/tests/docs-index-footprint.test.js +2 -2
  98. package/src/tests/heartbeat-checklist.test.ts +88 -0
  99. package/src/tests/heartbeat-jev-gate.test.ts +394 -10
  100. package/src/tests/inbox-state.test.ts +167 -0
  101. package/src/tests/kv-tool.test.ts +15 -0
  102. package/src/tests/script-connections.test.ts +2 -0
  103. package/src/types.ts +2 -0
  104. package/templates/skills/code-quality/content.md +2 -2
  105. package/templates/skills/pages/content.md +4 -0
  106. package/templates/skills/swarm-scripts/SKILL.md +6 -0
  107. package/templates/skills/swarm-scripts/content.md +6 -0
@@ -0,0 +1,167 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { createServer, type Server } from "node:http";
3
+ import {
4
+ closeDb,
5
+ createUser,
6
+ getDbClient,
7
+ initDb,
8
+ listInboxState,
9
+ upsertInboxState,
10
+ } from "../be/db";
11
+ import { handleInboxState } from "../http/inbox-state";
12
+ import { getPathSegments, parseQueryParams } from "../http/utils";
13
+ import { listenOnFreePort } from "./test-net";
14
+
15
+ const TEST_DB_PATH = "./test-inbox-state.sqlite";
16
+ const earlier = "2026-01-01T00:00:00.000Z";
17
+ const later = "2026-02-01T00:00:00.000Z";
18
+ let userId: string;
19
+ let server: Server;
20
+ let baseUrl: string;
21
+
22
+ beforeAll(async () => {
23
+ initDb(TEST_DB_PATH);
24
+ userId = (await createUser({ name: "Inbox User", email: "inbox-state@example.com" })).id;
25
+ server = createServer(async (req, res) => {
26
+ const url = req.url ?? "";
27
+ if (await handleInboxState(req, res, getPathSegments(url), parseQueryParams(url))) return;
28
+ res.writeHead(404).end();
29
+ });
30
+ baseUrl = `http://127.0.0.1:${await listenOnFreePort(server)}`;
31
+ });
32
+
33
+ afterAll(async () => {
34
+ await new Promise<void>((resolve) => server.close(() => resolve()));
35
+ closeDb();
36
+ for (const suffix of ["", "-wal", "-shm"]) {
37
+ const file = Bun.file(`${TEST_DB_PATH}${suffix}`);
38
+ if (await file.exists()) await file.delete();
39
+ }
40
+ });
41
+
42
+ async function patch(path: string, body: Record<string, unknown>) {
43
+ return fetch(`${baseUrl}/api/inbox-state${path}`, {
44
+ method: "PATCH",
45
+ headers: { "Content-Type": "application/json" },
46
+ body: JSON.stringify({ userId, ...body }),
47
+ });
48
+ }
49
+
50
+ describe("inbox read cursor", () => {
51
+ test("sets, preserves, moves back, and clears the cursor independently of sticky readAt", async () => {
52
+ const item = {
53
+ userId,
54
+ itemType: "to_read" as const,
55
+ itemId: "cursor",
56
+ status: "open" as const,
57
+ };
58
+ const initial = await upsertInboxState(item);
59
+ expect(initial.lastReadAt).toBeUndefined();
60
+ const set = await upsertInboxState({ ...item, readAt: earlier, lastReadAt: later });
61
+ expect(set.lastReadAt).toBe(later);
62
+ expect((await upsertInboxState({ ...item, readAt: later })).lastReadAt).toBe(later);
63
+ const movedBack = await upsertInboxState({ ...item, readAt: later, lastReadAt: earlier });
64
+ expect(movedBack.lastReadAt).toBe(earlier);
65
+ expect(movedBack.readAt).toBe(earlier);
66
+ const cleared = await upsertInboxState({ ...item, status: "done", lastReadAt: null });
67
+ expect(cleared.lastReadAt).toBeUndefined();
68
+ expect(cleared.readAt).toBe(earlier);
69
+ expect(cleared.status).toBe("done");
70
+ expect(
71
+ (await listInboxState({ userId })).find((row) => row.id === set.id)?.lastReadAt,
72
+ ).toBeUndefined();
73
+ });
74
+
75
+ test("single PATCH accepts a timestamp and null", async () => {
76
+ const item = { itemType: "to_read", itemId: "http-cursor", status: "open" };
77
+ const set = await patch("", { ...item, readAt: earlier, lastReadAt: later });
78
+ expect(set.status).toBe(200);
79
+ expect((await set.json()).item.lastReadAt).toBe(later);
80
+ const cleared = await patch("", { ...item, lastReadAt: null });
81
+ expect(cleared.status).toBe(200);
82
+ expect((await cleared.json()).item).toMatchObject({ readAt: earlier });
83
+ expect(
84
+ (await listInboxState({ userId })).find((row) => row.itemId === item.itemId)?.lastReadAt,
85
+ ).toBeUndefined();
86
+ });
87
+ });
88
+
89
+ describe("bulk inbox state", () => {
90
+ test("returns rows in request order and applies single-item cursor semantics", async () => {
91
+ const item = { itemType: "to_read", itemId: "bulk-cursor", status: "open" };
92
+ const response = await patch("/bulk", {
93
+ items: [
94
+ { ...item, readAt: earlier, lastReadAt: later },
95
+ { ...item, readAt: later },
96
+ { ...item, lastReadAt: earlier },
97
+ { ...item, lastReadAt: null },
98
+ { ...item, itemId: "bulk-other", status: "dismissed" },
99
+ ],
100
+ });
101
+ expect(response.status).toBe(200);
102
+ const { items } = await response.json();
103
+ expect(items.map((row: { itemId: string }) => row.itemId)).toEqual([
104
+ "bulk-cursor",
105
+ "bulk-cursor",
106
+ "bulk-cursor",
107
+ "bulk-cursor",
108
+ "bulk-other",
109
+ ]);
110
+ expect(items.map((row: { lastReadAt?: string }) => row.lastReadAt)).toEqual([
111
+ later,
112
+ later,
113
+ earlier,
114
+ undefined,
115
+ undefined,
116
+ ]);
117
+ expect(items.slice(0, 4).every((row: { readAt: string }) => row.readAt === earlier)).toBe(true);
118
+ });
119
+
120
+ test("rejects an invalid item before writing anything", async () => {
121
+ const response = await patch("/bulk", {
122
+ items: [
123
+ { itemType: "to_read", itemId: "must-not-write", status: "open" },
124
+ { itemType: "to_read", itemId: "invalid-cursor", status: "open", lastReadAt: "invalid" },
125
+ ],
126
+ });
127
+ expect(response.status).toBe(400);
128
+ expect((await listInboxState({ userId })).some((row) => row.itemId === "must-not-write")).toBe(
129
+ false,
130
+ );
131
+ });
132
+
133
+ test("rejects empty batches and more than 200 items", async () => {
134
+ for (const count of [0, 201]) {
135
+ const response = await patch("/bulk", {
136
+ items: Array.from({ length: count }, (_, i) => ({
137
+ itemType: "to_read",
138
+ itemId: `limit-${i}`,
139
+ status: "open",
140
+ })),
141
+ });
142
+ expect(response.status).toBe(400);
143
+ }
144
+ expect((await listInboxState({ userId })).some((row) => row.itemId.startsWith("limit-"))).toBe(
145
+ false,
146
+ );
147
+ });
148
+
149
+ test("rolls back earlier writes when a later upsert fails", async () => {
150
+ await getDbClient().run(`CREATE TRIGGER reject_inbox_state_test BEFORE INSERT ON inbox_item_state
151
+ WHEN NEW.itemId = 'reject-write' BEGIN SELECT RAISE(ABORT, 'test write failure'); END`);
152
+ try {
153
+ const response = await patch("/bulk", {
154
+ items: [
155
+ { itemType: "to_read", itemId: "rolled-back", status: "open" },
156
+ { itemType: "to_read", itemId: "reject-write", status: "open" },
157
+ ],
158
+ });
159
+ expect(response.status).toBe(500);
160
+ expect((await listInboxState({ userId })).some((row) => row.itemId === "rolled-back")).toBe(
161
+ false,
162
+ );
163
+ } finally {
164
+ await getDbClient().run("DROP TRIGGER reject_inbox_state_test");
165
+ }
166
+ });
167
+ });
@@ -118,6 +118,21 @@ describe("kv MCP tools", () => {
118
118
  expect(getRes.structuredContent.entry?.value).toEqual({ hello: "world" });
119
119
  });
120
120
 
121
+ test.each([
122
+ "heartbeat-checklist",
123
+ "heartbeat-checklist:reported-failures",
124
+ ])("kv-set refuses reserved heartbeat receipts even for lead (%s)", async (namespace) => {
125
+ const result = (await buildServer().set.handler(
126
+ { namespace, key: "checklist-id", value: ["forged-id"] },
127
+ meta(lead),
128
+ )) as StructuredResult<{ success: boolean; message: string }>;
129
+ expect(result.structuredContent.success).toBe(false);
130
+ expect(result.structuredContent.message).toContain("reserved for heartbeat checklist receipts");
131
+ expect(
132
+ await getDbClient().get("SELECT key FROM kv_entries WHERE namespace = ?", [namespace]),
133
+ ).toBeNull();
134
+ });
135
+
121
136
  test("kv-get returns entry=null for missing keys", async () => {
122
137
  const tools = buildServer();
123
138
  const getRes = (await tools.get.handler({ key: "nope" }, meta(agentA))) as StructuredResult<{
@@ -632,6 +632,8 @@ describe("script connections", () => {
632
632
  markMigrationApplied(database, "219_relabel_pi_context_compactions.sql");
633
633
  // 220 reads agents and swarm_config, which this migration-112-only fixture does not create.
634
634
  markMigrationApplied(database, "220_model_profiles.sql");
635
+ // 227 alters inbox_item_state, which this migration-112-only fixture does not create.
636
+ markMigrationApplied(database, "227_inbox_item_state_last_read_at.sql");
635
637
 
636
638
  const id = crypto.randomUUID();
637
639
  const now = new Date().toISOString();
package/src/types.ts CHANGED
@@ -1114,6 +1114,8 @@ export const InboxItemStateSchema = z
1114
1114
  doneAt: z.string().optional(),
1115
1115
  /** First-viewed timestamp, set once on first panel open. Independent of status. */
1116
1116
  readAt: z.string().optional(),
1117
+ /** Read cursor: latest time the user marked the item read. Clients can set, move back, or clear it. Independent of readAt and status. */
1118
+ lastReadAt: z.string().optional(),
1117
1119
  createdAt: z.string(),
1118
1120
  lastUpdatedAt: z.string(),
1119
1121
  })
@@ -43,7 +43,7 @@ The Repository Guidelines carry `allowMerge` and `mergeChecks`.
43
43
  1. Read `/review/rules.md` on the shared agent-fs drive. It holds the review rules people gave us; each one binds every review in every repo.
44
44
  2. Read the repo's `REVIEW.md` when it has one. It holds the repo-specific checks.
45
45
  3. Set the effort from the PR body's `Risk:` line. `low`: a quick pass that still covers CI, tests and the security read. `medium` or no line: the full list below. `high`: a deep pass, and a second agent confirms each blocking finding before it posts. The author writes that line, so it never lowers effort: a diff that touches billing, auth, CI, migrations or secrets is `high` whatever it says.
46
- 4. Re-review check. If you reviewed this PR before, compare `gh pr view <number> --json headRefOid --jq .headRefOid` with the `commit_id` of your last review (`gh api repos/<owner>/<repo>/pulls/<number>/reviews`). Same SHA: reuse that verdict, link it and stop. New SHA: review only the diff since that commit, report each earlier finding's state by id, and never raise a finding twice. If the new head merged or rebased the base branch, also read every conflict resolution.
46
+ 4. Re-review check. If you reviewed this PR before, compare `gh pr view <number> --json headRefOid --jq .headRefOid` with the `commit_id` of your last review (`gh api repos/<owner>/<repo>/pulls/<number>/reviews`). Same SHA: reuse that verdict, link it and stop. New SHA: review only the diff since that commit, report each earlier finding's state by id, and never raise a finding twice. Re-run each earlier repro on the new head. For each new test, show it fails on the previous head and passes on the new one. Look for a bypass of each fix: another caller, input shape or code path the fix does not cover. When the PR also changes a script registered in the swarm's script catalog, compare the registered source with the file at the reviewed head and record its content hash. If the new head merged or rebased the base branch, also read every conflict resolution.
47
47
  5. Round cap. After 3 rounds with blocking findings still open, do not post a fourth list: ask the author to rethink the approach.
48
48
 
49
49
  ### What to check
@@ -99,7 +99,7 @@ Nothing reaches GitHub without passing `review-post-gate`.
99
99
  ```
100
100
 
101
101
  On a PR the reviewing account authored, the gate returns `event: "COMMENT"`, because GitHub refuses APPROVE and REQUEST_CHANGES there. Post it as is.
102
- 4. Record the evidence (tests run, controls, "checked, fine" notes, the gate's `logKey`) in your task output and with `script-run name="pr-review-ledger-write"` (`action: "round"`). Never on the PR.
102
+ 4. Record the evidence (tests run, controls, "checked, fine" notes, the gate's `logKey`) in your task output and with `script-run name="pr-review-ledger-write" scope="global"` (`action: "round"`). Never on the PR. If the ledger write fails, keep the exact payload and the error in your task output, report it to the lead once, and finish the review. Read the PR's existing ledger rounds before any retry so a round is never written twice.
103
103
  5. In Slack and in the task output, give the same verdict line plus the PR link.
104
104
 
105
105
  If the gate cannot run, post nothing to GitHub. Keep the draft in your task output and report the gate failure (the error and the time) to the lead.
@@ -158,6 +158,10 @@ Setting these properties only on `:root` does not replace the injected body styl
158
158
 
159
159
  # Reference
160
160
 
161
+ ## QA a published page
162
+
163
+ To check a page by screenshot, open it from an origin its data endpoints allow. A page that fetches live data from a CORS-restricted endpoint falls back to its baked values when opened from any other origin (for example an internal service hostname), so a screenshot there proves layout only, never the live numbers. Check every element you changed and its neighbours at the viewport readers use.
164
+
161
165
  ## Auth Modes
162
166
 
163
167
  | Mode | URL behavior | When to use |
@@ -104,6 +104,9 @@ const { stdout, exitCode } = await ctx.sh("grep -rn TODO /afs/notes | sort | hea
104
104
 
105
105
  `launch-script-run` runs a script as a durable, journaled run with a different `ctx`: `ctx.run` (`id`, `agentId`, `args`) and `ctx.step.rawLlm(label, config)`, `ctx.step.agentTask(label, config)`, `ctx.step.swarmScript(label, config)`, plus `ctx.swarm.*`, `ctx.stdlib`, `ctx.logger`. Durable runs have no `ctx.api`, no `ctx.mcp`, no `ctx.fs`, no `ctx.sh`, and no `ctx.swarm.config`. A configured `ctx.step.agentTask` `agentId` records `human_pinned` unless `routingReason` is supplied. Call a connection or the drive from an inner script through `ctx.step.swarmScript`. See the `script-workflows` skill.
106
106
 
107
+ - Each `ctx.step.agentTask` step is a normal worker task (`taskType: "script-run-step"`). When it finishes, the lead gets the usual "Worker task completed — review needed" follow-up, even while the run is still awaiting that step. The step config has no `followUpConfig` field, so a value passed there is dropped. Let the script's own judge step be the gate and close those follow-ups with a one-line accept.
108
+ - When a crash supersedes an in-flight step and resumes it as a new task, the run does not follow the new task id: it can stay `failed` (for example `write EPIPE`) while the resumed task completes. Check the resumed task's output before you re-launch the run.
109
+
107
110
  ## Inline script pattern
108
111
 
109
112
  ```typescript
@@ -173,6 +176,9 @@ function rowsToObjects(res: any): any[] {
173
176
  - `script_run` returns an envelope (`result`, `truncated`, `durationMs`, `stdout`, `stderr`, `exitCode`). The callee payload is at `.result`. Take key names from a real response: a guessed key that is absent reads as a false zero.
174
177
  - An `argsSchema` validation error fires before the body runs and repeats on every retry. Fix the args instead of retrying.
175
178
  - Write a dedup or idempotency marker only after the step succeeds. A marker written after a failure blocks the next real attempt.
179
+ - `kv_set` returns `success: false` instead of throwing when a key has a character outside `[a-zA-Z0-9._:/-]` (for example `#` or `|` from a git ref). Check `success` after every write.
180
+ - `script_run` without `scope` runs an agent-scoped row of the same name before the global one, so a stale private copy silently wins. Pass `scope: "global"` for a shared script.
181
+ - A nested `script_run` does not overlap with the caller's own `ctx.stdlib.fetch` calls: started alongside them it added about 8 s to one measured run. Start it after the caller's own queries.
176
182
 
177
183
  Common tables: `agent_tasks`, `session_logs`, `agent_memory`, `scheduled_tasks`, `agents`. `session_logs` has no `tool_name` column. Tool names sit inside the `content` JSON column. Extract them with `instr` and `substr` in SQL, or parse the JSON in the script.
178
184
 
@@ -99,6 +99,9 @@ const { stdout, exitCode } = await ctx.sh("grep -rn TODO /afs/notes | sort | hea
99
99
 
100
100
  `launch-script-run` runs a script as a durable, journaled run with a different `ctx`: `ctx.run` (`id`, `agentId`, `args`) and `ctx.step.rawLlm(label, config)`, `ctx.step.agentTask(label, config)`, `ctx.step.swarmScript(label, config)`, plus `ctx.swarm.*`, `ctx.stdlib`, `ctx.logger`. Durable runs have no `ctx.api`, no `ctx.mcp`, no `ctx.fs`, no `ctx.sh`, and no `ctx.swarm.config`. A configured `ctx.step.agentTask` `agentId` records `human_pinned` unless `routingReason` is supplied. Call a connection or the drive from an inner script through `ctx.step.swarmScript`. See the `script-workflows` skill.
101
101
 
102
+ - Each `ctx.step.agentTask` step is a normal worker task (`taskType: "script-run-step"`). When it finishes, the lead gets the usual "Worker task completed — review needed" follow-up, even while the run is still awaiting that step. The step config has no `followUpConfig` field, so a value passed there is dropped. Let the script's own judge step be the gate and close those follow-ups with a one-line accept.
103
+ - When a crash supersedes an in-flight step and resumes it as a new task, the run does not follow the new task id: it can stay `failed` (for example `write EPIPE`) while the resumed task completes. Check the resumed task's output before you re-launch the run.
104
+
102
105
  ## Inline script pattern
103
106
 
104
107
  ```typescript
@@ -168,6 +171,9 @@ function rowsToObjects(res: any): any[] {
168
171
  - `script_run` returns an envelope (`result`, `truncated`, `durationMs`, `stdout`, `stderr`, `exitCode`). The callee payload is at `.result`. Take key names from a real response: a guessed key that is absent reads as a false zero.
169
172
  - An `argsSchema` validation error fires before the body runs and repeats on every retry. Fix the args instead of retrying.
170
173
  - Write a dedup or idempotency marker only after the step succeeds. A marker written after a failure blocks the next real attempt.
174
+ - `kv_set` returns `success: false` instead of throwing when a key has a character outside `[a-zA-Z0-9._:/-]` (for example `#` or `|` from a git ref). Check `success` after every write.
175
+ - `script_run` without `scope` runs an agent-scoped row of the same name before the global one, so a stale private copy silently wins. Pass `scope: "global"` for a shared script.
176
+ - A nested `script_run` does not overlap with the caller's own `ctx.stdlib.fetch` calls: started alongside them it added about 8 s to one measured run. Start it after the caller's own queries.
171
177
 
172
178
  Common tables: `agent_tasks`, `session_logs`, `agent_memory`, `scheduled_tasks`, `agents`. `session_logs` has no `tool_name` column. Tool names sit inside the `content` JSON column. Extract them with `instr` and `substr` in SQL, or parse the JSON in the script.
173
179