@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
@@ -99,6 +99,11 @@ describe("schedules command", () => {
99
99
  expect(schedules!.commands.map((command) => command.name())).toEqual([
100
100
  "list",
101
101
  "runs",
102
+ "create",
103
+ "enable",
104
+ "disable",
105
+ "cancel",
106
+ "delete",
102
107
  "execute",
103
108
  ]);
104
109
  });
@@ -361,6 +366,470 @@ describe("schedules runs", () => {
361
366
  });
362
367
  });
363
368
 
369
+ describe("schedules create", () => {
370
+ test("calls createSchedule with the required fields", async () => {
371
+ mockIpcResult = { ok: true, result: { schedules: [] } };
372
+
373
+ const { exitCode } = await runCommand([
374
+ "schedules",
375
+ "create",
376
+ "Heartbeat",
377
+ "--expression",
378
+ "*/30 * * * *",
379
+ "--message",
380
+ "run heartbeat",
381
+ ]);
382
+
383
+ expect(exitCode).toBe(0);
384
+ expect(ipcCalls).toEqual([
385
+ {
386
+ method: "createSchedule",
387
+ params: {
388
+ body: {
389
+ name: "Heartbeat",
390
+ expression: "*/30 * * * *",
391
+ message: "run heartbeat",
392
+ enabled: true,
393
+ },
394
+ },
395
+ },
396
+ ]);
397
+ expect(logLines).toEqual(["Created schedule: Heartbeat"]);
398
+ });
399
+
400
+ test("passes --timezone through to the request body", async () => {
401
+ mockIpcResult = { ok: true, result: { schedules: [] } };
402
+
403
+ const { exitCode } = await runCommand([
404
+ "schedules",
405
+ "create",
406
+ "Morning",
407
+ "--expression",
408
+ "0 9 * * MON-FRI",
409
+ "--message",
410
+ "morning summary",
411
+ "--timezone",
412
+ "America/New_York",
413
+ ]);
414
+
415
+ expect(exitCode).toBe(0);
416
+ expect(ipcCalls).toEqual([
417
+ {
418
+ method: "createSchedule",
419
+ params: {
420
+ body: {
421
+ name: "Morning",
422
+ expression: "0 9 * * MON-FRI",
423
+ message: "morning summary",
424
+ enabled: true,
425
+ timezone: "America/New_York",
426
+ },
427
+ },
428
+ },
429
+ ]);
430
+ });
431
+
432
+ test("sends enabled:false when --no-enabled is set", async () => {
433
+ mockIpcResult = { ok: true, result: { schedules: [] } };
434
+
435
+ const { exitCode } = await runCommand([
436
+ "schedules",
437
+ "create",
438
+ "Drafted",
439
+ "--expression",
440
+ "0 0 * * *",
441
+ "--message",
442
+ "placeholder",
443
+ "--no-enabled",
444
+ ]);
445
+
446
+ expect(exitCode).toBe(0);
447
+ expect(ipcCalls).toEqual([
448
+ {
449
+ method: "createSchedule",
450
+ params: {
451
+ body: {
452
+ name: "Drafted",
453
+ expression: "0 0 * * *",
454
+ message: "placeholder",
455
+ enabled: false,
456
+ },
457
+ },
458
+ },
459
+ ]);
460
+ });
461
+
462
+ test("emits JSON when --json is set", async () => {
463
+ mockIpcResult = {
464
+ ok: true,
465
+ result: {
466
+ schedules: [
467
+ {
468
+ id: "new-schedule-id",
469
+ name: "Heartbeat",
470
+ enabled: true,
471
+ syntax: "cron",
472
+ expression: "*/30 * * * *",
473
+ cronExpression: "*/30 * * * *",
474
+ timezone: null,
475
+ message: "run heartbeat",
476
+ script: null,
477
+ nextRunAt: 1_778_800_000_000,
478
+ lastRunAt: null,
479
+ lastStatus: null,
480
+ retryCount: 0,
481
+ maxRetries: 3,
482
+ retryBackoffMs: 60_000,
483
+ description: "Every 30 minutes",
484
+ mode: "execute",
485
+ status: "active",
486
+ routingIntent: "all_channels",
487
+ reuseConversation: false,
488
+ wakeConversationId: null,
489
+ isOneShot: false,
490
+ },
491
+ ],
492
+ },
493
+ };
494
+
495
+ const { stdout, exitCode } = await runCommand([
496
+ "schedules",
497
+ "create",
498
+ "Heartbeat",
499
+ "--expression",
500
+ "*/30 * * * *",
501
+ "--message",
502
+ "run heartbeat",
503
+ "--json",
504
+ ]);
505
+
506
+ expect(exitCode).toBe(0);
507
+ expect(JSON.parse(stdout)).toEqual({
508
+ schedules: [expect.objectContaining({ id: "new-schedule-id" })],
509
+ });
510
+ expect(logLines).toEqual([]);
511
+ });
512
+
513
+ test("routes IPC failure through exitFromIpcResult", async () => {
514
+ mockIpcResult = {
515
+ ok: false,
516
+ error: "expression could not be parsed as cron or rrule",
517
+ };
518
+
519
+ const { exitCode } = await runCommand([
520
+ "schedules",
521
+ "create",
522
+ "Bad",
523
+ "--expression",
524
+ "not-a-cron",
525
+ "--message",
526
+ "noop",
527
+ ]);
528
+
529
+ expect(exitCode).toBe(10);
530
+ expect(exitFromIpcResultCalls).toEqual([mockIpcResult]);
531
+ expect(errorLines).toEqual([]);
532
+ });
533
+ });
534
+
535
+ describe("schedules enable/disable", () => {
536
+ test("enable calls toggleSchedule with enabled true", async () => {
537
+ mockIpcResult = { ok: true, result: { schedules: [] } };
538
+
539
+ const { exitCode } = await runCommand([
540
+ "schedules",
541
+ "enable",
542
+ "schedule-1",
543
+ ]);
544
+
545
+ expect(exitCode).toBe(0);
546
+ expect(ipcCalls).toEqual([
547
+ {
548
+ method: "toggleSchedule",
549
+ params: {
550
+ pathParams: { id: "schedule-1" },
551
+ body: { enabled: true },
552
+ },
553
+ },
554
+ ]);
555
+ expect(logLines).toEqual(["Enabled schedule: schedule-1"]);
556
+ });
557
+
558
+ test("disable calls toggleSchedule with enabled false", async () => {
559
+ mockIpcResult = { ok: true, result: { schedules: [] } };
560
+
561
+ const { exitCode } = await runCommand([
562
+ "schedules",
563
+ "disable",
564
+ "schedule-1",
565
+ ]);
566
+
567
+ expect(exitCode).toBe(0);
568
+ expect(ipcCalls).toEqual([
569
+ {
570
+ method: "toggleSchedule",
571
+ params: {
572
+ pathParams: { id: "schedule-1" },
573
+ body: { enabled: false },
574
+ },
575
+ },
576
+ ]);
577
+ expect(logLines).toEqual(["Disabled schedule: schedule-1"]);
578
+ });
579
+
580
+ test("emits JSON result when --json is set", async () => {
581
+ mockIpcResult = {
582
+ ok: true,
583
+ result: {
584
+ schedules: [
585
+ {
586
+ id: "schedule-1",
587
+ name: "Heartbeat",
588
+ enabled: false,
589
+ syntax: "cron",
590
+ expression: "*/30 * * * *",
591
+ cronExpression: "*/30 * * * *",
592
+ timezone: "UTC",
593
+ message: "run heartbeat",
594
+ script: null,
595
+ nextRunAt: 1_778_800_000_000,
596
+ lastRunAt: null,
597
+ lastStatus: "ok",
598
+ retryCount: 0,
599
+ maxRetries: 3,
600
+ retryBackoffMs: 60_000,
601
+ description: "Every 30 minutes",
602
+ mode: "execute",
603
+ status: "active",
604
+ routingIntent: "all_channels",
605
+ reuseConversation: false,
606
+ wakeConversationId: null,
607
+ isOneShot: false,
608
+ },
609
+ ],
610
+ },
611
+ };
612
+
613
+ const { stdout, exitCode } = await runCommand([
614
+ "schedules",
615
+ "disable",
616
+ "schedule-1",
617
+ "--json",
618
+ ]);
619
+
620
+ expect(exitCode).toBe(0);
621
+ expect(JSON.parse(stdout)).toEqual({
622
+ schedules: [
623
+ expect.objectContaining({ id: "schedule-1", enabled: false }),
624
+ ],
625
+ });
626
+ expect(logLines).toEqual([]);
627
+ });
628
+
629
+ test("routes IPC failure through exitFromIpcResult", async () => {
630
+ mockIpcResult = { ok: false, error: "Schedule not found" };
631
+
632
+ const { exitCode } = await runCommand([
633
+ "schedules",
634
+ "enable",
635
+ "missing-schedule",
636
+ ]);
637
+
638
+ expect(exitCode).toBe(10);
639
+ expect(exitFromIpcResultCalls).toEqual([mockIpcResult]);
640
+ expect(errorLines).toEqual([]);
641
+ });
642
+ });
643
+
644
+ describe("schedules cancel", () => {
645
+ test("calls cancelSchedule with the schedule ID path param", async () => {
646
+ mockIpcResult = { ok: true, result: { schedules: [] } };
647
+
648
+ const { exitCode } = await runCommand([
649
+ "schedules",
650
+ "cancel",
651
+ "schedule-1",
652
+ ]);
653
+
654
+ expect(exitCode).toBe(0);
655
+ expect(ipcCalls).toEqual([
656
+ {
657
+ method: "cancelSchedule",
658
+ params: { pathParams: { id: "schedule-1" } },
659
+ },
660
+ ]);
661
+ expect(logLines).toEqual(["Cancelled schedule: schedule-1"]);
662
+ });
663
+
664
+ test("emits JSON result when --json is set", async () => {
665
+ mockIpcResult = {
666
+ ok: true,
667
+ result: {
668
+ schedules: [
669
+ {
670
+ id: "remaining-schedule",
671
+ name: "Heartbeat",
672
+ enabled: true,
673
+ syntax: "cron",
674
+ expression: "*/30 * * * *",
675
+ cronExpression: "*/30 * * * *",
676
+ timezone: "UTC",
677
+ message: "run heartbeat",
678
+ script: null,
679
+ nextRunAt: 1_778_800_000_000,
680
+ lastRunAt: null,
681
+ lastStatus: "ok",
682
+ retryCount: 0,
683
+ maxRetries: 3,
684
+ retryBackoffMs: 60_000,
685
+ description: "Every 30 minutes",
686
+ mode: "execute",
687
+ status: "active",
688
+ routingIntent: "all_channels",
689
+ reuseConversation: false,
690
+ wakeConversationId: null,
691
+ isOneShot: false,
692
+ },
693
+ ],
694
+ },
695
+ };
696
+
697
+ const { stdout, exitCode } = await runCommand([
698
+ "schedules",
699
+ "cancel",
700
+ "schedule-1",
701
+ "--json",
702
+ ]);
703
+
704
+ expect(exitCode).toBe(0);
705
+ expect(JSON.parse(stdout)).toEqual({
706
+ schedules: [expect.objectContaining({ id: "remaining-schedule" })],
707
+ });
708
+ expect(logLines).toEqual([]);
709
+ });
710
+
711
+ test("routes IPC failure through exitFromIpcResult", async () => {
712
+ mockIpcResult = {
713
+ ok: false,
714
+ error: "Schedule not found or not cancellable",
715
+ };
716
+
717
+ const { exitCode } = await runCommand([
718
+ "schedules",
719
+ "cancel",
720
+ "missing-schedule",
721
+ ]);
722
+
723
+ expect(exitCode).toBe(10);
724
+ expect(exitFromIpcResultCalls).toEqual([mockIpcResult]);
725
+ expect(errorLines).toEqual([]);
726
+ });
727
+ });
728
+
729
+ describe("schedules delete", () => {
730
+ test("calls deleteSchedule with the schedule ID path param when --force is set", async () => {
731
+ mockIpcResult = { ok: true, result: { schedules: [] } };
732
+
733
+ const { exitCode } = await runCommand([
734
+ "schedules",
735
+ "delete",
736
+ "schedule-1",
737
+ "--force",
738
+ ]);
739
+
740
+ expect(exitCode).toBe(0);
741
+ expect(ipcCalls).toEqual([
742
+ {
743
+ method: "deleteSchedule",
744
+ params: { pathParams: { id: "schedule-1" } },
745
+ },
746
+ ]);
747
+ expect(logLines).toEqual(["Deleted schedule: schedule-1"]);
748
+ });
749
+
750
+ test("emits JSON result when --json is set with --force", async () => {
751
+ mockIpcResult = {
752
+ ok: true,
753
+ result: {
754
+ schedules: [
755
+ {
756
+ id: "remaining-schedule",
757
+ name: "Heartbeat",
758
+ enabled: true,
759
+ syntax: "cron",
760
+ expression: "*/30 * * * *",
761
+ cronExpression: "*/30 * * * *",
762
+ timezone: "UTC",
763
+ message: "run heartbeat",
764
+ script: null,
765
+ nextRunAt: 1_778_800_000_000,
766
+ lastRunAt: null,
767
+ lastStatus: "ok",
768
+ retryCount: 0,
769
+ maxRetries: 3,
770
+ retryBackoffMs: 60_000,
771
+ description: "Every 30 minutes",
772
+ mode: "execute",
773
+ status: "active",
774
+ routingIntent: "all_channels",
775
+ reuseConversation: false,
776
+ wakeConversationId: null,
777
+ isOneShot: false,
778
+ },
779
+ ],
780
+ },
781
+ };
782
+
783
+ const { stdout, exitCode } = await runCommand([
784
+ "schedules",
785
+ "delete",
786
+ "schedule-1",
787
+ "--force",
788
+ "--json",
789
+ ]);
790
+
791
+ expect(exitCode).toBe(0);
792
+ expect(JSON.parse(stdout)).toEqual({
793
+ schedules: [expect.objectContaining({ id: "remaining-schedule" })],
794
+ });
795
+ expect(logLines).toEqual([]);
796
+ });
797
+
798
+ test("routes IPC failure through exitFromIpcResult", async () => {
799
+ mockIpcResult = {
800
+ ok: false,
801
+ error: "Schedule not found",
802
+ };
803
+
804
+ const { exitCode } = await runCommand([
805
+ "schedules",
806
+ "delete",
807
+ "missing-schedule",
808
+ "--force",
809
+ ]);
810
+
811
+ expect(exitCode).toBe(10);
812
+ expect(exitFromIpcResultCalls).toEqual([mockIpcResult]);
813
+ expect(errorLines).toEqual([]);
814
+ });
815
+
816
+ test("refuses to delete non-interactively without --force", async () => {
817
+ // bun's test runner attaches a non-TTY stdin, so confirmPrompt takes the
818
+ // non-interactive branch and the IPC is never invoked. This locks in the
819
+ // safety guarantee that scripts must opt in via --force.
820
+ mockIpcResult = { ok: true, result: { schedules: [] } };
821
+
822
+ const { exitCode } = await runCommand([
823
+ "schedules",
824
+ "delete",
825
+ "schedule-1",
826
+ ]);
827
+
828
+ expect(exitCode).toBe(1);
829
+ expect(ipcCalls).toEqual([]);
830
+ });
831
+ });
832
+
364
833
  describe("schedules execute", () => {
365
834
  test("calls runScheduleNow with the schedule ID path param", async () => {
366
835
  mockIpcResult = {
@@ -1,6 +1,10 @@
1
1
  import type { Command } from "commander";
2
2
 
3
- import { cliIpcCall } from "../../ipc/cli-client.js";
3
+ import {
4
+ cliIpcCall,
5
+ exitCodeFromIpcResult,
6
+ exitFromIpcResult,
7
+ } from "../../ipc/cli-client.js";
4
8
  import { registerCommand } from "../lib/register-command.js";
5
9
  import { log } from "../logger.js";
6
10
  import { shouldOutputJson, writeOutput } from "../output.js";
@@ -26,11 +30,14 @@ source channel, event name, and attention hints. The decision engine evaluates
26
30
  whether and where to deliver the notification based on connected channels,
27
31
  urgency, and user preferences.
28
32
 
33
+ Minimal usage: only --message is required. Add --urgent for a push + visual
34
+ flag in the inbox. Source channel/event name fall back to assistant_tool /
35
+ assistant.share when omitted.
36
+
29
37
  Examples:
30
- $ assistant notifications send --source-channel assistant_tool --source-event-name user.send_notification --message "Build finished"
31
- $ assistant notifications send --source-channel scheduler --source-event-name schedule.notify --message "Stand-up in 5 minutes" --urgency high
32
- $ assistant notifications send --source-channel watcher --source-event-name watcher.notification --message "File changed" --no-requires-action --is-async-background
33
- $ assistant notifications send --source-channel assistant_tool --source-event-name user.send_notification --message "Deploy complete" --preferred-channels vellum,telegram --json`,
38
+ $ assistant notifications send --message "Build finished"
39
+ $ assistant notifications send --message "Pager: prod is down" --urgent
40
+ $ assistant notifications send --message "Build green" --conversation-id 649c4645-3a6f-4ded-a713-504f02ca806b`,
34
41
  );
35
42
 
36
43
  // -------------------------------------------------------------------------
@@ -40,28 +47,33 @@ Examples:
40
47
  notifications
41
48
  .command("send")
42
49
  .description(
43
- "Send a notification through the unified notification router",
50
+ "Send a notification through the unified notification router. Only --message is required; pass --urgent for a push + visual flag.",
44
51
  )
45
52
  .requiredOption(
53
+ "--message <message>",
54
+ "Notification message the user should receive",
55
+ )
56
+ .option(
57
+ "--urgent",
58
+ "Mark this notification as urgent (fires push + visual flag in inbox)",
59
+ false,
60
+ )
61
+ .option(
46
62
  "--source-channel <channel>",
47
- "Source channel producing this notification",
63
+ "Source channel producing this notification (default: assistant_tool)",
48
64
  )
49
- .requiredOption(
65
+ .option(
50
66
  "--source-event-name <name>",
51
- "Event name for audit, routing, and dedupe grouping",
52
- )
53
- .requiredOption(
54
- "--message <message>",
55
- "Notification message the user should receive",
67
+ "Event name for audit, routing, and dedupe grouping (default: assistant.share)",
56
68
  )
57
69
  .option("--title <title>", "Optional notification title")
58
70
  .option(
59
71
  "--urgency <urgency>",
60
- "Urgency hint: low, medium, high, critical (default: medium)",
72
+ "Urgency hint: low, medium, high, critical (default: low; use --urgent for critical)",
61
73
  )
62
74
  .option(
63
75
  "--requires-action",
64
- "Whether the notification expects user action (default: true)",
76
+ "Whether the notification expects user action (default: false; use --urgent to force true)",
65
77
  )
66
78
  .option(
67
79
  "--no-requires-action",
@@ -111,15 +123,16 @@ Examples:
111
123
  "after",
112
124
  `
113
125
  Arguments:
114
- --source-channel One of the registered source channels (see "assistant notifications --help")
115
- --source-event-name One of the registered event names (see "assistant notifications --help")
116
126
  --message The notification body text (required, must be non-empty)
127
+ --urgent Shortcut that maps to urgency=critical + requires-action=true
117
128
 
118
129
  Behavioral notes:
119
130
  - The signal is emitted through the full notification pipeline: event store,
120
131
  decision engine, deterministic checks, and channel dispatch.
121
- - --requires-action defaults to true; use --no-requires-action to disable.
122
- - --urgency defaults to medium if not specified.
132
+ - --urgent overrides --urgency and --requires-action defaults so the signal
133
+ is treated as critical and requires user action. Explicit --urgency /
134
+ --requires-action flags still win for back-compat.
135
+ - Without --urgent, --urgency defaults to low and --requires-action to false.
123
136
  - --preferred-channels are hints only; the decision engine may override them.
124
137
  - --dedupe-key suppresses duplicate signals with the same key.
125
138
  - --conversation-id pins delivery to an existing vellum conversation
@@ -127,20 +140,20 @@ Behavioral notes:
127
140
  binding-based pairing for their external threads.
128
141
 
129
142
  Examples:
130
- $ assistant notifications send --source-channel assistant_tool --source-event-name user.send_notification --message "Task complete"
131
- $ assistant notifications send --source-channel scheduler --source-event-name schedule.notify --message "Meeting in 5 min" --urgency high --title "Reminder"
132
- $ assistant notifications send --source-channel watcher --source-event-name watcher.notification --message "Detected change" --no-requires-action --is-async-background --json
133
- $ assistant notifications send --source-channel assistant_tool --source-event-name user.send_notification --message "Build green" --conversation-id 649c4645-3a6f-4ded-a713-504f02ca806b`,
143
+ $ assistant notifications send --message "Task complete"
144
+ $ assistant notifications send --message "Pager: prod is down" --urgent
145
+ $ assistant notifications send --message "Build green" --conversation-id 649c4645-3a6f-4ded-a713-504f02ca806b`,
134
146
  )
135
147
  .action(
136
148
  async (
137
149
  opts: {
138
- sourceChannel: string;
139
- sourceEventName: string;
150
+ sourceChannel?: string;
151
+ sourceEventName?: string;
140
152
  message: string;
153
+ urgent: boolean;
141
154
  title?: string;
142
155
  urgency?: string;
143
- requiresAction: boolean;
156
+ requiresAction?: boolean;
144
157
  isAsyncBackground: boolean;
145
158
  visibleInSourceNow: boolean;
146
159
  deadlineAt?: string;
@@ -153,6 +166,11 @@ Examples:
153
166
  cmd: Command,
154
167
  ) => {
155
168
  try {
169
+ // Apply defaults for optional source fields (minimal-surface
170
+ // ergonomics; explicit values from the CLI still win).
171
+ const sourceChannel = opts.sourceChannel ?? "assistant_tool";
172
+ const sourceEventName = opts.sourceEventName ?? "assistant.share";
173
+
156
174
  // Validate --message (keep basic validation for immediate CLI feedback)
157
175
  const message = opts.message.trim();
158
176
  if (message.length === 0) {
@@ -164,8 +182,15 @@ Examples:
164
182
  return;
165
183
  }
166
184
 
185
+ // --urgent is a shortcut for urgency=critical + requiresAction=true.
186
+ // Explicit --urgency / --requires-action flags still win so the
187
+ // back-compat path keeps working during the deprecation window.
188
+ const urgentDefaults = opts.urgent
189
+ ? { urgency: "critical", requiresAction: true }
190
+ : { urgency: "low", requiresAction: false };
191
+
167
192
  // Validate --urgency
168
- const urgency = opts.urgency ?? "medium";
193
+ const urgency = opts.urgency ?? urgentDefaults.urgency;
169
194
  if (
170
195
  urgency !== "low" &&
171
196
  urgency !== "medium" &&
@@ -179,6 +204,8 @@ Examples:
179
204
  process.exitCode = 1;
180
205
  return;
181
206
  }
207
+ const requiresAction =
208
+ opts.requiresAction ?? urgentDefaults.requiresAction;
182
209
 
183
210
  // Parse --deadline-at
184
211
  let deadlineAt: number | undefined;
@@ -241,11 +268,11 @@ Examples:
241
268
  reason: string;
242
269
  }>("emit_notification_signal", {
243
270
  body: {
244
- sourceChannel: opts.sourceChannel,
245
- sourceEventName: opts.sourceEventName,
271
+ sourceChannel,
272
+ sourceEventName,
246
273
  sourceContextId,
247
274
  attentionHints: {
248
- requiresAction: opts.requiresAction ?? true,
275
+ requiresAction,
249
276
  urgency,
250
277
  deadlineAt,
251
278
  isAsyncBackground: opts.isAsyncBackground ?? false,
@@ -253,7 +280,7 @@ Examples:
253
280
  },
254
281
  contextPayload: {
255
282
  requestedMessage: message,
256
- requestedBySource: opts.sourceChannel,
283
+ requestedBySource: sourceChannel,
257
284
  ...(opts.title ? { requestedTitle: opts.title } : {}),
258
285
  ...(preferredChannels?.length ? { preferredChannels } : {}),
259
286
  ...(deepLinkMetadata ? { deepLinkMetadata } : {}),
@@ -267,9 +294,12 @@ Examples:
267
294
  });
268
295
 
269
296
  if (!result.ok) {
270
- writeOutput(cmd, { ok: false, error: result.error });
271
- process.exitCode = 1;
272
- return;
297
+ if (shouldOutputJson(cmd)) {
298
+ writeOutput(cmd, { ok: false, error: result.error });
299
+ process.exitCode = exitCodeFromIpcResult(result);
300
+ return;
301
+ }
302
+ return exitFromIpcResult(result);
273
303
  }
274
304
 
275
305
  const signal = result.result!;
@@ -382,7 +412,7 @@ Examples:
382
412
 
383
413
  if (!result.ok) {
384
414
  writeOutput(cmd, { ok: false, error: result.error });
385
- process.exitCode = 1;
415
+ process.exitCode = exitCodeFromIpcResult(result);
386
416
  return;
387
417
  }
388
418