@desplega.ai/agent-swarm 2.4.1 → 2.4.3

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 (153) hide show
  1. package/dist/{acp-adapter-zvzr538d.js → acp-adapter-1885dpr7.js} +4 -4
  2. package/dist/{actions-jhtqdw69.js → actions-pn76n32q.js} +18 -18
  3. package/dist/{amp-adapter-bd84rhs2.js → amp-adapter-af84rx1d.js} +3 -3
  4. package/dist/{app-kreq1gwg.js → app-6sv8mhzm.js} +3 -3
  5. package/dist/{assistant-3k22sbsq.js → assistant-sc2tf825.js} +20 -20
  6. package/dist/{boot-reembed-f6z923ny.js → boot-reembed-gvxjs2py.js} +1 -1
  7. package/dist/{boot-reembed-e53x6b9v.js → boot-reembed-y3m0ydzj.js} +1 -1
  8. package/dist/{boot-scrub-sweep-0538vsb7.js → boot-scrub-sweep-1j9mk05f.js} +3 -3
  9. package/dist/{boot-seeds-6wxqkfk2.js → boot-seeds-37sp626b.js} +4 -4
  10. package/dist/{claude-adapter-gfts5044.js → claude-adapter-rp0jtnwr.js} +4 -4
  11. package/dist/{claude-managed-setup-h00hsgtz.js → claude-managed-setup-rdt248k7.js} +1 -1
  12. package/dist/{claude-sdk-session-56besmfz.js → claude-sdk-session-53rcw5we.js} +9822 -9749
  13. package/dist/{cli-3tnpj2bh.js → cli-038fga5k.js} +2 -2
  14. package/dist/{cli-kmqgmcnb.js → cli-0dxxwfkt.js} +8 -8
  15. package/dist/{cli-471qk6s2.js → cli-1012xb7j.js} +28 -28
  16. package/dist/{cli-sv5gb0yb.js → cli-1eszr343.js} +1 -1
  17. package/dist/{cli-7225gwj7.js → cli-1m17dc9d.js} +1 -1
  18. package/dist/{cli-xftrvxag.js → cli-1ma335fz.js} +3 -3
  19. package/dist/{cli-s48yvzb8.js → cli-1n9gdg56.js} +4 -4
  20. package/dist/{cli-ash6xxk7.js → cli-1xsraagq.js} +3 -3
  21. package/dist/{cli-xbzvet96.js → cli-36ckw0wx.js} +5 -4
  22. package/dist/{cli-mhywgrrz.js → cli-4sydckyj.js} +1 -1
  23. package/dist/{cli-3r448dqn.js → cli-4yef8ghv.js} +3 -3
  24. package/dist/{cli-g0hgtz4p.js → cli-58vwn100.js} +3 -3
  25. package/dist/{cli-pw7v69vp.js → cli-625tb7aq.js} +1 -1
  26. package/dist/{cli-m2qgh5sv.js → cli-6aen3ezb.js} +1 -1
  27. package/dist/{cli-z071fb95.js → cli-6eyncg0y.js} +6 -6
  28. package/dist/{cli-0bh477zn.js → cli-73ezfhha.js} +120 -12
  29. package/dist/{cli-x5mq1fyq.js → cli-74c1wpm0.js} +3 -3
  30. package/dist/{cli-b1dfr1m4.js → cli-7a6p4e65.js} +1 -1
  31. package/dist/{cli-fpmsqq59.js → cli-836ehxmt.js} +1 -1
  32. package/dist/{cli-1pa3rz69.js → cli-8vverzf7.js} +1 -1
  33. package/dist/{cli-rdq7tzwm.js → cli-8y99kfey.js} +11 -11
  34. package/dist/{cli-z1ew8vbq.js → cli-aa3tb29r.js} +10 -5
  35. package/dist/{cli-6ncjvx55.js → cli-aypbg9r2.js} +1 -1
  36. package/dist/{cli-3kh4kpqy.js → cli-bxrqvpg5.js} +6 -6
  37. package/dist/{cli-0hyhdkdk.js → cli-cebvbc9t.js} +4 -1
  38. package/dist/{cli-p6argxh9.js → cli-dm8p2dpf.js} +7 -7
  39. package/dist/{cli-jh0qcrga.js → cli-e4d78xsv.js} +1 -1
  40. package/dist/{cli-7a91w22y.js → cli-e8xyjvfc.js} +1 -1
  41. package/dist/{cli-axh29tyr.js → cli-f0p0f6ja.js} +10 -10
  42. package/dist/{cli-w9g4sz81.js → cli-gf2850q7.js} +1 -1
  43. package/dist/{cli-bn5kevnf.js → cli-gx0j2khs.js} +4 -4
  44. package/dist/{cli-gf0kfg81.js → cli-h0mgxann.js} +5 -5
  45. package/dist/{cli-kr2pmfqx.js → cli-kgnkyabk.js} +85 -71
  46. package/dist/{cli-hqhzg1qq.js → cli-mzkwkbdy.js} +1 -1
  47. package/dist/{cli-k9z4zd4p.js → cli-npfe9t1s.js} +2 -2
  48. package/dist/{cli-syhq378v.js → cli-p0qerd2r.js} +1 -1
  49. package/dist/{cli-sjyetj21.js → cli-pygdsgh8.js} +1 -1
  50. package/dist/{cli-2wag8973.js → cli-qeaq3j6t.js} +1 -1
  51. package/dist/{cli-nx462e4v.js → cli-qp2m4btv.js} +3 -3
  52. package/dist/{cli-tbhwrxpt.js → cli-s9rr4gr2.js} +5 -5
  53. package/dist/{cli-fz9kewbh.js → cli-sds5zzs8.js} +1 -1
  54. package/dist/{cli-vy0fay6k.js → cli-tkf4x6ha.js} +15 -15
  55. package/dist/{cli-mfe7f7b5.js → cli-v87dr6e3.js} +2 -2
  56. package/dist/{cli-rqqsfz30.js → cli-vcgscaan.js} +91 -21
  57. package/dist/{cli-zfg9enh9.js → cli-vfha21c9.js} +6 -1
  58. package/dist/{cli-3p8fqcnh.js → cli-vq9gqarq.js} +1 -1
  59. package/dist/{cli-a4gvwttf.js → cli-wk28hzqn.js} +2 -2
  60. package/dist/{cli-p0rakbrk.js → cli-zvbb4n0e.js} +6 -5
  61. package/dist/cli.js +17 -17
  62. package/dist/{codex-adapter-p6rmm7vv.js → codex-adapter-2zrmab9b.js} +2 -2
  63. package/dist/{codex-hook-x7nejyrr.js → codex-hook-e2he7aca.js} +2 -2
  64. package/dist/{codex-session-runner-8vd5t9t0.js → codex-session-runner-kbkz62vc.js} +2 -2
  65. package/dist/{commands-7ems2p13.js → commands-hd574th6.js} +3 -3
  66. package/dist/{controller-m263fg43.js → controller-pz5mcfg4.js} +3 -3
  67. package/dist/{controllers-connect-bg33ae53.js → controllers-connect-5rgq2hfk.js} +19 -19
  68. package/dist/{cursor-adapter-cz6nm6mj.js → cursor-adapter-mgaprq0j.js} +2 -2
  69. package/dist/{db-w505dwa8.js → db-6nf1207q.js} +6 -2
  70. package/dist/{db-ys2rbdft.js → db-h06dxp05.js} +3 -3
  71. package/dist/{dispatcher-0s0mckqr.js → dispatcher-zthpy4m1.js} +17 -17
  72. package/dist/{dsh-adapter-3n3net33.js → dsh-adapter-64dhw0j7.js} +2 -2
  73. package/dist/{e2b-thdcxtjd.js → e2b-16d6ch39.js} +1 -1
  74. package/dist/{e2b-7am6tmq6.js → e2b-zn0b4w5w.js} +1 -1
  75. package/dist/{external-channel-5b23fv8z.js → external-channel-x01t7ke6.js} +2 -2
  76. package/dist/{grok-adapter-q9fghe5q.js → grok-adapter-h21jv594.js} +5 -5
  77. package/dist/handlers-2qjw434w.js +39 -0
  78. package/dist/{heartbeat-tww6yx1w.js → heartbeat-zzfksckz.js} +20 -20
  79. package/dist/{hook-pvmh9b6m.js → hook-3xfqhh2s.js} +2 -2
  80. package/dist/{hook-dx0pkvar.js → hook-z4zhzj1f.js} +10 -5
  81. package/dist/{http-k2ejbt05.js → http-q813tmya.js} +121 -75
  82. package/dist/{index-2z710v4x.js → index-220tk9bw.js} +9 -9
  83. package/dist/index-d0g18bax.js +41 -0
  84. package/dist/{index-dgz61nff.js → index-m0a4ydqj.js} +17 -17
  85. package/dist/{index-h2ek08d6.js → index-wbbs14c5.js} +17 -17
  86. package/dist/{index-x5aktmp8.js → index-x8xbyn1t.js} +1 -1
  87. package/dist/{keepalive-nhvpw10z.js → keepalive-y5t1my7m.js} +3 -3
  88. package/dist/{lead-jb6sf69z.js → lead-2xwtq66b.js} +30 -30
  89. package/dist/{maintenance-agakc519.js → maintenance-ybfsaedv.js} +1 -1
  90. package/dist/{oauth-refresh-sweep-4dcbhtn0.js → oauth-refresh-sweep-909jmn12.js} +2 -2
  91. package/dist/{onboard-4n2kwa4v.js → onboard-xn6fkkys.js} +3 -3
  92. package/dist/{opencode-adapter-3s7ax02e.js → opencode-adapter-x4tqzhkh.js} +3 -3
  93. package/dist/{otel-impl-tfa12080.js → otel-impl-e6mn0z13.js} +1 -1
  94. package/dist/{pi-mono-adapter-ytp7gz8b.js → pi-mono-adapter-mcfkn9vf.js} +3 -3
  95. package/dist/{pricing-refresh-3cqx3f3x.js → pricing-refresh-ygd362ep.js} +3 -3
  96. package/dist/{rbac-roles-9fae5rnv.js → rbac-roles-c9axvfnz.js} +3 -3
  97. package/dist/{rbac-roles-y6zbfzve.js → rbac-roles-mb470p4e.js} +2 -2
  98. package/dist/{render-v2-emawjvc4.js → render-v2-g0306nh1.js} +3 -3
  99. package/dist/{resume-zhgts7h7.js → resume-v315hnq6.js} +5 -5
  100. package/dist/{session-access-cm823e06.js → session-access-2zg285a1.js} +4 -4
  101. package/dist/{setup-tkp99w1e.js → setup-a4mtf02k.js} +1 -1
  102. package/dist/{work-objects-f722h263.js → work-objects-9fr0fgtn.js} +21 -21
  103. package/dist/{worker-p7f336rq.js → worker-qae22jeb.js} +30 -30
  104. package/openapi.json +393 -1
  105. package/package.json +3 -3
  106. package/src/apps/catalog.generated.json +3 -2
  107. package/src/apps/definition.ts +152 -4
  108. package/src/apps/row-store.ts +19 -7
  109. package/src/apps/schema-migrate.ts +14 -6
  110. package/src/apps/script-types.ts +1 -0
  111. package/src/apps/sync.ts +20 -19
  112. package/src/be/db/tasks/read.ts +112 -0
  113. package/src/be/db.ts +9 -4
  114. package/src/be/migrations/231_slack_tasks_last_updated_index.sql +5 -0
  115. package/src/be/swarm-config-guard.ts +15 -0
  116. package/src/github/handlers.ts +6 -5
  117. package/src/hooks/hook.ts +2 -1
  118. package/src/http/apps.ts +15 -2
  119. package/src/http/status.ts +49 -43
  120. package/src/http/tasks.ts +47 -0
  121. package/src/providers/acp-targets.ts +1 -1
  122. package/src/providers/amp-adapter.ts +1 -1
  123. package/src/providers/codex-adapter.ts +4 -4
  124. package/src/providers/opencode-adapter.ts +2 -2
  125. package/src/providers/pi-mono-extension.ts +2 -1
  126. package/src/telemetry.ts +15 -3
  127. package/src/tests/app-row-tools.test.ts +25 -0
  128. package/src/tests/apps-json-column.test.ts +611 -0
  129. package/src/tests/apps-script-types.test.ts +2 -0
  130. package/src/tests/apps-sync-engine.test.ts +42 -0
  131. package/src/tests/claude-stop-hook.test.ts +47 -0
  132. package/src/tests/codex-adapter.test.ts +42 -0
  133. package/src/tests/db-task-read-contract.test.ts +4 -0
  134. package/src/tests/db-task-write-contract.test.ts +2 -0
  135. package/src/tests/docs-index-footprint.test.js +2 -2
  136. package/src/tests/env-flag.test.ts +15 -1
  137. package/src/tests/github-handlers-cancel-config.test.ts +29 -8
  138. package/src/tests/github-handlers-inline-comments.test.ts +9 -2
  139. package/src/tests/runner-harness-telemetry-env.test.ts +6 -0
  140. package/src/tests/script-connections.test.ts +2 -0
  141. package/src/tests/skill-script-refs.test.ts +63 -0
  142. package/src/tests/slack-render-v2-query-plans.test.ts +147 -0
  143. package/src/tests/status.test.ts +158 -9
  144. package/src/tests/task-lineage.test.ts +180 -0
  145. package/src/tests/telemetry-init.test.ts +62 -5
  146. package/src/tests/user-token-rest-auth.test.ts +38 -0
  147. package/src/tools/app-rows.ts +5 -0
  148. package/src/utils/env-flag.ts +9 -0
  149. package/templates/skills/apps/SKILL.md +4 -2
  150. package/templates/skills/apps/content.md +4 -2
  151. package/templates/skills/code-quality/content.md +9 -14
  152. package/dist/handlers-mqrym347.js +0 -39
  153. package/dist/index-pqfwh9nn.js +0 -41
