@desplega.ai/agent-swarm 1.131.1 → 1.133.1

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 (250) hide show
  1. package/README.md +2 -2
  2. package/dist/{actions-jgcy477k.js → actions-cskpayd7.js} +11 -9
  3. package/dist/{anthropic-messages-rjmd2e7n.js → anthropic-messages-tpxtbx7b.js} +28 -12
  4. package/dist/{app-5da78bd4.js → app-ang77nbb.js} +8 -6
  5. package/dist/{assistant-f2vkk1hj.js → assistant-b4exebdw.js} +16 -13
  6. package/dist/{azure-openai-responses-c0yjmbfd.js → azure-openai-responses-m3921t4y.js} +8 -8
  7. package/dist/{boot-reembed-q7mp8hcr.js → boot-reembed-ps2aftpb.js} +9 -7
  8. package/dist/{boot-reembed-xmw8wcx6.js → boot-reembed-znyk3nxb.js} +10 -8
  9. package/dist/{boot-scrub-logs-dsqrv79n.js → boot-scrub-logs-2ax3xgz1.js} +7 -5
  10. package/dist/{claude-adapter-q6x7yspx.js → claude-adapter-3ct8kgxw.js} +13 -9
  11. package/dist/{claude-managed-adapter-wsg37bt8.js → claude-managed-adapter-akx4ntf2.js} +2 -2
  12. package/dist/{cli-114fvtmj.js → cli-13w2hwkh.js} +1 -1
  13. package/dist/{cli-4rkfg1wx.js → cli-1532szb1.js} +8 -6
  14. package/dist/{cli-g74vpjp2.js → cli-23d3rseb.js} +33 -17
  15. package/dist/{cli-3ty0t082.js → cli-2a75z9c5.js} +196 -7
  16. package/dist/{cli-pxfxkx5m.js → cli-2t30rj0d.js} +1 -1
  17. package/dist/{cli-rgrfzg8z.js → cli-3c3tdgab.js} +10 -10
  18. package/dist/{cli-6dp3ydxa.js → cli-3zzxa9jz.js} +15 -11
  19. package/dist/{cli-2f07m0xr.js → cli-4k6233gc.js} +1 -1
  20. package/dist/{cli-t8d349cy.js → cli-5p2tz05r.js} +2 -2
  21. package/dist/{cli-w2wxd56v.js → cli-5z2yhzvd.js} +127 -49
  22. package/dist/{cli-kd1ck4rx.js → cli-6zfq31nj.js} +9 -3
  23. package/dist/{cli-er3a4bb0.js → cli-8m9de10a.js} +6 -1
  24. package/dist/{cli-gm9cbhbq.js → cli-8t14vg9w.js} +27 -8
  25. package/dist/{cli-ccf9vy7q.js → cli-9nfc2es8.js} +6 -6
  26. package/dist/{cli-ycrnynpw.js → cli-9ta3pwms.js} +11 -4
  27. package/dist/{cli-7dm2zqy2.js → cli-avr0ns7z.js} +2 -2
  28. package/dist/{cli-n6h8qt8n.js → cli-c897wyhw.js} +12 -7
  29. package/dist/{cli-gecy3z2s.js → cli-dgk20ft7.js} +4 -4
  30. package/dist/cli-et1bbwqf.js +13 -0
  31. package/dist/{cli-bqw0mt1n.js → cli-f5w27pj3.js} +3 -3
  32. package/dist/{cli-2wadx7p2.js → cli-f8dre4bz.js} +2 -2
  33. package/dist/{cli-6pcz9h15.js → cli-ftwpv451.js} +170 -387
  34. package/dist/{cli-nrkkknat.js → cli-g854s2dw.js} +1 -1
  35. package/dist/{cli-793jvbsh.js → cli-ggmdxeqa.js} +13 -8
  36. package/dist/cli-hkwfe3qj.js +530 -0
  37. package/dist/{cli-5dsbcjjw.js → cli-jqwnqx9m.js} +1 -1
  38. package/dist/{cli-9dr5qzc0.js → cli-ny122vh4.js} +21 -10
  39. package/dist/{cli-sy7jk3tx.js → cli-pvzqhbs9.js} +2 -2
  40. package/dist/cli-pzq5rjym.js +15 -0
  41. package/dist/{cli-xpxde7rm.js → cli-qes8ef67.js} +48 -30
  42. package/dist/{cli-sajgpkqg.js → cli-qs9hhsvb.js} +1723 -325
  43. package/dist/cli-qys3rhhz.js +27 -0
  44. package/dist/{cli-se2p6rkb.js → cli-r6tpm37h.js} +2 -2
  45. package/dist/{cli-g1re8r05.js → cli-rch80c76.js} +2 -2
  46. package/dist/{cli-0258203w.js → cli-rpp0x4zy.js} +137 -17
  47. package/dist/{cli-5n6e5yfn.js → cli-s1k0wxhe.js} +12 -7
  48. package/dist/{cli-aca5pz9f.js → cli-sfd9v7sj.js} +1 -1
  49. package/dist/{cli-2rk0cx7s.js → cli-sh3ytsyj.js} +49 -39
  50. package/dist/{cli-zsspvvm2.js → cli-w1mp9jy7.js} +1 -1
  51. package/dist/{cli-6dpytdr2.js → cli-xdb4mpnp.js} +2 -2
  52. package/dist/{cli-57khth04.js → cli-xmyqv00z.js} +120 -4
  53. package/dist/{cli-jbcva0xj.js → cli-xv8t8p3c.js} +56 -31
  54. package/dist/{cli-f1am9szv.js → cli-xxv5pn4f.js} +1 -1
  55. package/dist/{cli-jevgrb4w.js → cli-y08jvxzx.js} +601 -140
  56. package/dist/{cli-50vbt00w.js → cli-zfbf3srj.js} +650 -245
  57. package/dist/{cli-ygrbmjja.js → cli-zmq89k34.js} +6 -6
  58. package/dist/cli.js +22 -22
  59. package/dist/{codex-adapter-htjkn22b.js → codex-adapter-bef93pq4.js} +9 -7
  60. package/dist/{codex-hook-hq54t9ft.js → codex-hook-e8dba65p.js} +3 -2
  61. package/dist/{codex-session-runner-wjfc7smz.js → codex-session-runner-5pkxys7x.js} +9 -7
  62. package/dist/{commands-xhrfwqsk.js → commands-e23s1bff.js} +9 -7
  63. package/dist/{db-anvwb22f.js → db-m3nabatn.js} +17 -5
  64. package/dist/{google-generative-ai-hysrd8qx.js → google-generative-ai-8maw55xh.js} +11 -8
  65. package/dist/{google-vertex-p5arrga0.js → google-vertex-8vjqmx8g.js} +11 -8
  66. package/dist/{handlers-b9kbc621.js → handlers-akazedvr.js} +16 -13
  67. package/dist/{hook-xkvn6c5h.js → hook-ze4cpf0v.js} +55 -41
  68. package/dist/{hook-cgk4wxmx.js → hook-zfqyyenw.js} +10 -7
  69. package/dist/{http-9xh6ta4r.js → http-sja0n6kw.js} +608 -147
  70. package/dist/index-0gr8vd6k.js +39 -0
  71. package/dist/{index-eaxyqcmj.js → index-354vkhdv.js} +154 -74
  72. package/dist/{index-070v4eyb.js → index-5qthek5t.js} +2 -3
  73. package/dist/{index-25wjsdv2.js → index-95t3cqq5.js} +16 -14
  74. package/dist/{index-9znhar3x.js → index-sea66619.js} +3 -4
  75. package/dist/{index-ywds4812.js → index-vpn36mma.js} +15 -13
  76. package/dist/{keepalive-vc7s2aa6.js → keepalive-3sp3meqj.js} +16 -10
  77. package/dist/{lead-r2yqfmx7.js → lead-mefeewkf.js} +39 -36
  78. package/dist/{maintenance-mw70k468.js → maintenance-8tbrbjfg.js} +10 -8
  79. package/dist/mistral-conversations-00zrp7g4.js +717 -0
  80. package/dist/{oauth-refresh-sweep-yecgjpk4.js → oauth-refresh-sweep-srjwjz6m.js} +8 -6
  81. package/dist/{onboard-xj0f73f9.js → onboard-mt2tgyjg.js} +7 -7
  82. package/dist/{openai-codex-responses-5g27pd6f.js → openai-codex-responses-map32h2s.js} +18 -18
  83. package/dist/{openai-completions-3vkmfz2a.js → openai-completions-w871b1cq.js} +12 -11
  84. package/dist/{openai-responses-dg4t7j7a.js → openai-responses-b0j7xnw8.js} +15 -12
  85. package/dist/{opencode-adapter-1gm38aba.js → opencode-adapter-b39jefcj.js} +5 -3
  86. package/dist/{openrouter-images-g3gm8931.js → openrouter-images-srfkywy0.js} +4 -4
  87. package/dist/{otel-impl-nxg0n0b3.js → otel-impl-z6pxt3j9.js} +224 -9
  88. package/dist/{pi-mono-adapter-4g5kby31.js → pi-mono-adapter-btvqmj58.js} +1138 -408
  89. package/dist/{pricing-refresh-2cn6effr.js → pricing-refresh-kyq8cezh.js} +9 -7
  90. package/dist/{rbac-roles-1hcxdtg7.js → rbac-roles-3ryfrfna.js} +8 -6
  91. package/dist/{rbac-roles-vynday5g.js → rbac-roles-6qfvbee1.js} +9 -7
  92. package/dist/{render-v2-tc0bh8kk.js → render-v2-p61ppjey.js} +8 -6
  93. package/dist/{seed-pricing-8kceb43f.js → seed-pricing-j56bbj4y.js} +8 -6
  94. package/dist/{setup-46xtgq3c.js → setup-tpr6c1kn.js} +2 -2
  95. package/dist/{worker-qrbz6hmv.js → worker-9w1v0mbb.js} +39 -36
  96. package/openapi.json +542 -16
  97. package/package.json +6 -6
  98. package/src/be/budget-refusal-notify.ts +14 -7
  99. package/src/be/db-queries/task-artifacts.ts +182 -0
  100. package/src/be/db.ts +783 -169
  101. package/src/be/events.ts +5 -0
  102. package/src/be/migrations/131_session_logs_created_at_index.sql +2 -0
  103. package/src/be/migrations/132_multi_runtime_instances.sql +44 -0
  104. package/src/be/migrations/133_workflow_run_trigger_type.sql +12 -0
  105. package/src/be/migrations/135_backfill_task_pull_request_attachments.sql +228 -0
  106. package/src/be/migrations/136_task_requester_provenance.sql +37 -0
  107. package/src/be/migrations/137_memory_retrieval_composite_index.sql +1 -0
  108. package/src/be/multi-runtime.ts +430 -0
  109. package/src/be/scripts/extract-schema.ts +3 -1
  110. package/src/be/scripts/typecheck.ts +3 -2
  111. package/src/be/seed-skills/index.ts +7 -0
  112. package/src/be/steering.ts +1 -1
  113. package/src/be/swarm-config-guard.ts +18 -0
  114. package/src/commands/credential-wait.ts +54 -0
  115. package/src/commands/onboard/steps/core-credentials.tsx +1 -1
  116. package/src/commands/onboard/steps/custom-templates.tsx +1 -1
  117. package/src/commands/onboard/steps/harness-credentials.tsx +1 -1
  118. package/src/commands/onboard/steps/health-check.tsx +1 -1
  119. package/src/commands/onboard/steps/start.tsx +1 -1
  120. package/src/commands/profile-sync-audit.ts +56 -0
  121. package/src/commands/profile-sync.ts +287 -15
  122. package/src/commands/provider-credentials.ts +46 -19
  123. package/src/commands/runner.ts +196 -33
  124. package/src/commands/templates.ts +22 -0
  125. package/src/github/handlers.ts +55 -9
  126. package/src/heartbeat/heartbeat.ts +71 -12
  127. package/src/hooks/hook.ts +49 -33
  128. package/src/http/active-sessions.ts +11 -1
  129. package/src/http/agents.ts +261 -30
  130. package/src/http/all-routes.ts +1 -0
  131. package/src/http/config.ts +13 -2
  132. package/src/http/core.ts +102 -4
  133. package/src/http/db-query-bounded.ts +313 -0
  134. package/src/http/db-query-shared.ts +249 -0
  135. package/src/http/db-query.ts +54 -42
  136. package/src/http/events.ts +2 -0
  137. package/src/http/index.ts +22 -2
  138. package/src/http/mcp-bridge.ts +37 -3
  139. package/src/http/mcp.ts +12 -0
  140. package/src/http/memory.ts +13 -2
  141. package/src/http/openapi.ts +1 -0
  142. package/src/http/poll.ts +70 -27
  143. package/src/http/route-def.ts +43 -9
  144. package/src/http/schedules.ts +2 -2
  145. package/src/http/scripts.ts +10 -1
  146. package/src/http/session-data.ts +44 -0
  147. package/src/http/tasks.ts +2 -2
  148. package/src/http/users.ts +74 -52
  149. package/src/http/webhooks.ts +5 -2
  150. package/src/http/workflows.ts +1 -4
  151. package/src/integrations/kapso/inbound.ts +1 -1
  152. package/src/jira/outbound.ts +39 -8
  153. package/src/jira/webhook-lifecycle.ts +13 -2
  154. package/src/linear/outbound.ts +43 -10
  155. package/src/oauth/keepalive.ts +13 -2
  156. package/src/prompts/base-prompt.ts +1 -1
  157. package/src/prompts/session-templates.ts +45 -7
  158. package/src/providers/claude-adapter.ts +6 -0
  159. package/src/providers/codex-adapter.ts +6 -0
  160. package/src/providers/opencode-adapter.ts +6 -0
  161. package/src/providers/pi-mono-adapter.ts +8 -0
  162. package/src/providers/pricing-sources.md +24 -0
  163. package/src/queue-stall-alarm.ts +216 -0
  164. package/src/scheduler/scheduler.ts +7 -0
  165. package/src/script-workflows/executor.ts +2 -1
  166. package/src/scripts-runtime/executors/native.ts +2 -1
  167. package/src/scripts-runtime/executors/types.ts +5 -0
  168. package/src/scripts-runtime/loader.ts +5 -0
  169. package/src/scripts-runtime/swarm-config.ts +8 -0
  170. package/src/scripts-runtime/swarm-sdk.ts +7 -0
  171. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  172. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  173. package/src/tests/budget-claim-gate.test.ts +20 -0
  174. package/src/tests/claude-adapter.test.ts +32 -0
  175. package/src/tests/codex-adapter.test.ts +30 -0
  176. package/src/tests/create-task-validation.test.ts +128 -0
  177. package/src/tests/credential-wait.test.ts +67 -1
  178. package/src/tests/db-query.test.ts +711 -2
  179. package/src/tests/http-api-integration.test.ts +81 -0
  180. package/src/tests/http-users.test.ts +166 -1
  181. package/src/tests/identity-field-budget.test.ts +176 -0
  182. package/src/tests/mcp-tools-user.test.ts +34 -0
  183. package/src/tests/mcp-user-route.test.ts +17 -0
  184. package/src/tests/metrics-http.test.ts +44 -0
  185. package/src/tests/multi-runtime-registration.test.ts +2703 -0
  186. package/src/tests/openapi-response-contract.test.ts +24 -0
  187. package/src/tests/openapi-route-headers.test.ts +83 -0
  188. package/src/tests/opencode-adapter.test.ts +40 -0
  189. package/src/tests/outbound-listener-rejection.test.ts +138 -0
  190. package/src/tests/pi-mono-adapter.test.ts +98 -0
  191. package/src/tests/profile-sync-warning.test.ts +277 -0
  192. package/src/tests/profile-sync.test.ts +232 -6
  193. package/src/tests/prompt-template-session.test.ts +32 -3
  194. package/src/tests/queue-stall-alarm.test.ts +226 -0
  195. package/src/tests/requester-comms.test.ts +32 -0
  196. package/src/tests/runner-requester-profile.test.ts +38 -0
  197. package/src/tests/schedule-target-type.test.ts +38 -0
  198. package/src/tests/script-connections.test.ts +13 -1
  199. package/src/tests/scripts-runtime-identity.test.ts +375 -0
  200. package/src/tests/scripts-runtime.test.ts +4 -3
  201. package/src/tests/sdk-allowlist.test.ts +47 -0
  202. package/src/tests/send-task-requested-by.test.ts +9 -0
  203. package/src/tests/session-costs.test.ts +506 -1
  204. package/src/tests/shutdown-error-scrubbing.test.ts +92 -0
  205. package/src/tests/steering-core.test.ts +28 -0
  206. package/src/tests/task-artifacts.test.ts +386 -0
  207. package/src/tests/task-attachments-schema.test.ts +17 -1
  208. package/src/tests/task-completion-idempotency.test.ts +57 -0
  209. package/src/tests/task-pull-request-backfill-migration.test.ts +321 -0
  210. package/src/tests/update-profile-api.test.ts +67 -39
  211. package/src/tests/update-profile-auth.test.ts +48 -4
  212. package/src/tests/user-attribution-migration.test.ts +54 -0
  213. package/src/tests/user-identity.test.ts +72 -0
  214. package/src/tests/workflow-async-v2.test.ts +39 -1
  215. package/src/tests/workflow-http-v2.test.ts +3 -2
  216. package/src/tests/workflow-mcp-trigger-schema.test.ts +52 -3
  217. package/src/tests/workflow-retry-v2.test.ts +45 -1
  218. package/src/tools/db-query.ts +18 -8
  219. package/src/tools/get-task-details.ts +24 -3
  220. package/src/tools/manage-user.ts +36 -3
  221. package/src/tools/poll-task.ts +93 -14
  222. package/src/tools/script-common.ts +5 -0
  223. package/src/tools/send-task.ts +1 -4
  224. package/src/tools/store-progress.ts +13 -2
  225. package/src/tools/swarm-config/delete-config.ts +9 -0
  226. package/src/tools/swarm-config/set-config.ts +3 -2
  227. package/src/tools/task-action.ts +30 -0
  228. package/src/tools/task-tool-ctx.ts +8 -1
  229. package/src/tools/update-profile.ts +16 -21
  230. package/src/tools/utils.ts +5 -0
  231. package/src/tools/workflows/trigger-workflow.ts +7 -1
  232. package/src/types.ts +165 -1
  233. package/src/utils/github-pull-request.ts +76 -0
  234. package/src/utils/identity-field-budget.ts +73 -0
  235. package/src/utils/multi-runtime.ts +20 -0
  236. package/src/utils/requester-comms.ts +24 -0
  237. package/src/workflows/engine.ts +22 -2
  238. package/src/workflows/resume.ts +19 -11
  239. package/src/workflows/retry-poller.ts +1 -0
  240. package/src/workflows/triggers.ts +4 -0
  241. package/templates/skills/db-query-guidance/config.json +14 -0
  242. package/templates/skills/db-query-guidance/content.md +57 -0
  243. package/dist/cli-3te4jdkm.js +0 -225
  244. package/dist/cli-bmze986d.js +0 -7
  245. package/dist/cli-kh3ba2zf.js +0 -607
  246. package/dist/index-vg4snay4.js +0 -36
  247. package/dist/mistral-conversations-81fwav0j.js +0 -30578
  248. package/dist/otel-nqa12wf3.js +0 -3149
  249. package/dist/{cli-wz9115x0.js → cli-zr9q011v.js} +6 -6
  250. package/dist/{pi-messages-tqj8q6b9.js → pi-messages-sssy32x4.js} +3 -3
