@vellumai/assistant 0.8.2 → 0.8.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 (231) hide show
  1. package/ARCHITECTURE.md +11 -12
  2. package/docker-entrypoint.sh +13 -1
  3. package/docker-init-apt-root.sh +79 -6
  4. package/openapi.yaml +336 -21
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
  7. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  8. package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
  9. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  10. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  11. package/src/__tests__/context-token-estimator.test.ts +30 -65
  12. package/src/__tests__/conversation-agent-loop.test.ts +57 -1
  13. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  14. package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
  15. package/src/__tests__/date-context.test.ts +45 -0
  16. package/src/__tests__/external-plugin-loader.test.ts +91 -19
  17. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  18. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  19. package/src/__tests__/heartbeat-service.test.ts +24 -164
  20. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  21. package/src/__tests__/host-app-control-proxy.test.ts +241 -0
  22. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  23. package/src/__tests__/injector-background-turn.test.ts +153 -0
  24. package/src/__tests__/injector-chain.test.ts +5 -0
  25. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
  26. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  27. package/src/__tests__/llm-catalog-parity.test.ts +3 -0
  28. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  29. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  30. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
  31. package/src/__tests__/llm-resolver.test.ts +255 -2
  32. package/src/__tests__/managed-profile-guard.test.ts +10 -0
  33. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  34. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  35. package/src/__tests__/notification-deep-link.test.ts +15 -0
  36. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  37. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  38. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  39. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  40. package/src/__tests__/openai-provider.test.ts +218 -3
  41. package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
  42. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  43. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  44. package/src/__tests__/platform-proxy-context.test.ts +6 -1
  45. package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
  46. package/src/__tests__/plugin-types.test.ts +2 -2
  47. package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
  48. package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
  49. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  50. package/src/__tests__/system-prompt.test.ts +6 -73
  51. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  52. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  53. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  54. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  55. package/src/a2a/__tests__/task-store.test.ts +246 -0
  56. package/src/a2a/agent-card.ts +58 -0
  57. package/src/a2a/feature-gate.ts +8 -0
  58. package/src/a2a/protocol-constants.ts +21 -0
  59. package/src/a2a/protocol-errors.ts +50 -0
  60. package/src/a2a/protocol-types.ts +162 -0
  61. package/src/a2a/task-store.ts +168 -0
  62. package/src/agent/loop.ts +167 -18
  63. package/src/channels/config.ts +9 -0
  64. package/src/channels/types.ts +14 -0
  65. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  66. package/src/cli/commands/__tests__/schedules.test.ts +469 -0
  67. package/src/cli/commands/notifications.ts +65 -35
  68. package/src/cli/commands/plugins.ts +67 -0
  69. package/src/cli/commands/schedules.ts +297 -5
  70. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  71. package/src/cli/lib/install-from-github.ts +8 -9
  72. package/src/cli/lib/search-plugins.ts +163 -0
  73. package/src/cli/program.ts +14 -0
  74. package/src/config/assistant-feature-flags.ts +24 -54
  75. package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
  76. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  77. package/src/config/call-site-defaults.ts +105 -0
  78. package/src/config/feature-flag-registry.json +21 -29
  79. package/src/config/llm-resolver.ts +52 -1
  80. package/src/config/schema.ts +2 -0
  81. package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
  82. package/src/config/schemas/channels.ts +9 -0
  83. package/src/config/schemas/conversations.ts +10 -0
  84. package/src/config/schemas/heartbeat.ts +14 -0
  85. package/src/config/schemas/llm.ts +1 -3
  86. package/src/config/schemas/memory-retrospective.ts +1 -1
  87. package/src/config/schemas/memory-v2.ts +4 -4
  88. package/src/config/schemas/memory.ts +3 -1
  89. package/src/config/seed-inference-profiles.ts +99 -29
  90. package/src/context/compactor.ts +72 -12
  91. package/src/context/token-estimator.ts +32 -34
  92. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
  93. package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
  94. package/src/daemon/conversation-agent-loop.ts +29 -2
  95. package/src/daemon/conversation-runtime-assembly.ts +9 -0
  96. package/src/daemon/conversation.ts +0 -7
  97. package/src/daemon/date-context.ts +40 -0
  98. package/src/daemon/guardian-action-generators.ts +1 -125
  99. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  100. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  101. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  102. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  103. package/src/daemon/handlers/config-a2a.ts +289 -0
  104. package/src/daemon/handlers/conversations.ts +1 -0
  105. package/src/daemon/host-app-control-proxy.ts +69 -18
  106. package/src/daemon/host-proxy-preactivation.ts +85 -18
  107. package/src/daemon/lifecycle.ts +49 -61
  108. package/src/daemon/memory-v2-startup.ts +49 -13
  109. package/src/daemon/message-types/notifications.ts +21 -0
  110. package/src/daemon/pkb-reminder-builder.test.ts +10 -53
  111. package/src/daemon/pkb-reminder-builder.ts +4 -19
  112. package/src/daemon/process-message.ts +3 -0
  113. package/src/daemon/skill-memory-refresh.ts +5 -1
  114. package/src/daemon/wake-target-adapter.ts +2 -0
  115. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  116. package/src/export/transcript-formatter.ts +54 -20
  117. package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
  118. package/src/heartbeat/heartbeat-service.ts +34 -191
  119. package/src/home/__tests__/feed-types.test.ts +40 -0
  120. package/src/home/feed-types.ts +14 -2
  121. package/src/ipc/cli-client.ts +147 -45
  122. package/src/memory/__tests__/conversation-queries.test.ts +220 -0
  123. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  124. package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
  125. package/src/memory/conversation-queries.ts +87 -1
  126. package/src/memory/conversation-title-service.ts +26 -4
  127. package/src/memory/db-init.ts +6 -0
  128. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
  129. package/src/memory/graph/conversation-graph-memory.ts +18 -6
  130. package/src/memory/graph/tools.ts +6 -37
  131. package/src/memory/invite-store.ts +53 -0
  132. package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
  133. package/src/memory/llm-request-log-store.ts +92 -1
  134. package/src/memory/memory-retrospective-enqueue.ts +1 -20
  135. package/src/memory/memory-retrospective-job.ts +33 -6
  136. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  137. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  138. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  139. package/src/memory/migrations/index.ts +3 -0
  140. package/src/memory/migrations/registry.ts +8 -0
  141. package/src/memory/schema/a2a.ts +15 -0
  142. package/src/memory/schema/index.ts +1 -0
  143. package/src/memory/schema/inference.ts +2 -0
  144. package/src/memory/schema/infrastructure.ts +1 -0
  145. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  146. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  147. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  148. package/src/memory/v2/__tests__/injection.test.ts +190 -3
  149. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  150. package/src/memory/v2/activation-store.ts +14 -16
  151. package/src/memory/v2/cli-command-content.ts +19 -0
  152. package/src/memory/v2/cli-command-store.ts +304 -0
  153. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  154. package/src/memory/v2/injection.ts +49 -20
  155. package/src/memory/v2/page-index.ts +38 -13
  156. package/src/memory/v2/static-context.ts +4 -4
  157. package/src/memory/v2/types.ts +23 -0
  158. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  159. package/src/messaging/providers/a2a/deliver.ts +156 -0
  160. package/src/messaging/providers/gmail/client.ts +9 -2
  161. package/src/messaging/providers/index.ts +11 -2
  162. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  163. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  164. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  165. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  166. package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
  167. package/src/notifications/adapters/macos.ts +12 -2
  168. package/src/notifications/broadcaster.ts +29 -4
  169. package/src/notifications/copy-composer.ts +17 -64
  170. package/src/notifications/decision-engine.ts +111 -44
  171. package/src/notifications/deterministic-checks.ts +96 -0
  172. package/src/notifications/emit-signal.ts +1 -0
  173. package/src/notifications/home-feed-side-effect.ts +85 -6
  174. package/src/notifications/signal.ts +0 -4
  175. package/src/notifications/types.ts +8 -0
  176. package/src/oauth/platform-connection.test.ts +43 -3
  177. package/src/oauth/platform-connection.ts +13 -4
  178. package/src/plugins/defaults/injectors.ts +38 -19
  179. package/src/plugins/external-plugin-loader.ts +82 -10
  180. package/src/plugins/types.ts +16 -7
  181. package/src/prompts/__tests__/system-prompt.test.ts +6 -51
  182. package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
  183. package/src/prompts/system-prompt.ts +0 -8
  184. package/src/prompts/templates/BOOTSTRAP.md +5 -5
  185. package/src/prompts/templates/system-sections.ts +0 -9
  186. package/src/providers/__tests__/inference.test.ts +2 -0
  187. package/src/providers/call-site-routing.ts +24 -6
  188. package/src/providers/connection-resolution.ts +63 -13
  189. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  190. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  191. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  192. package/src/providers/inference/adapter-factory.ts +9 -20
  193. package/src/providers/inference/auth.ts +12 -0
  194. package/src/providers/inference/backfill.ts +14 -1
  195. package/src/providers/inference/connections.ts +85 -5
  196. package/src/providers/inference/resolve-auth.ts +2 -0
  197. package/src/providers/model-catalog.ts +199 -244
  198. package/src/providers/model-intents.ts +3 -3
  199. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  200. package/src/providers/openai/chat-completions-provider.ts +159 -6
  201. package/src/providers/openrouter/client.ts +42 -4
  202. package/src/providers/platform-proxy/constants.ts +3 -4
  203. package/src/providers/provider-catalog-visibility.ts +3 -1
  204. package/src/providers/provider-send-message.ts +27 -12
  205. package/src/providers/registry.ts +30 -1
  206. package/src/runtime/agent-wake.ts +61 -1
  207. package/src/runtime/auth/route-policy.ts +13 -0
  208. package/src/runtime/http-server.ts +7 -16
  209. package/src/runtime/http-types.ts +0 -47
  210. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  211. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
  212. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  213. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  214. package/src/runtime/routes/channel-availability-routes.ts +5 -0
  215. package/src/runtime/routes/consolidation-routes.ts +100 -0
  216. package/src/runtime/routes/conversation-query-routes.ts +70 -11
  217. package/src/runtime/routes/conversation-routes.ts +7 -0
  218. package/src/runtime/routes/index.ts +2 -0
  219. package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
  220. package/src/runtime/routes/integrations/a2a.ts +235 -0
  221. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  222. package/src/runtime/routes/subagents-routes.ts +41 -0
  223. package/src/subagent/manager.ts +2 -0
  224. package/src/tools/memory/register.ts +1 -9
  225. package/src/tools/registry.ts +2 -2
  226. package/src/tools/types.ts +37 -2
  227. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  228. package/src/workspace/migrations/registry.ts +2 -0
  229. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  230. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  231. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