package/src/http/apps.ts CHANGED
@@ -8,10 +8,10 @@ import {
8
8
  type AppQueryDef,
9
9
  type AppValidationIssue,
10
10
  applyAppDefinitionPatch,
11
- ColumnKindSchema,
12
11
  isIso8601Date,
13
12
  type ModelDef,
14
13
  parseAppDefinition,
14
+ ScalarColumnKindSchema,
15
15
  SORTABLE_SYSTEM_DATE_COLUMNS,
16
16
  SYSTEM_COLUMN_KINDS,
17
17
  scriptCompatWarnings,
@@ -216,7 +216,7 @@ const UserConfigValuesSchema = z.record(
216
216
  * on the wire anyway — the key can only ever be absent.
217
217
  */
218
218
  const AppUserConfigFieldSchema = z.object({
219
- kind: ColumnKindSchema,
219
+ kind: ScalarColumnKindSchema,
220
220
  default: z.union([z.string(), z.number(), z.boolean()]).optional(),
221
221
  enum: z.array(z.string()).optional(),
222
222
  label: z.string().optional(),
@@ -785,6 +785,7 @@ async function resolveModel(
785
785
  }
786
786
 
787
787
  function parseFilterValue(raw: string, column: ModelDef["columns"][string]): unknown {
788
+ if (column.kind === "json") throw new Error("json columns cannot be filtered");
788
789
  if (column.kind === "number") {
789
790
  if (!DECIMAL_NUMBER_PATTERN.test(raw)) throw new Error("must be a decimal number");
790
791
  const value = Number(raw);
@@ -1017,6 +1018,15 @@ export async function selectAppRows(
1017
1018
  limit = 200,
1018
1019
  systemColumns = false,
1019
1020
  ) {
1021
+ // Backstop for every caller: the REST route (`filter.<col>`) and the MCP
1022
+ // tools (`equals.<col>`) reject json filters with their own paths first.
1023
+ const jsonFilterIssues = filters.flatMap((filter) =>
1024
+ Object.hasOwn(model.columns, filter.column) && model.columns[filter.column]!.kind === "json"
1025
+ ? [{ path: `filter.${filter.column}`, message: "json columns cannot be filtered" }]
1026
+ : [],
1027
+ );
1028
+ if (jsonFilterIssues.length > 0) throw new AppRowValidationError(jsonFilterIssues);
1029
+
1020
1030
  // SQL narrows the scan; the `===` re-check keeps the exact filter semantics.
1021
1031
  const rows = (
1022
1032
  await listAppRows(
@@ -1046,6 +1056,9 @@ export async function selectAppRows(
1046
1056
  { path: "sort", message: "must be <column>:<asc|desc> for a known column" },
1047
1057
  ]);
1048
1058
  }
1059
+ if (model.columns[column]?.kind === "json") {
1060
+ throw new AppRowValidationError([{ path: "sort", message: "json columns cannot be sorted" }]);
1061
+ }
1049
1062
  rows.sort((a, b) => {
1050
1063
  const result = compareValues(
1051
1064
  rowValue(a, column),
@@ -197,7 +197,7 @@ export type AutomationStatus = z.infer<typeof AutomationStatusSchema>;
197
197
  * - `broken` — harness or workers blocking (creds missing, no live workers ever).
198
198
  * - `degraded` — at least one optional integration `unverified`/`configured`
199
199
  * while another is configured, OR harness creds present but
200
- * never tested (`configured`).
200
+ * the latest live test failed or never ran (`configured`).
201
201
  * - `ok` — harness + workers `verified`; no integration left in
202
202
  * `configured` state.
203
203
  */
@@ -237,12 +237,12 @@ export const TestConnectionResponseSchema = z.object({
237
237
  /**
238
238
  * Read all worker reports for a given harness provider and reduce them to a
239
239
  * single milestone-grade rollup. Reports are joined from `agents.cred_status`
240
- * (migration 055) — no provider-adapter code runs here.
240
+ * (migration 055) — no provider-adapter code runs here. Offline agents are
241
+ * excluded: `workers` and `reports` count online agents only.
241
242
  *
242
- * `verified` ⇐ at least one worker has `liveTest.ok === true` and the test
243
- * is fresher than `getCredVerifyTtlMs()`.
243
+ * `verified` ⇐ the provider's most recent live test passed, whatever its age.
244
244
  * `configured` ⇐ at least one worker has `ready: true` (presence check
245
- * passed) but no fresh passing live test.
245
+ * passed) but the latest live test failed or none ever ran.
246
246
  * `unverified` ⇐ no worker reported, or all reporting workers have
247
247
  * `ready: false`.
248
248
  */
@@ -258,14 +258,6 @@ export interface CredRollup {
258
258
  oldestReportAgeMs: number | null;
259
259
  }
260
260
 
261
- function getCredVerifyTtlMs(): number {
262
- const raw = process.env.SWARM_VERIFY_TTL_MS;
263
- if (!raw) return 3_600_000; // 1h default — matches pre-refactor behavior.
264
- const parsed = Number.parseInt(raw, 10);
265
- if (Number.isNaN(parsed) || parsed <= 0) return 3_600_000;
266
- return parsed;
267
- }
268
-
269
261
  /**
270
262
  * Compatibility stub. The pre-refactor in-memory test-connection cache is
271
263
  * gone — credential state now lives on agent rows. Tests still call this
@@ -276,7 +268,11 @@ export function _resetTestConnectionCache(): void {
276
268
  }
277
269
 
278
270
  export async function rollupCredStatusForProvider(provider: string): Promise<CredRollup> {
279
- const agents = await listAgentsWithCredStatusByProvider(provider);
271
+ // An offline agent keeps its last cred_status after `/close`, so only online
272
+ // agents decide the rollup; a stale failure must not override a live pass.
273
+ const agents = (await listAgentsWithCredStatusByProvider(provider)).filter(
274
+ (a) => a.status !== "offline",
275
+ );
280
276
  const reports = agents.map((a) => a.credStatus).filter((s): s is AgentCredStatus => s != null);
281
277
 
282
278
  if (reports.length === 0) {
@@ -291,23 +287,19 @@ export async function rollupCredStatusForProvider(provider: string): Promise<Cre
291
287
  };
292
288
  }
293
289
 
294
- // Pick the most-recent passing live test, if any, then fall back to
295
- // most-recent live test of any kind.
296
- const ttl = getCredVerifyTtlMs();
290
+ // The provider's latest live test decides, whatever its age. Workers run the
291
+ // test at boot and re-run it only when credentials change, so an old pass is
292
+ // the normal state of a long-running worker, not a missing check.
297
293
  const now = Date.now();
298
- const isFreshPassingReport = (report: AgentCredStatus): boolean =>
299
- report.liveTest?.ok === true && now - (report.liveTest?.testedAt ?? 0) < ttl;
300
- const passing = reports
301
- .filter(isFreshPassingReport)
302
- .sort((a, b) => (b.liveTest?.testedAt ?? 0) - (a.liveTest?.testedAt ?? 0));
294
+ const passing = reports.filter((r) => r.liveTest?.ok === true);
303
295
  const anyLive = reports
304
296
  .filter((r) => r.liveTest != null)
305
297
  .sort((a, b) => (b.liveTest?.testedAt ?? 0) - (a.liveTest?.testedAt ?? 0));
306
- const latestLiveTest = passing[0]?.liveTest ?? anyLive[0]?.liveTest ?? null;
298
+ const latestLiveTest = anyLive[0]?.liveTest ?? null;
307
299
 
308
300
  const anyReady = reports.some((r) => r.ready);
309
301
  const state: CredRollupState =
310
- passing.length > 0 ? "verified" : anyReady ? "configured" : "unverified";
302
+ latestLiveTest?.ok === true ? "verified" : anyReady ? "configured" : "unverified";
311
303
 
312
304
  // For UI hints, pick the most-recent missing[] from a not-ready report.
313
305
  const latestNotReady = reports
@@ -349,23 +341,19 @@ function buildIdentity(): StatusIdentity {
349
341
  // ─── Setup milestones ────────────────────────────────────────────────────────
350
342
 
351
343
  /**
352
- * Compose a one-line, per-provider description for the milestone hint.
353
- * Examples:
354
- * "1 worker · live test ok"
355
- * "2 workers · presence ok, awaiting live test"
356
- * "missing: OPENAI_API_KEY"
344
+ * One short phrase for a provider that needs attention, e.g. "live test
345
+ * failed" or "missing: OPENAI_API_KEY". A verified provider gets none.
357
346
  */
358
- function describeRoll(roll: CredRollup): string {
347
+ function describeRoll(roll: CredRollup): string | null {
348
+ if (roll.state === "verified") return null;
359
349
  if (roll.workers === 0) return "no workers";
360
- if (roll.reports === 0) {
361
- return `${roll.workers} ${roll.workers === 1 ? "worker" : "workers"}, none reported`;
350
+ if (roll.reports === 0) return "no credential report";
351
+ if (roll.state === "configured") {
352
+ return roll.latestLiveTest?.ok === false ? "live test failed" : "not live-tested";
362
353
  }
363
- const w = `${roll.workers} ${roll.workers === 1 ? "worker" : "workers"}`;
364
- if (roll.state === "verified") return `${w} · live test ok`;
365
- if (roll.state === "configured") return `${w} · presence ok, awaiting live test`;
366
354
  return roll.latestMissing.length > 0
367
355
  ? `missing: ${roll.latestMissing.join(", ")}`
368
- : "creds blocked";
356
+ : "credentials blocked";
369
357
  }
370
358
 
371
359
  /**
@@ -375,7 +363,7 @@ function describeRoll(roll: CredRollup): string {
375
363
  * workers via migration 054/055), runs the cred-status rollup per provider,
376
364
  * and aggregates:
377
365
  *
378
- * `verified` — every provider in the fleet has a fresh passing live test.
366
+ * `verified` — every provider's latest live test passed.
379
367
  * `configured` — every provider has at least `configured`, ≥1 not verified.
380
368
  * `unverified` — any provider has `unverified` (no reports OR all-blocked).
381
369
  *
@@ -398,13 +386,25 @@ async function harnessMilestone(): Promise<SetupMilestone> {
398
386
  }
399
387
 
400
388
  const perProvider: { provider: z.infer<typeof ProviderNameSchema>; roll: CredRollup }[] = [];
389
+ let recognised = 0;
401
390
  for (const { provider } of fleet) {
402
391
  const parsed = ProviderNameSchema.safeParse(provider);
403
392
  if (!parsed.success) continue;
404
- perProvider.push({
405
- provider: parsed.data,
406
- roll: await rollupCredStatusForProvider(parsed.data),
407
- });
393
+ recognised++;
394
+ const roll = await rollupCredStatusForProvider(parsed.data);
395
+ // A provider whose agents are all offline has nothing live to judge.
396
+ if (roll.workers === 0) continue;
397
+ perProvider.push({ provider: parsed.data, roll });
398
+ }
399
+
400
+ if (recognised > 0 && perProvider.length === 0) {
401
+ return {
402
+ id: "harness",
403
+ label: "Harness configured",
404
+ state: "unverified",
405
+ hint: "No agents online. Start a worker to run a live credential test.",
406
+ action_url: "/agents",
407
+ };
408
408
  }
409
409
 
410
410
  if (perProvider.length === 0) {
@@ -424,7 +424,13 @@ async function harnessMilestone(): Promise<SetupMilestone> {
424
424
  ? "configured"
425
425
  : "unverified";
426
426
 
427
- const hint = perProvider.map((p) => `${p.provider}: ${describeRoll(p.roll)}`).join(" · ");
427
+ const hint =
428
+ perProvider
429
+ .flatMap((p) => {
430
+ const phrase = describeRoll(p.roll);
431
+ return phrase ? [`${p.provider}: ${phrase}`] : [];
432
+ })
433
+ .join(" · ") || undefined;
428
434
  const singleProvider = perProvider.length === 1 ? perProvider[0]?.provider : undefined;
429
435
 
430
436
  return {
@@ -848,7 +854,7 @@ export async function handleStatus(
848
854
  if (roll.workers === 0) {
849
855
  json(res, {
850
856
  ok: false,
851
- error: `No workers registered with HARNESS_PROVIDER=${provider}. Start a worker — its boot loop will run a live test and report it here.`,
857
+ error: `No online workers with HARNESS_PROVIDER=${provider}. Start a worker — its boot loop will run a live test and report it here.`,
852
858
  latency_ms: 0,
853
859
  });
854
860
  return true;
package/src/http/tasks.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  getSteeringMessagesForTask,
21
21
  getTaskAttachments,
22
22
  getTaskById,
23
+ getTaskLineage,
23
24
  getTasksCount,
24
25
  markSteeringDelivered,
25
26
  markSteeringHandled,
@@ -29,6 +30,7 @@ import {
29
30
  resumeTask,
30
31
  settleSupersededTaskDependents,
31
32
  supersedeTask,
33
+ TASK_LINEAGE_LIMITS,
32
34
  updateAgentStatusFromCapacity,
33
35
  updateTaskClaudeSessionId,
34
36
  updateTaskProgress,
@@ -578,6 +580,33 @@ const getTask = route({
578
580
  },
579
581
  });
580
582
 
583
+ const getTaskLineageRoute = route({
584
+ method: "get",
585
+ path: "/api/tasks/{id}/lineage",
586
+ pattern: ["api", "tasks", null, "lineage"],
587
+ session: { taskParam: "id" },
588
+ summary: "Get the tree a task belongs to (ancestors and descendants)",
589
+ description: `Follows \`parentTaskId\` up to the topmost ancestor, then returns every descendant of that root as slim task rows, the task's own subtree first. Bounded: at most ${TASK_LINEAGE_LIMITS.maxAncestors} hops up, \`maxDepth\` levels down and about twice \`maxNodes\` rows; \`truncated\` is true when any task in the tree is missing from the response.`,
590
+ tags: ["Tasks"],
591
+ params: z.object({ id: z.string() }),
592
+ query: z.object({
593
+ maxNodes: z.coerce.number().int().min(1).max(TASK_LINEAGE_LIMITS.maxNodes).optional(),
594
+ maxDepth: z.coerce.number().int().min(1).max(TASK_LINEAGE_LIMITS.maxDepth).optional(),
595
+ }),
596
+ responses: {
597
+ 200: {
598
+ description: "The lineage tree as a flat list linked by `parentTaskId`",
599
+ schema: z.object({
600
+ rootTaskId: z.string(),
601
+ tasks: z.array(AgentTaskSummarySchema.extend({ workflowRunId: z.string().optional() })),
602
+ truncated: z.boolean(),
603
+ }),
604
+ },
605
+ 403: { description: "A user caller cannot read the task's session" },
606
+ 404: { description: "Task not found" },
607
+ },
608
+ });
609
+
581
610
  const updateTaskProgressRoute = route({
582
611
  method: "post",
583
612
  path: "/api/tasks/{id}/progress",
@@ -1121,6 +1150,8 @@ export async function handleTasks(
1121
1150
  offeredTo: parsed.body.offeredTo || undefined,
1122
1151
  dir: parsed.body.dir || undefined,
1123
1152
  parentTaskId: parsed.body.parentTaskId || undefined,
1153
+ // Human chat starts a new output contract; agent children keep inheriting.
1154
+ inheritParentOutputSchema: principal.kind === "agent",
1124
1155
  source: parsed.body.source || "api",
1125
1156
  outputSchema: parsed.body.outputSchema || undefined,
1126
1157
  contextKey: parsed.body.contextKey || undefined,
@@ -1615,6 +1646,22 @@ export async function handleTasks(
1615
1646
  return true;
1616
1647
  }
1617
1648
 
1649
+ if (getTaskLineageRoute.match(req.method, pathSegments)) {
1650
+ const parsed = await getTaskLineageRoute.parse(req, res, pathSegments, queryParams);
1651
+ if (!parsed) return true;
1652
+ // Every task in the tree shares the session root, so the path posture covers them all.
1653
+ const lineage = await getTaskLineage(parsed.params.id, {
1654
+ maxNodes: parsed.query.maxNodes,
1655
+ maxDepth: parsed.query.maxDepth,
1656
+ });
1657
+ if (!lineage) {
1658
+ jsonError(res, "Task not found", 404);
1659
+ return true;
1660
+ }
1661
+ getTaskLineageRoute.respond(res, 200, lineage);
1662
+ return true;
1663
+ }
1664
+
1618
1665
  if (updateTaskProgressRoute.match(req.method, pathSegments)) {
1619
1666
  const parsed = await updateTaskProgressRoute.parse(req, res, pathSegments, queryParams);
1620
1667
  if (!parsed) return true;
@@ -359,7 +359,7 @@ const copilotTargetProfile: AcpTargetProfile = {
359
359
  },
360
360
  };
361
361
 
362
- /** Qwen Code's `--auth-type` choices (0.25.2). `qwen-oauth` needs a browser, so it is left out. */
362
+ /** Qwen Code's `--auth-type` choices (0.25.3). `qwen-oauth` needs a browser, so it is left out. */
363
363
  const QWEN_AUTH_TYPES = ["openai", "openai-responses", "anthropic", "gemini", "vertex-ai"] as const;
364
364
 
365
365
  // Qwen Code is a Gemini CLI fork: QWEN_SYSTEM_MD replaces its built-in prompt
@@ -44,7 +44,7 @@ import type {
44
44
  * default; the image installs exactly this version and the per-task settings
45
45
  * set `amp.updates.mode: "disabled"`. Bump it deliberately, with a live run.
46
46
  */
47
- export const AMP_PACKAGE = "@ampcode/cli@0.0.1792368068-g91990d";
47
+ export const AMP_PACKAGE = "@ampcode/cli@0.0.1792470027-g8c393d";
48
48
 
49
49
  /** MCP server name. Amp exposes its tools as `agent_swarm` / `store_progress` (dashes become underscores). */
50
50
  const AMP_MCP_SERVER_NAME = "agent-swarm";
@@ -37,7 +37,7 @@ import {
37
37
  clampContextPercent,
38
38
  computeContextUsedUnified,
39
39
  } from "../utils/context-window";
40
- import { isEnvFlagEnabled } from "../utils/env-flag";
40
+ import { isEnvFlagEnabled, isSessionSummarySkipped } from "../utils/env-flag";
41
41
  import { SessionErrorTracker } from "../utils/error-tracker";
42
42
  import { summarizeSession as runSummarize } from "../utils/internal-ai";
43
43
  import { getMemoryRaterNames } from "../utils/memory-raters";
@@ -1739,8 +1739,8 @@ export class CodexSession implements ProviderSession {
1739
1739
  } finally {
1740
1740
  // Session-end summarization. Pure addition for codex — no behavior to
1741
1741
  // preserve. Wrapped in its own try/catch so summary failure must NOT
1742
- // block the existing log/AGENTS.md cleanup below. Gate `SKIP_SESSION_SUMMARY=1`
1743
- // matches the parity convention used by the claude Stop hook + pi/opencode.
1742
+ // block the existing log/AGENTS.md cleanup below. `SKIP_SESSION_SUMMARY` is
1743
+ // read with `isSessionSummarySkipped`, the same as the claude Stop hook + pi/opencode.
1744
1744
  //
1745
1745
  // Skip the summary entirely when the session was aborted. The transcript
1746
1746
  // is incomplete, the LLM call would retry 3× through openrouter and
@@ -1751,7 +1751,7 @@ export class CodexSession implements ProviderSession {
1751
1751
  this.aborted ||
1752
1752
  this.abortController?.signal.aborted === true ||
1753
1753
  this.pendingResult?.exitCode === 130;
1754
- if (process.env.SKIP_SESSION_SUMMARY !== "1" && !sessionWasAborted) {
1754
+ if (!isSessionSummarySkipped() && !sessionWasAborted) {
1755
1755
  try {
1756
1756
  await this.summarizeAtEnd();
1757
1757
  } catch (err) {
@@ -31,7 +31,7 @@ import {
31
31
  splitCredentialPool,
32
32
  validateOpencodeCredentials,
33
33
  } from "../utils/credentials";
34
- import { isEnvFlagEnabled } from "../utils/env-flag";
34
+ import { isEnvFlagEnabled, isSessionSummarySkipped } from "../utils/env-flag";
35
35
  import { fetchInstalledMcpServers } from "../utils/mcp-server-fetcher";
36
36
  import { swarmRuntimeInstanceId } from "../utils/multi-runtime";
37
37
  import {
@@ -797,7 +797,7 @@ export class OpencodeSession implements ProviderSession {
797
797
  /** Identity sync, session summary, and session close (Stop-hook parity). */
798
798
  private async runSessionEnd(signal: AbortSignal): Promise<void> {
799
799
  await sessionHooks.syncIdentity(this.swarm, "session_sync", signal);
800
- if (process.env.SKIP_SESSION_SUMMARY) {
800
+ if (isSessionSummarySkipped()) {
801
801
  console.debug("session_summary skipped (opencode): SKIP_SESSION_SUMMARY is set");
802
802
  } else {
803
803
  const transcript = this.transcript.join("\n").slice(-TRANSCRIPT_TAIL_CHARS);
@@ -10,6 +10,7 @@ import type { ExtensionFactory, McpServerConfig } from "@earendil-works/pi-codin
10
10
  import { buildRatingsFromLlm, fetchRetrievalsForTask, postRatings } from "../be/memory/raters/llm";
11
11
  import { checkToolLoop, clearToolHistory } from "../hooks/tool-loop-detection";
12
12
  import { renderGoalReminder } from "../prompts/goal-reminder";
13
+ import { isSessionSummarySkipped } from "../utils/env-flag";
13
14
  import { summarizeSession as runSummarize } from "../utils/internal-ai";
14
15
  import { getMemoryRaterNames } from "../utils/memory-raters";
15
16
  import { getOpenRouterAttributionHeaders } from "../utils/openrouter-base-url";
@@ -743,7 +744,7 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
743
744
 
744
745
  // Session summarization — get session file from context's session manager
745
746
  const sessionFile = ctx.sessionManager.getSessionFile?.();
746
- if (!process.env.SKIP_SESSION_SUMMARY) {
747
+ if (!isSessionSummarySkipped()) {
747
748
  await summarizeSessionForPi(config, sessionFile);
748
749
  } else {
749
750
  console.debug("session_summary skipped (pi): SKIP_SESSION_SUMMARY is set");
package/src/telemetry.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * - Opt-out via ANONYMIZED_TELEMETRY=false
5
5
  * - Fire-and-forget: never throws, never blocks
6
- * - No DB and no external services (global fetch + node:crypto only)
6
+ * - No DB and no external services (global fetch + built-in APIs only)
7
7
  * - Importable from both API server and workers
8
8
  * - Every event is schema_version 2: a top-level `context` identity envelope
9
9
  * plus properties typed against the proxy's event catalog
@@ -11,6 +11,7 @@
11
11
  * catalog fails `tsc`, and the proxy rejects it at ingest.
12
12
  */
13
13
  import { randomUUID } from "node:crypto";
14
+ import { existsSync } from "node:fs";
14
15
  import pkg from "../package.json";
15
16
  import { isSlackConfigured } from "./slack/config";
16
17
  import {
@@ -591,14 +592,24 @@ function isCloudDeployment(): boolean {
591
592
  return raw === "true" || raw === "1";
592
593
  }
593
594
 
594
- function getTelemetryEnvironment(): string {
595
+ let cachedIsGitCheckout: boolean | undefined;
596
+
597
+ export function getTelemetryEnvironment(packageRoot = new URL("../", import.meta.url)): string {
595
598
  const explicit = process.env.DESPLEGA_TELEMETRY_ENV?.trim();
596
599
  if (explicit) return explicit;
597
600
 
598
601
  // Do not default from NODE_ENV: shipped Bun/npm installs can report
599
602
  // "development" even when the operator did not choose a telemetry cohort.
600
603
  if (getNodeEnv() === "test") return "test";
601
- return "production";
604
+ // Git checkouts (including worktrees) are development; shipped packages omit .git.
605
+ if (cachedIsGitCheckout === undefined) {
606
+ try {
607
+ cachedIsGitCheckout = existsSync(new URL(".git", packageRoot));
608
+ } catch {
609
+ cachedIsGitCheckout = false;
610
+ }
611
+ }
612
+ return cachedIsGitCheckout ? "development" : "production";
602
613
  }
603
614
 
604
615
  /**
@@ -741,6 +752,7 @@ function emit(
741
752
  * Do not call from production code.
742
753
  */
743
754
  export function _resetTelemetryStateForTests(): void {
755
+ cachedIsGitCheckout = undefined;
744
756
  installationId = null;
745
757
  installedAt = null;
746
758
  ingestObserver = null;
@@ -217,6 +217,31 @@ test("rejects invalid filters, sorts, values, source projections, join keys, and
217
217
  expect((await call("app-rows-list", {}, "")).isError).toBe(true);
218
218
  });
219
219
 
220
+ test("app-rows-list rejects equals on a json column, like REST filters and named queries", async () => {
221
+ const jsonApp = await createApp({
222
+ name: "Row tools json",
223
+ definition: {
224
+ ...definition,
225
+ models: { note: { columns: { title: { kind: "string" }, payload: { kind: "json" } } } },
226
+ },
227
+ });
228
+ const created = await call("app-row-create", {
229
+ appId: jsonApp.id,
230
+ values: { title: "json", payload: { x: 1 } },
231
+ });
232
+ expect(created.isError).not.toBe(true);
233
+
234
+ const listed = await call("app-rows-list", { appId: jsonApp.id, equals: { payload: "x" } });
235
+ expect(listed.isError).toBe(true);
236
+ expect(listed.structuredContent).toMatchObject({
237
+ issues: [{ path: "equals.payload", message: "json columns cannot be filtered" }],
238
+ });
239
+ // A scalar column on the same model still filters.
240
+ const byTitle = await call("app-rows-list", { appId: jsonApp.id, equals: { title: "json" } });
241
+ expect(byTitle.isError).not.toBe(true);
242
+ expect(byTitle.structuredContent.total).toBe(1);
243
+ });
244
+
220
245
  test("enforces row, bulk, filter, and limit caps before writing", async () => {
221
246
  await expect(
222
247
  call("app-rows-create", { rows: Array.from({ length: 501 }, () => ({ title: "too-many" })) }),