@@ -1,11 +1,14 @@
1
1
  import { describe, expect, spyOn, test } from "bun:test";
2
2
  import {
3
3
  buildIdentityPayload,
4
+ buildIndependentIdentityPayloads,
4
5
  CLAUDE_MD_PATH,
5
6
  collectProfilePayloads,
6
7
  contentSha256,
7
8
  extractSetupScriptContent,
8
9
  type FileReader,
10
+ findProfileDivergences,
11
+ HEARTBEAT_MD_PATH,
9
12
  IDENTITY_BASELINES_PATH,
10
13
  IDENTITY_MD_PATH,
11
14
  type IdentityBaselines,
@@ -17,7 +20,9 @@ import {
17
20
  TOOLS_MD_PATH,
18
21
  WORKSPACE_CLAUDE_MD_PATH,
19
22
  } from "../commands/profile-sync";
23
+ import { profileSyncAuditExitCode, runProfileSyncAudit } from "../commands/profile-sync-audit";
20
24
  import { MAX_PROFILE_FILE_LENGTH } from "../utils/constants";
25
+ import { IDENTITY_FIELD_BUDGETS } from "../utils/identity-field-budget";
21
26
 
22
27
  const MARKER_START = "# === Agent-managed setup (from DB) ===";
23
28
  const MARKER_END = "# === End agent-managed setup ===";
@@ -25,6 +30,182 @@ const MARKER_END = "# === End agent-managed setup ===";
25
30
  // A SOUL/IDENTITY body long enough to clear the 500-char min-length guard.
26
31
  const LONG = "x".repeat(600);
27
32
 
33
+ describe("profile divergence audit", () => {
34
+ test("compares all four budgeted fields plus ungated heartbeatMd", () => {
35
+ const divergences = findProfileDivergences(
36
+ {
37
+ soulMd: "disk soul",
38
+ identityMd: "disk identity",
39
+ claudeMd: "disk claude",
40
+ toolsMd: "disk tools",
41
+ heartbeatMd: "disk heartbeat",
42
+ },
43
+ {
44
+ soulMd: "db soul",
45
+ identityMd: "db identity",
46
+ claudeMd: "db claude",
47
+ toolsMd: "db tools",
48
+ heartbeatMd: "db heartbeat",
49
+ },
50
+ );
51
+
52
+ expect(divergences.map((entry) => entry.field)).toEqual([
53
+ "soulMd",
54
+ "identityMd",
55
+ "claudeMd",
56
+ "toolsMd",
57
+ "heartbeatMd",
58
+ ]);
59
+ expect(divergences[0]?.budget).toBe(IDENTITY_FIELD_BUDGETS.soulMd);
60
+ expect(divergences[2]?.budget).toBe(IDENTITY_FIELD_BUDGETS.claudeMd);
61
+ expect(divergences[4]?.budget).toBeNull();
62
+ expect(divergences[4]?.diskSize).toBe("disk heartbeat".length);
63
+ expect(divergences[4]?.dbSize).toBe("db heartbeat".length);
64
+ });
65
+
66
+ test("fetches the DB profile and reads the canonical local paths", async () => {
67
+ const local = {
68
+ [SOUL_MD_PATH]: "same soul",
69
+ [IDENTITY_MD_PATH]: "disk identity",
70
+ [WORKSPACE_CLAUDE_MD_PATH]: "same claude",
71
+ [TOOLS_MD_PATH]: "same tools",
72
+ [HEARTBEAT_MD_PATH]: "disk heartbeat",
73
+ };
74
+ const readFile: FileReader = async (path) => local[path as keyof typeof local];
75
+ let request: Request | null = null;
76
+ const fetchImpl = (async (input: string | URL | Request, init?: RequestInit) => {
77
+ request = new Request(input, init);
78
+ return new Response(
79
+ JSON.stringify({
80
+ soulMd: "same soul",
81
+ identityMd: "db identity",
82
+ claudeMd: "same claude",
83
+ toolsMd: "same tools",
84
+ heartbeatMd: "db heartbeat",
85
+ }),
86
+ { status: 200, headers: { "Content-Type": "application/json" } },
87
+ );
88
+ }) as typeof fetch;
89
+
90
+ const result = await runProfileSyncAudit(
91
+ {
92
+ agentId: "agent-1",
93
+ apiUrl: "https://api.example.test",
94
+ apiKey: "secret-key",
95
+ claudeMdPath: WORKSPACE_CLAUDE_MD_PATH,
96
+ },
97
+ { fetchImpl, readFile },
98
+ );
99
+
100
+ expect(request?.url).toBe("https://api.example.test/me");
101
+ expect(request?.headers.get("X-Agent-ID")).toBe("agent-1");
102
+ expect(result.divergences.map((entry) => entry.field)).toEqual(["identityMd", "heartbeatMd"]);
103
+ });
104
+
105
+ test("uses the registered effective provider to select the Claude profile path", async () => {
106
+ const readPaths: string[] = [];
107
+ const readFile: FileReader = async (path) => {
108
+ readPaths.push(path);
109
+ return "same";
110
+ };
111
+ const fetchImpl = (async () =>
112
+ new Response(
113
+ JSON.stringify({
114
+ harnessProvider: "codex",
115
+ soulMd: "same",
116
+ identityMd: "same",
117
+ claudeMd: "same",
118
+ toolsMd: "same",
119
+ heartbeatMd: "same",
120
+ }),
121
+ { status: 200, headers: { "Content-Type": "application/json" } },
122
+ )) as typeof fetch;
123
+
124
+ const result = await runProfileSyncAudit(
125
+ {
126
+ agentId: "agent-codex",
127
+ apiUrl: "https://api.example.test",
128
+ apiKey: "secret-key",
129
+ },
130
+ { fetchImpl, readFile },
131
+ );
132
+
133
+ expect(result.divergences).toEqual([]);
134
+ expect(readPaths).toContain(WORKSPACE_CLAUDE_MD_PATH);
135
+ expect(readPaths).not.toContain(CLAUDE_MD_PATH);
136
+ });
137
+
138
+ test("refuses to guess the audit path when no effective provider is registered", async () => {
139
+ const fetchImpl = (async () =>
140
+ new Response(JSON.stringify({}), {
141
+ status: 200,
142
+ headers: { "Content-Type": "application/json" },
143
+ })) as typeof fetch;
144
+
145
+ expect(
146
+ runProfileSyncAudit(
147
+ {
148
+ agentId: "agent-unregistered",
149
+ apiUrl: "https://api.example.test",
150
+ apiKey: "secret-key",
151
+ },
152
+ { fetchImpl, readFile: async () => undefined },
153
+ ),
154
+ ).rejects.toThrow("registered harness provider");
155
+ });
156
+
157
+ test("treats profile fields absent on disk and empty in the DB as equal", async () => {
158
+ const readFile: FileReader = async () => undefined;
159
+ const fetchImpl = (async () =>
160
+ new Response(JSON.stringify({ soulMd: "" }), {
161
+ status: 200,
162
+ headers: { "Content-Type": "application/json" },
163
+ })) as typeof fetch;
164
+
165
+ const result = await runProfileSyncAudit(
166
+ {
167
+ agentId: "agent-unset",
168
+ apiUrl: "https://api.example.test",
169
+ apiKey: "secret-key",
170
+ claudeMdPath: WORKSPACE_CLAUDE_MD_PATH,
171
+ },
172
+ { fetchImpl, readFile },
173
+ );
174
+
175
+ expect(result.divergences).toEqual([]);
176
+ expect(profileSyncAuditExitCode(result.divergences)).toBe(0);
177
+ });
178
+
179
+ test("reports a missing local file when the DB value is non-empty", () => {
180
+ const divergences = findProfileDivergences(
181
+ {
182
+ soulMd: undefined,
183
+ identityMd: "same identity",
184
+ claudeMd: "same claude",
185
+ toolsMd: "same tools",
186
+ heartbeatMd: "same heartbeat",
187
+ },
188
+ {
189
+ soulMd: "stored soul",
190
+ identityMd: "same identity",
191
+ claudeMd: "same claude",
192
+ toolsMd: "same tools",
193
+ heartbeatMd: "same heartbeat",
194
+ },
195
+ );
196
+
197
+ expect(divergences).toHaveLength(1);
198
+ expect(divergences[0]).toMatchObject({
199
+ field: "soulMd",
200
+ diskMissing: true,
201
+ diskSize: null,
202
+ diskHash: null,
203
+ dbSize: "stored soul".length,
204
+ delta: null,
205
+ });
206
+ });
207
+ });
208
+
28
209
  describe("extractSetupScriptContent (marker extraction)", () => {
29
210
  test("extracts ONLY the content between the agent-managed markers", () => {
30
211
  const raw = [
@@ -97,6 +278,19 @@ describe("buildIdentityPayload (min-length guard)", () => {
97
278
  expect(payload.heartbeatMd).toBe("");
98
279
  });
99
280
 
281
+ test("forwards oversized gated identity fields so the server ratchet can decide", () => {
282
+ const oversized = "x".repeat(MAX_PROFILE_FILE_LENGTH + 1);
283
+ const payload = buildIdentityPayload({
284
+ soulMd: oversized,
285
+ identityMd: oversized,
286
+ toolsMd: oversized,
287
+ });
288
+
289
+ expect(payload.soulMd).toBe(oversized);
290
+ expect(payload.identityMd).toBe(oversized);
291
+ expect(payload.toolsMd).toBe(oversized);
292
+ });
293
+
100
294
  test("logs the agent, field, actual length, and cap when a file is too large", () => {
101
295
  const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
102
296
  try {
@@ -142,7 +336,13 @@ describe("collectProfilePayloads (field gate)", () => {
142
336
  "session_sync",
143
337
  files,
144
338
  );
145
- expect(all.map((p) => p.label).sort()).toEqual(["claude", "identity", "setup"]);
339
+ expect(all.map((p) => p.label).sort()).toEqual([
340
+ "claude",
341
+ "identity.identityMd",
342
+ "identity.soulMd",
343
+ "identity.toolsMd",
344
+ "setup",
345
+ ]);
146
346
  });
147
347
 
148
348
  test("a missing file yields no payload for that group (no empty POST)", async () => {
@@ -413,11 +613,13 @@ describe("collectProfilePayloads (baseline integration)", () => {
413
613
  });
414
614
 
415
615
  const payloads = await collectProfilePayloads(["identity"], "self_edit", files);
416
- expect(payloads).toHaveLength(1);
417
- // self_edit should include ALL files regardless of baselines
418
- expect(payloads[0]?.body.soulMd).toBe(identityContent);
419
- expect(payloads[0]?.body.identityMd).toBe(identityContent);
420
- expect(payloads[0]?.body.toolsMd).toBe("tools");
616
+ expect(payloads).toHaveLength(3);
617
+ // self_edit should include ALL files regardless of baselines, independently.
618
+ expect(payloads.map((payload) => payload.body)).toEqual([
619
+ { soulMd: identityContent, changeSource: "self_edit" },
620
+ { identityMd: identityContent, changeSource: "self_edit" },
621
+ { toolsMd: "tools", changeSource: "self_edit" },
622
+ ]);
421
623
  });
422
624
 
423
625
  test("session_sync skips unchanged CLAUDE.md when baseline matches", async () => {
@@ -482,3 +684,27 @@ describe("collectProfilePayloads (baseline integration)", () => {
482
684
  expect(payloads).toEqual([]);
483
685
  });
484
686
  });
687
+
688
+ describe("buildIndependentIdentityPayloads", () => {
689
+ test("splits fields so one rejected write cannot discard valid siblings", () => {
690
+ expect(
691
+ buildIndependentIdentityPayloads(
692
+ { soulMd: "rejected growth", toolsMd: "valid edit", heartbeatMd: "ungated edit" },
693
+ "session_sync",
694
+ ),
695
+ ).toEqual([
696
+ {
697
+ label: "identity.soulMd",
698
+ body: { soulMd: "rejected growth", changeSource: "session_sync" },
699
+ },
700
+ {
701
+ label: "identity.toolsMd",
702
+ body: { toolsMd: "valid edit", changeSource: "session_sync" },
703
+ },
704
+ {
705
+ label: "identity.heartbeatMd",
706
+ body: { heartbeatMd: "ungated edit", changeSource: "session_sync" },
707
+ },
708
+ ]);
709
+ });
710
+ });
@@ -68,7 +68,7 @@ describe("Session templates — registration", () => {
68
68
  await ensureTemplatesRegistered();
69
69
  });
70
70
 
71
- test("all 18 system templates are registered", () => {
71
+ test("all 19 system templates are registered", () => {
72
72
  const systemTemplates = [
73
73
  "system.agent.role",
74
74
  "system.agent.register",
@@ -89,6 +89,7 @@ describe("Session templates — registration", () => {
89
89
  "system.agent.services",
90
90
  "system.agent.artifacts",
91
91
  "system.agent.share_urls",
92
+ "system.agent.communication_style",
92
93
  ];
93
94
 
94
95
  for (const eventType of systemTemplates) {
@@ -113,7 +114,7 @@ describe("Session templates — registration", () => {
113
114
  }
114
115
  });
115
116
 
116
- test("total of 31 session/system templates registered", () => {
117
+ test("total of 32 session/system templates registered", () => {
117
118
  const all = getAllTemplateDefinitions();
118
119
  const sessionSystem = all.filter((d) => d.category === "system" || d.category === "session");
119
120
  // 26 = the original 19 + `system.session.worker.pi` + `system.agent.seed_scripts`
@@ -124,7 +125,8 @@ describe("Session templates — registration", () => {
124
125
  // 30 = 29 + `system.agent.scheduling` (split out of context_mode so the
125
126
  // pi composites keep the scheduling rules without the ctx_* tool block).
126
127
  // 31 = 30 + `system.agent.apps`.
127
- expect(sessionSystem.length).toBe(31);
128
+ // 32 = 31 + `system.agent.communication_style`.
129
+ expect(sessionSystem.length).toBe(32);
128
130
  });
129
131
  });
130
132
 
@@ -332,6 +334,9 @@ describe("Session templates — composite resolution", () => {
332
334
  // Contains register section
333
335
  expect(result.text).toContain("join-swarm");
334
336
 
337
+ // Lead is hinted to persist learned requester comms preferences
338
+ expect(result.text).toContain("persist them with its `comms` field");
339
+
335
340
  // Contains lead-specific section (not worker)
336
341
  expect(result.text).toContain("CRITICAL: You are a coordinator");
337
342
  expect(result.text).toContain("coordinator");
@@ -351,6 +356,9 @@ describe("Session templates — composite resolution", () => {
351
356
 
352
357
  // Contains system
353
358
  expect(result.text).toContain("System packages available");
359
+
360
+ // Contains communication style
361
+ expect(result.text).toContain("### Communication Style");
354
362
  });
355
363
 
356
364
  test("system.session.worker resolves all template references", () => {
@@ -387,6 +395,27 @@ describe("Session templates — composite resolution", () => {
387
395
 
388
396
  // Contains system
389
397
  expect(result.text).toContain("System packages available");
398
+
399
+ // Contains communication style
400
+ expect(result.text).toContain("### Communication Style");
401
+ });
402
+
403
+ test("communication style block resolves in every session composite", () => {
404
+ const composites = [
405
+ "system.session.lead",
406
+ "system.session.worker",
407
+ "system.session.worker.pi",
408
+ "system.session.lead.pi",
409
+ "system.session.worker.remote",
410
+ ];
411
+
412
+ for (const eventType of composites) {
413
+ const result = resolveTemplate(eventType, { role: "worker", agentId: "agent-001" });
414
+ expect(result.skipped).toBe(false);
415
+ expect(result.text).toContain("### Communication Style");
416
+ expect(result.text).toContain("Mirror the requester");
417
+ expect(result.text).toContain("Never use em dashes");
418
+ }
390
419
  });
391
420
 
392
421
  test("composite does NOT include conditional sections (agent_fs, services, artifacts)", () => {
@@ -0,0 +1,226 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { closeDb, createAgent, createTaskExtended, getDb, initDb, updateTaskVcs } from "../be/db";
4
+ import {
5
+ _test,
6
+ checkQueueStall,
7
+ getQueueStallSnapshot,
8
+ isQueueStalled,
9
+ QUEUE_STALL_THRESHOLD_MS,
10
+ } from "../queue-stall-alarm";
11
+
12
+ const TEST_DB_PATH = "./test-queue-stall-alarm.sqlite";
13
+ const NOW = new Date("2026-08-17T17:00:00.000Z");
14
+
15
+ beforeAll(() => {
16
+ closeDb();
17
+ initDb(TEST_DB_PATH);
18
+ });
19
+
20
+ beforeEach(() => {
21
+ _test.resetState();
22
+ getDb().run("DELETE FROM agent_log");
23
+ getDb().run("DELETE FROM agent_tasks");
24
+ getDb().run("DELETE FROM agents");
25
+ });
26
+
27
+ afterAll(async () => {
28
+ _test.resetState();
29
+ closeDb();
30
+ await unlink(TEST_DB_PATH).catch(() => {});
31
+ await unlink(`${TEST_DB_PATH}-wal`).catch(() => {});
32
+ await unlink(`${TEST_DB_PATH}-shm`).catch(() => {});
33
+ });
34
+
35
+ function setCreatedAt(taskId: string, createdAt: string): void {
36
+ getDb().run("UPDATE agent_tasks SET createdAt = ?, lastUpdatedAt = ? WHERE id = ?", [
37
+ createdAt,
38
+ createdAt,
39
+ taskId,
40
+ ]);
41
+ getDb().run(
42
+ "UPDATE agent_log SET createdAt = ? WHERE taskId = ? AND eventType = 'task_created'",
43
+ [createdAt, taskId],
44
+ );
45
+ }
46
+
47
+ describe("queue stall alarm", () => {
48
+ test("positive control: alerts when a claimable task is older than 30 minutes", async () => {
49
+ const worker = createAgent({ name: "worker", isLead: false, status: "idle" });
50
+ const task = createTaskExtended("Old claimable task", { agentId: worker.id });
51
+ setCreatedAt(task.id, "2026-08-17T16:29:59.000Z");
52
+ const notify = mock(async (_message: string) => {});
53
+
54
+ const snapshot = await checkQueueStall(NOW, notify);
55
+
56
+ expect(isQueueStalled(snapshot)).toBe(true);
57
+ expect(snapshot.claimableCount).toBe(1);
58
+ expect(snapshot.oldestTaskId).toBe(task.id);
59
+ expect(snapshot.oldestAgeMs).toBeGreaterThan(QUEUE_STALL_THRESHOLD_MS);
60
+ expect(notify).toHaveBeenCalledTimes(1);
61
+ expect(notify.mock.calls[0]?.[0]).toContain("queue pickup stalled");
62
+ });
63
+
64
+ test("negative control: an empty queue never alerts", async () => {
65
+ const notify = mock(async (_message: string) => {});
66
+
67
+ const snapshot = await checkQueueStall(NOW, notify);
68
+
69
+ expect(snapshot.claimableCount).toBe(0);
70
+ expect(isQueueStalled(snapshot)).toBe(false);
71
+ expect(notify).not.toHaveBeenCalled();
72
+ });
73
+
74
+ test("does not alert for a non-empty but fresh queue", async () => {
75
+ const task = createTaskExtended("Fresh pool task");
76
+ setCreatedAt(task.id, "2026-08-17T16:45:00.000Z");
77
+ const notify = mock(async (_message: string) => {});
78
+
79
+ const snapshot = await checkQueueStall(NOW, notify);
80
+
81
+ expect(snapshot.claimableCount).toBe(1);
82
+ expect(isQueueStalled(snapshot)).toBe(false);
83
+ expect(notify).not.toHaveBeenCalled();
84
+ });
85
+
86
+ test("measures queue age from a backlog task becoming claimable", async () => {
87
+ const task = createTaskExtended("Released backlog task");
88
+ setCreatedAt(task.id, "2026-08-17T12:00:00.000Z");
89
+ getDb().run("UPDATE agent_tasks SET status = 'backlog' WHERE id = ?", [task.id]);
90
+ getDb().run("UPDATE agent_tasks SET status = 'unassigned', lastUpdatedAt = ? WHERE id = ?", [
91
+ "2026-08-17T16:59:00.000Z",
92
+ task.id,
93
+ ]);
94
+ getDb().run(
95
+ `INSERT INTO agent_log (id, eventType, taskId, oldValue, newValue, createdAt)
96
+ VALUES (?, 'task_status_change', ?, 'backlog', 'unassigned', ?)`,
97
+ [crypto.randomUUID(), task.id, "2026-08-17T16:59:00.000Z"],
98
+ );
99
+ const notify = mock(async (_message: string) => {});
100
+
101
+ const snapshot = await checkQueueStall(NOW, notify);
102
+
103
+ expect(snapshot.oldestAgeMs).toBe(60_000);
104
+ expect(isQueueStalled(snapshot)).toBe(false);
105
+ expect(notify).not.toHaveBeenCalled();
106
+ });
107
+
108
+ test("measures queue age from the final dependency completing", async () => {
109
+ const worker = createAgent({ name: "worker", isLead: false, status: "idle" });
110
+ const prerequisite = createTaskExtended("Prerequisite", { agentId: worker.id });
111
+ const blocked = createTaskExtended("Newly unblocked", {
112
+ agentId: worker.id,
113
+ dependsOn: [prerequisite.id],
114
+ });
115
+ setCreatedAt(blocked.id, "2026-08-17T12:00:00.000Z");
116
+ getDb().run(
117
+ "UPDATE agent_tasks SET status = 'completed', finishedAt = ?, lastUpdatedAt = ? WHERE id = ?",
118
+ ["2026-08-17T16:59:00.000Z", "2026-08-17T16:59:00.000Z", prerequisite.id],
119
+ );
120
+ const notify = mock(async (_message: string) => {});
121
+
122
+ const snapshot = await checkQueueStall(NOW, notify);
123
+
124
+ expect(snapshot.oldestTaskId).toBe(blocked.id);
125
+ expect(snapshot.oldestAgeMs).toBe(60_000);
126
+ expect(isQueueStalled(snapshot)).toBe(false);
127
+ expect(notify).not.toHaveBeenCalled();
128
+ });
129
+
130
+ test("does not reset queue age when VCS metadata updates a claimable task", () => {
131
+ const task = createTaskExtended("Old task with new VCS metadata");
132
+ setCreatedAt(task.id, "2026-08-17T16:00:00.000Z");
133
+
134
+ updateTaskVcs(task.id, {
135
+ vcsProvider: "github",
136
+ vcsRepo: "desplega-ai/agent-swarm",
137
+ vcsNumber: 1177,
138
+ vcsUrl: "https://github.com/desplega-ai/agent-swarm/pull/1177",
139
+ });
140
+
141
+ const snapshot = getQueueStallSnapshot(NOW);
142
+ expect(snapshot.oldestAgeMs).toBe(60 * 60 * 1000);
143
+ expect(isQueueStalled(snapshot)).toBe(true);
144
+ });
145
+
146
+ test("does not reset claimable age when VCS metadata updates a completed dependency", () => {
147
+ const worker = createAgent({ name: "worker", isLead: false, status: "idle" });
148
+ const prerequisite = createTaskExtended("Completed prerequisite", { agentId: worker.id });
149
+ const blocked = createTaskExtended("Waiting after prerequisite", {
150
+ agentId: worker.id,
151
+ dependsOn: [prerequisite.id],
152
+ });
153
+ setCreatedAt(blocked.id, "2026-08-17T12:00:00.000Z");
154
+ getDb().run(
155
+ "UPDATE agent_tasks SET status = 'completed', finishedAt = ?, lastUpdatedAt = ? WHERE id = ?",
156
+ ["2026-08-17T16:00:00.000Z", "2026-08-17T16:00:00.000Z", prerequisite.id],
157
+ );
158
+
159
+ updateTaskVcs(prerequisite.id, {
160
+ vcsProvider: "github",
161
+ vcsRepo: "desplega-ai/agent-swarm",
162
+ vcsNumber: 1177,
163
+ vcsUrl: "https://github.com/desplega-ai/agent-swarm/pull/1177",
164
+ });
165
+
166
+ const snapshot = getQueueStallSnapshot(NOW);
167
+ expect(snapshot.oldestTaskId).toBe(blocked.id);
168
+ expect(snapshot.oldestAgeMs).toBe(60 * 60 * 1000);
169
+ expect(isQueueStalled(snapshot)).toBe(true);
170
+ });
171
+
172
+ test("excludes pending tasks whose dependencies are not complete", () => {
173
+ const worker = createAgent({ name: "worker", isLead: false, status: "idle" });
174
+ const prerequisite = createTaskExtended("Prerequisite", { agentId: worker.id });
175
+ const blocked = createTaskExtended("Blocked", {
176
+ agentId: worker.id,
177
+ dependsOn: [prerequisite.id],
178
+ });
179
+ setCreatedAt(blocked.id, "2026-08-17T12:00:00.000Z");
180
+
181
+ const snapshot = getQueueStallSnapshot(NOW);
182
+
183
+ expect(snapshot.claimableCount).toBe(1);
184
+ expect(snapshot.oldestTaskId).toBe(prerequisite.id);
185
+ });
186
+
187
+ test("reports direct and pool pickup transitions as diagnostic context", () => {
188
+ const worker = createAgent({ name: "worker", isLead: false, status: "idle" });
189
+ const task = createTaskExtended("Queued", { agentId: worker.id });
190
+ getDb().run(
191
+ `INSERT INTO agent_log (id, eventType, taskId, agentId, oldValue, newValue, createdAt)
192
+ VALUES (?, 'task_status_change', ?, ?, 'pending', 'in_progress', ?)`,
193
+ [crypto.randomUUID(), task.id, worker.id, "2026-08-17T16:50:00.000Z"],
194
+ );
195
+ getDb().run(
196
+ `INSERT INTO agent_log (id, eventType, taskId, agentId, oldValue, newValue, createdAt)
197
+ VALUES (?, 'task_claimed', ?, ?, 'unassigned', 'in_progress', ?)`,
198
+ [crypto.randomUUID(), task.id, worker.id, "2026-08-17T16:55:00.000Z"],
199
+ );
200
+
201
+ expect(getQueueStallSnapshot(NOW).recentPickupCount).toBe(2);
202
+ });
203
+
204
+ test("scrubs secrets from notification failures before logging", () => {
205
+ const token = "ghp_1234567890abcdefABCDEF1234567890ABCD";
206
+
207
+ const output = _test.formatCheckFailure(new Error(`Slack rejected token ${token}`));
208
+
209
+ expect(output).not.toContain(token);
210
+ expect(output).toContain("[REDACTED:github_token]");
211
+ });
212
+
213
+ test("deduplicates an active alarm and sends recovery", async () => {
214
+ const task = createTaskExtended("Old pool task");
215
+ setCreatedAt(task.id, "2026-08-17T16:00:00.000Z");
216
+ const notify = mock(async (_message: string) => {});
217
+
218
+ await checkQueueStall(NOW, notify);
219
+ await checkQueueStall(NOW, notify);
220
+ getDb().run("UPDATE agent_tasks SET status = 'completed' WHERE id = ?", [task.id]);
221
+ await checkQueueStall(NOW, notify);
222
+
223
+ expect(notify).toHaveBeenCalledTimes(2);
224
+ expect(notify.mock.calls[1]?.[0]).toContain("queue pickup recovered");
225
+ });
226
+ });
@@ -0,0 +1,32 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { getUserCommsPrefs } from "../utils/requester-comms";
3
+
4
+ describe("getUserCommsPrefs", () => {
5
+ test("returns undefined for missing user or metadata", () => {
6
+ expect(getUserCommsPrefs(undefined)).toBeUndefined();
7
+ expect(getUserCommsPrefs({ metadata: undefined })).toBeUndefined();
8
+ expect(getUserCommsPrefs({ metadata: {} })).toBeUndefined();
9
+ });
10
+
11
+ test("returns undefined when comms is not an object", () => {
12
+ expect(getUserCommsPrefs({ metadata: { comms: "casual" } })).toBeUndefined();
13
+ expect(getUserCommsPrefs({ metadata: { comms: ["casual"] } })).toBeUndefined();
14
+ expect(getUserCommsPrefs({ metadata: { comms: null } })).toBeUndefined();
15
+ });
16
+
17
+ test("picks trimmed string fields and drops the rest", () => {
18
+ expect(
19
+ getUserCommsPrefs({
20
+ metadata: {
21
+ comms: { tone: " casual ", language: "uk", verbosity: 3, extra: "ignored" },
22
+ },
23
+ }),
24
+ ).toEqual({ tone: "casual", language: "uk", verbosity: undefined });
25
+ });
26
+
27
+ test("returns undefined when all comms fields are empty or non-string", () => {
28
+ expect(
29
+ getUserCommsPrefs({ metadata: { comms: { tone: " ", language: 7 } } }),
30
+ ).toBeUndefined();
31
+ });
32
+ });
@@ -22,4 +22,42 @@ describe("runner requester profile prompt", () => {
22
22
  expect(prompt).toContain("Lead with the answer; keep updates terse.");
23
23
  expect(prompt).toContain("where it doesn't conflict with correctness or your operating rules");
24
24
  });
25
+
26
+ test("renders structured comms preferences alongside role and notes", async () => {
27
+ const prompt = await buildRequesterProfilePrompt({
28
+ name: "Taras",
29
+ role: "CEO",
30
+ notes: "Keep updates terse.",
31
+ comms: { tone: "casual", language: "Ukrainian", verbosity: "terse" },
32
+ });
33
+
34
+ expect(prompt).toContain(
35
+ "Their communication preferences: tone: casual, language: Ukrainian, verbosity: terse.",
36
+ );
37
+ expect(prompt).toContain("Their stated notes for how you should respond and act:");
38
+ });
39
+
40
+ test("fires when only comms preferences are set", async () => {
41
+ const prompt = await buildRequesterProfilePrompt({
42
+ name: "Taras",
43
+ comms: { language: "Ukrainian" },
44
+ });
45
+
46
+ expect(prompt).toContain("## Requester Profile");
47
+ expect(prompt).toContain("This task was requested by Taras.");
48
+ expect(prompt).toContain("Their communication preferences: language: Ukrainian.");
49
+ expect(prompt).not.toContain("Their stated notes");
50
+ });
51
+
52
+ test("hints that learned preferences should be persisted via manage-user", async () => {
53
+ const prompt = await buildRequesterProfilePrompt({ name: "Taras", role: "CEO" });
54
+
55
+ expect(prompt).toContain(
56
+ "the lead updates `comms` (tone, language, verbosity) via `manage-user`",
57
+ );
58
+ });
59
+
60
+ test("ignores empty comms object", async () => {
61
+ await expect(buildRequesterProfilePrompt({ name: "Taras", comms: {} })).resolves.toBe("");
62
+ });
25
63
  });