@@ -4,7 +4,7 @@ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
4
4
  // Test isolation: mock logger and IPC client
5
5
  // ---------------------------------------------------------------------------
6
6
 
7
- mock.module("../../util/logger.js", () => ({
7
+ mock.module("../../../util/logger.js", () => ({
8
8
  getLogger: () =>
9
9
  new Proxy({} as Record<string, unknown>, {
10
10
  get: () => () => {},
@@ -18,21 +18,43 @@ mock.module("../../util/logger.js", () => ({
18
18
  // Track cliIpcCall invocations and control responses
19
19
  const ipcCalls: Array<{ method: string; params?: Record<string, unknown> }> =
20
20
  [];
21
- let ipcResponse: { ok: boolean; result?: unknown; error?: string } = {
21
+ let ipcResponse: {
22
+ ok: boolean;
23
+ result?: unknown;
24
+ error?: string;
25
+ statusCode?: number;
26
+ } = {
22
27
  ok: true,
23
28
  result: {},
24
29
  };
25
30
 
26
- mock.module("../../ipc/cli-client.js", () => ({
31
+ const exitCodeFromIpcResult = (r: { statusCode?: number }): number => {
32
+ if (r.statusCode === undefined) return 10;
33
+ if (r.statusCode >= 500) return 3;
34
+ if (r.statusCode >= 400) return 2;
35
+ return 1;
36
+ };
37
+
38
+ mock.module("../../../ipc/cli-client.js", () => ({
27
39
  cliIpcCall: async (method: string, params?: Record<string, unknown>) => {
28
40
  ipcCalls.push({ method, params });
29
41
  return ipcResponse;
30
42
  },
43
+ exitFromIpcResult: (r: {
44
+ ok: false;
45
+ error?: string;
46
+ statusCode?: number;
47
+ }) => {
48
+ process.stderr.write((r.error ?? "Unknown error") + "\n");
49
+ process.exitCode = exitCodeFromIpcResult(r);
50
+ return undefined as never;
51
+ },
52
+ exitCodeFromIpcResult,
31
53
  }));
32
54
 
33
55
  import { Command } from "commander";
34
56
 
35
- import { registerNotificationsCommand } from "../commands/notifications.js";
57
+ import { registerNotificationsCommand } from "../notifications.js";
36
58
 
37
59
  // ---------------------------------------------------------------------------
38
60
  // Helpers
@@ -40,6 +62,7 @@ import { registerNotificationsCommand } from "../commands/notifications.js";
40
62
 
41
63
  interface CommandResult {
42
64
  parsed: Record<string, unknown>;
65
+ stderr: string;
43
66
  exitCode: number;
44
67
  }
45
68
 
@@ -52,7 +75,9 @@ interface CommandResult {
52
75
  */
53
76
  async function runCommand(args: string[]): Promise<CommandResult> {
54
77
  const chunks: string[] = [];
78
+ const stderrChunks: string[] = [];
55
79
  const originalWrite = process.stdout.write;
80
+ const originalStderrWrite = process.stderr.write;
56
81
 
57
82
  process.exitCode = 0;
58
83
 
@@ -61,6 +86,11 @@ async function runCommand(args: string[]): Promise<CommandResult> {
61
86
  return true;
62
87
  }) as typeof process.stdout.write;
63
88
 
89
+ process.stderr.write = ((chunk: string | Buffer) => {
90
+ stderrChunks.push(typeof chunk === "string" ? chunk : chunk.toString());
91
+ return true;
92
+ }) as typeof process.stderr.write;
93
+
64
94
  try {
65
95
  const program = new Command();
66
96
  program.exitOverride();
@@ -76,6 +106,7 @@ async function runCommand(args: string[]): Promise<CommandResult> {
76
106
  // Commander throws on .exitOverride() for --help/errors; ignore
77
107
  } finally {
78
108
  process.stdout.write = originalWrite;
109
+ process.stderr.write = originalStderrWrite;
79
110
  }
80
111
 
81
112
  const exitCode = process.exitCode ?? 0;
@@ -87,7 +118,13 @@ async function runCommand(args: string[]): Promise<CommandResult> {
87
118
  ? (JSON.parse(firstLine) as Record<string, unknown>)
88
119
  : {};
89
120
 
90
- return { parsed, exitCode };
121
+ return { parsed, stderr: stderrChunks.join(""), exitCode };
122
+ }
123
+
124
+ function lastSendBody(): Record<string, unknown> {
125
+ expect(ipcCalls).toHaveLength(1);
126
+ expect(ipcCalls[0].method).toBe("emit_notification_signal");
127
+ return ipcCalls[0].params?.body as Record<string, unknown>;
91
128
  }
92
129
 
93
130
  // ---------------------------------------------------------------------------
@@ -132,13 +169,10 @@ describe("notifications send", () => {
132
169
  expect(parsed.ok).toBe(true);
133
170
  expect(parsed.signalId).toBe("mock-id");
134
171
 
135
- expect(ipcCalls).toHaveLength(1);
136
- const call = ipcCalls[0];
137
- expect(call.method).toBe("emit_notification_signal");
138
- const callBody = call.params?.body as Record<string, unknown>;
139
- expect(callBody?.sourceChannel).toBe("assistant_tool");
140
- expect(callBody?.sourceEventName).toBe("user.send_notification");
141
- const payload = callBody?.contextPayload as Record<string, unknown>;
172
+ const body = lastSendBody();
173
+ expect(body.sourceChannel).toBe("assistant_tool");
174
+ expect(body.sourceEventName).toBe("user.send_notification");
175
+ const payload = body.contextPayload as Record<string, unknown>;
142
176
  expect(payload.requestedMessage).toBe("Hello");
143
177
  });
144
178
 
@@ -160,9 +194,7 @@ describe("notifications send", () => {
160
194
  expect(exitCode).toBe(0);
161
195
  expect(parsed.ok).toBe(true);
162
196
 
163
- expect(ipcCalls).toHaveLength(1);
164
- const emitBody = ipcCalls[0].params?.body as Record<string, unknown>;
165
- const hints = emitBody?.attentionHints as Record<string, unknown>;
197
+ const hints = lastSendBody().attentionHints as Record<string, unknown>;
166
198
  expect(hints.urgency).toBe("high");
167
199
  expect(hints.requiresAction).toBe(true);
168
200
  expect(hints.isAsyncBackground).toBe(true);
@@ -184,9 +216,7 @@ describe("notifications send", () => {
184
216
  expect(exitCode).toBe(0);
185
217
  expect(parsed.ok).toBe(true);
186
218
 
187
- expect(ipcCalls).toHaveLength(1);
188
- const dlBody = ipcCalls[0].params?.body as Record<string, unknown>;
189
- const payload = dlBody?.contextPayload as Record<string, unknown>;
219
+ const payload = lastSendBody().contextPayload as Record<string, unknown>;
190
220
  expect(payload.preferredChannels).toEqual(["telegram", "slack"]);
191
221
  });
192
222
 
@@ -230,8 +260,8 @@ describe("notifications send", () => {
230
260
  expect(exitCode).toBe(0);
231
261
  expect(parsed.ok).toBe(true);
232
262
 
233
- const callBody = ipcCalls[0].params?.body as Record<string, unknown>;
234
- expect(callBody.conversationAffinityHint).toEqual({ vellum: "conv-123" });
263
+ const body = lastSendBody();
264
+ expect(body.conversationAffinityHint).toEqual({ vellum: "conv-123" });
235
265
  });
236
266
 
237
267
  test("send omits conversationAffinityHint when --conversation-id not passed", async () => {
@@ -245,8 +275,8 @@ describe("notifications send", () => {
245
275
  "Hi",
246
276
  ]);
247
277
 
248
- const callBody = ipcCalls[0].params?.body as Record<string, unknown>;
249
- expect(callBody.conversationAffinityHint).toBeUndefined();
278
+ const body = lastSendBody();
279
+ expect(body.conversationAffinityHint).toBeUndefined();
250
280
  });
251
281
 
252
282
  test("send rejects empty --conversation-id", async () => {
@@ -268,10 +298,35 @@ describe("notifications send", () => {
268
298
  expect(ipcCalls).toHaveLength(0);
269
299
  });
270
300
 
271
- test("send surfaces IPC error response", async () => {
301
+ test("send surfaces IPC error response as JSON envelope in --json mode", async () => {
272
302
  ipcResponse = {
273
303
  ok: false,
274
- error: "Daemon rejected the signal",
304
+ error: "Could not connect to assistant daemon. Is it running?",
305
+ };
306
+
307
+ const { parsed, stderr, exitCode } = await runCommand([
308
+ "send",
309
+ "--source-channel",
310
+ "assistant_tool",
311
+ "--source-event-name",
312
+ "user.send_notification",
313
+ "--message",
314
+ "Hello",
315
+ ]);
316
+
317
+ // Transport failure (no statusCode) maps to exit 10 per exitFromIpcResult.
318
+ expect(exitCode).toBe(10);
319
+ expect(parsed.ok).toBe(false);
320
+ expect(parsed.error).toContain("Could not connect");
321
+ // --json mode keeps error on stdout envelope, not stderr.
322
+ expect(stderr).toBe("");
323
+ });
324
+
325
+ test("send maps daemon 4xx to exit 2 while preserving --json envelope", async () => {
326
+ ipcResponse = {
327
+ ok: false,
328
+ error: "Invalid signal payload",
329
+ statusCode: 422,
275
330
  };
276
331
 
277
332
  const { parsed, exitCode } = await runCommand([
@@ -284,9 +339,96 @@ describe("notifications send", () => {
284
339
  "Hello",
285
340
  ]);
286
341
 
287
- expect(exitCode).toBe(1);
342
+ expect(exitCode).toBe(2);
288
343
  expect(parsed.ok).toBe(false);
289
- expect(parsed.error).toBe("Daemon rejected the signal");
344
+ expect(parsed.error).toBe("Invalid signal payload");
345
+ });
346
+ });
347
+
348
+ // ---------------------------------------------------------------------------
349
+ // send — minimal-surface ergonomics (--urgent and source defaults)
350
+ // ---------------------------------------------------------------------------
351
+
352
+ describe("notifications send — minimal-surface ergonomics", () => {
353
+ test("--urgent maps to urgency=critical + requiresAction=true", async () => {
354
+ const { exitCode } = await runCommand([
355
+ "send",
356
+ "--message",
357
+ "Pager: prod is down",
358
+ "--urgent",
359
+ ]);
360
+
361
+ expect(exitCode).toBe(0);
362
+ const hints = lastSendBody().attentionHints as Record<string, unknown>;
363
+ expect(hints.urgency).toBe("critical");
364
+ expect(hints.requiresAction).toBe(true);
365
+ });
366
+
367
+ test("missing --source-channel defaults to 'assistant_tool'", async () => {
368
+ const { exitCode } = await runCommand(["send", "--message", "hello"]);
369
+
370
+ expect(exitCode).toBe(0);
371
+ const body = lastSendBody();
372
+ expect(body.sourceChannel).toBe("assistant_tool");
373
+ // The context payload echoes the source channel via requestedBySource.
374
+ const payload = body.contextPayload as Record<string, unknown>;
375
+ expect(payload.requestedBySource).toBe("assistant_tool");
376
+ });
377
+
378
+ test("missing --source-event-name defaults to 'assistant.share'", async () => {
379
+ const { exitCode } = await runCommand(["send", "--message", "hello"]);
380
+
381
+ expect(exitCode).toBe(0);
382
+ expect(lastSendBody().sourceEventName).toBe("assistant.share");
383
+ });
384
+
385
+ test("explicit --urgency high still overrides defaults when --urgent is absent", async () => {
386
+ const { exitCode } = await runCommand([
387
+ "send",
388
+ "--message",
389
+ "stand-up reminder",
390
+ "--urgency",
391
+ "high",
392
+ ]);
393
+
394
+ expect(exitCode).toBe(0);
395
+ const hints = lastSendBody().attentionHints as Record<string, unknown>;
396
+ expect(hints.urgency).toBe("high");
397
+ // Without --urgent or --requires-action, requiresAction stays at the new
398
+ // default of false.
399
+ expect(hints.requiresAction).toBe(false);
400
+ });
401
+
402
+ test("explicit --urgency wins even when --urgent is also passed (back-compat)", async () => {
403
+ const { exitCode } = await runCommand([
404
+ "send",
405
+ "--message",
406
+ "deploy complete",
407
+ "--urgent",
408
+ "--urgency",
409
+ "medium",
410
+ ]);
411
+
412
+ expect(exitCode).toBe(0);
413
+ const hints = lastSendBody().attentionHints as Record<string, unknown>;
414
+ expect(hints.urgency).toBe("medium");
415
+ // --urgent still flips requiresAction since no explicit flag was passed.
416
+ expect(hints.requiresAction).toBe(true);
417
+ });
418
+
419
+ test("explicit --no-requires-action wins even when --urgent is passed", async () => {
420
+ const { exitCode } = await runCommand([
421
+ "send",
422
+ "--message",
423
+ "fyi only",
424
+ "--urgent",
425
+ "--no-requires-action",
426
+ ]);
427
+
428
+ expect(exitCode).toBe(0);
429
+ const hints = lastSendBody().attentionHints as Record<string, unknown>;
430
+ expect(hints.urgency).toBe("critical");
431
+ expect(hints.requiresAction).toBe(false);
290
432
  });
291
433
  });
292
434
 
@@ -342,7 +484,9 @@ describe("notifications list", () => {
342
484
  expect(parsed.ok).toBe(true);
343
485
 
344
486
  expect(ipcCalls).toHaveLength(1);
345
- expect((ipcCalls[0].params?.body as Record<string, unknown>)?.limit).toBe(5);
487
+ expect((ipcCalls[0].params?.body as Record<string, unknown>)?.limit).toBe(
488
+ 5,
489
+ );
346
490
  });
347
491
 
348
492
  test("list passes --source-event-name to IPC", async () => {
@@ -371,8 +515,37 @@ describe("notifications list", () => {
371
515
 
372
516
  const { parsed, exitCode } = await runCommand(["list"]);
373
517
 
374
- expect(exitCode).toBe(1);
518
+ // Transport failure (no statusCode) maps to exit 10 per exitCodeFromIpcResult.
519
+ expect(exitCode).toBe(10);
375
520
  expect(parsed.ok).toBe(false);
376
521
  expect(parsed.error).toContain("Could not connect");
377
522
  });
523
+
524
+ test("list maps daemon 4xx to exit 2", async () => {
525
+ ipcResponse = {
526
+ ok: false,
527
+ error: "Invalid limit",
528
+ statusCode: 400,
529
+ };
530
+
531
+ const { parsed, exitCode } = await runCommand(["list"]);
532
+
533
+ expect(exitCode).toBe(2);
534
+ expect(parsed.ok).toBe(false);
535
+ expect(parsed.error).toBe("Invalid limit");
536
+ });
537
+
538
+ test("list maps daemon 5xx to exit 3", async () => {
539
+ ipcResponse = {
540
+ ok: false,
541
+ error: "Internal daemon error",
542
+ statusCode: 500,
543
+ };
544
+
545
+ const { parsed, exitCode } = await runCommand(["list"]);
546
+
547
+ expect(exitCode).toBe(3);
548
+ expect(parsed.ok).toBe(false);
549
+ expect(parsed.error).toBe("Internal daemon error");
550
+ });
378
551
  });