@vellumai/assistant 0.9.0 → 0.9.1-staging.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 (222) hide show
  1. package/ARCHITECTURE.md +18 -34
  2. package/bun.lock +7 -8
  3. package/docs/activation-funnel-telemetry.md +4 -4
  4. package/docs/architecture/security.md +29 -28
  5. package/docs/stt-provider-onboarding.md +3 -5
  6. package/docs/workflows-testing.md +13 -44
  7. package/docs/workflows.md +3 -5
  8. package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
  9. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
  10. package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
  11. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -6
  12. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
  13. package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
  14. package/openapi.yaml +135 -59
  15. package/package.json +2 -1
  16. package/scripts/sync-llm-catalog.ts +6 -15
  17. package/scripts/sync-web-search-catalog.ts +3 -11
  18. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +14 -26
  19. package/src/__tests__/agent-loop-compaction-strip.test.ts +240 -0
  20. package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
  21. package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
  22. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
  23. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  24. package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
  25. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
  26. package/src/__tests__/avatar-identity-sync.test.ts +2 -27
  27. package/src/__tests__/btw-routes.test.ts +6 -8
  28. package/src/__tests__/checker.test.ts +0 -3
  29. package/src/__tests__/config-loader-backfill.test.ts +103 -6
  30. package/src/__tests__/config-watcher.test.ts +0 -18
  31. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
  32. package/src/__tests__/credential-broker.test.ts +449 -1
  33. package/src/__tests__/credential-execution-tools.test.ts +0 -1
  34. package/src/__tests__/credential-prompt-route.test.ts +3 -4
  35. package/src/__tests__/credential-routes.test.ts +360 -0
  36. package/src/__tests__/credential-security-invariants.test.ts +2 -13
  37. package/src/__tests__/dynamic-page-surface.test.ts +101 -1
  38. package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
  39. package/src/__tests__/gateway-only-guard.test.ts +3 -7
  40. package/src/__tests__/identity-routes.test.ts +0 -189
  41. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  42. package/src/__tests__/invite-redemption-service.test.ts +4 -4
  43. package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
  44. package/src/__tests__/llm-catalog-parity.test.ts +0 -22
  45. package/src/__tests__/llm-resolver.test.ts +49 -24
  46. package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
  47. package/src/__tests__/onboarding-persona-write.test.ts +1 -1
  48. package/src/__tests__/persona-resolver.test.ts +11 -14
  49. package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
  50. package/src/__tests__/registry.test.ts +2 -7
  51. package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
  52. package/src/__tests__/schedule-routes.test.ts +0 -30
  53. package/src/__tests__/schedule-tools.test.ts +2 -18
  54. package/src/__tests__/skill-execute-input.test.ts +46 -1
  55. package/src/__tests__/skill-runtime-path.test.ts +2 -3
  56. package/src/__tests__/subagent-tools.test.ts +116 -0
  57. package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
  58. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
  59. package/src/__tests__/token-manager.test.ts +519 -0
  60. package/src/__tests__/tool-executor.test.ts +0 -79
  61. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  62. package/src/__tests__/trusted-contact-verification.test.ts +6 -6
  63. package/src/__tests__/voice-invite-redemption.test.ts +2 -2
  64. package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
  65. package/src/__tests__/workspace-greetings.test.ts +152 -0
  66. package/src/agent/loop.ts +25 -5
  67. package/src/api/README.md +6 -6
  68. package/src/api/responses/conversation-message.ts +2 -4
  69. package/src/api/responses/home.ts +0 -4
  70. package/src/approvals/guardian-request-resolvers.ts +2 -2
  71. package/src/calls/relay-access-wait.ts +1 -1
  72. package/src/calls/voice-session-bridge.ts +2 -2
  73. package/src/cli/commands/plugins.ts +143 -2
  74. package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
  75. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +313 -0
  76. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +253 -2
  77. package/src/cli/lib/diff-plugin.ts +346 -0
  78. package/src/cli/lib/install-from-github.ts +105 -17
  79. package/src/cli/lib/merge-plugin-tree.ts +228 -0
  80. package/src/cli/lib/plugin-fingerprint.ts +14 -0
  81. package/src/cli/lib/upgrade-plugin.ts +270 -10
  82. package/src/cli/program.ts +0 -2
  83. package/src/config/bundled-skills/subagent/SKILL.md +4 -0
  84. package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
  85. package/src/config/bundled-skills/workflows/SKILL.md +0 -1
  86. package/src/config/bundled-tool-registry.ts +2 -7
  87. package/src/config/call-site-defaults.ts +12 -2
  88. package/src/config/feature-flag-registry.json +1 -17
  89. package/src/config/inference-profile-validation.ts +26 -0
  90. package/src/config/loader.ts +4 -0
  91. package/src/config/profile-order.ts +28 -0
  92. package/src/config/schemas/elevenlabs.ts +0 -1
  93. package/src/config/schemas/platform.ts +0 -8
  94. package/src/config/seed-inference-profiles.ts +25 -20
  95. package/src/contacts/contact-store.ts +87 -96
  96. package/src/contacts/contacts-write.ts +5 -21
  97. package/src/context/compactor.ts +2 -2
  98. package/src/credential-execution/process-manager.ts +55 -14
  99. package/src/credential-execution/prompted-credential.ts +2 -3
  100. package/src/daemon/config-watcher.ts +0 -4
  101. package/src/daemon/conversation-agent-loop.ts +15 -4
  102. package/src/daemon/conversation-slash.ts +2 -23
  103. package/src/daemon/conversation-tool-setup.ts +11 -3
  104. package/src/daemon/conversation.ts +2 -0
  105. package/src/daemon/handlers/config-channels.ts +20 -16
  106. package/src/daemon/handlers/config-slack-channel.ts +2 -3
  107. package/src/daemon/lifecycle.ts +0 -7
  108. package/src/daemon/message-types/conversations.ts +3 -3
  109. package/src/daemon/message-types/sync.ts +0 -1
  110. package/src/daemon/orphan-reaper.test.ts +0 -19
  111. package/src/daemon/orphan-reaper.ts +2 -24
  112. package/src/daemon/server.ts +0 -10
  113. package/src/home/relationship-state.ts +2 -4
  114. package/src/memory/__tests__/memory-retrospective-job.test.ts +195 -401
  115. package/src/memory/bookmark-crud.ts +1 -2
  116. package/src/memory/db-init.ts +7 -17
  117. package/src/memory/embedding-backend.ts +23 -0
  118. package/src/memory/embedding-billing-breaker.ts +96 -0
  119. package/src/memory/jobs-store.ts +25 -13
  120. package/src/memory/jobs-worker.ts +52 -0
  121. package/src/memory/memory-retrospective-constants.ts +4 -4
  122. package/src/memory/memory-retrospective-job.ts +19 -227
  123. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +62 -0
  124. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +311 -0
  125. package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
  126. package/src/memory/migrations/index.ts +1 -0
  127. package/src/memory/migrations/run-migrations.ts +41 -0
  128. package/src/memory/migrations/validate-migration-state.ts +1 -1
  129. package/src/memory/schema/contacts.ts +0 -4
  130. package/src/messaging/providers/slack/adapter.ts +1 -1
  131. package/src/notifications/adapters/shared.ts +29 -0
  132. package/src/notifications/adapters/slack.ts +5 -32
  133. package/src/notifications/adapters/telegram.ts +2 -20
  134. package/src/notifications/broadcaster.ts +10 -1
  135. package/src/notifications/home-feed-side-effect.ts +4 -3
  136. package/src/notifications/notification-utils.ts +17 -19
  137. package/src/notifications/types.ts +7 -0
  138. package/src/oauth/AGENTS.md +5 -24
  139. package/src/plugin-api/constants.ts +1 -1
  140. package/src/plugin-api/index.ts +6 -1
  141. package/src/plugin-api/model-profiles.ts +33 -0
  142. package/src/plugin-api/types.ts +50 -2
  143. package/src/plugins/defaults/index.ts +25 -0
  144. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +54 -2
  145. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +107 -7
  146. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
  147. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
  148. package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
  149. package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
  150. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +1 -1
  151. package/src/prompts/persona-resolver.ts +2 -2
  152. package/src/runtime/AGENTS.md +0 -1
  153. package/src/runtime/actor-trust-resolver.ts +12 -44
  154. package/src/runtime/btw-sidechain.ts +3 -6
  155. package/src/runtime/channel-approval-types.ts +18 -45
  156. package/src/runtime/channel-invite-transports/telegram.ts +4 -4
  157. package/src/runtime/channel-verification-service.ts +4 -3
  158. package/src/runtime/invite-redemption-service.ts +3 -3
  159. package/src/runtime/routes/__tests__/plugins-routes.test.ts +218 -1
  160. package/src/runtime/routes/app-routes.ts +1 -1
  161. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
  162. package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
  163. package/src/runtime/routes/btw-routes.ts +1 -27
  164. package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
  165. package/src/runtime/routes/conversation-routes.ts +2 -2
  166. package/src/runtime/routes/credential-routes.ts +40 -16
  167. package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
  168. package/src/runtime/routes/identity-routes.ts +1 -296
  169. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +1 -1
  170. package/src/runtime/routes/plugins-routes.ts +171 -5
  171. package/src/runtime/routes/schedule-routes.ts +0 -22
  172. package/src/runtime/routes/workflow-routes.test.ts +4 -43
  173. package/src/runtime/routes/workflow-routes.ts +0 -28
  174. package/src/runtime/routes/workspace-greetings.ts +55 -0
  175. package/src/runtime/sync/resource-sync-events.ts +1 -11
  176. package/src/schedule/inference-profile.ts +2 -14
  177. package/src/subagent/manager.ts +6 -0
  178. package/src/subagent/types.ts +6 -0
  179. package/src/tools/AGENTS.md +3 -3
  180. package/src/tools/browser/browser-execution.ts +1 -1
  181. package/src/tools/network/web-search-error.ts +1 -1
  182. package/src/tools/permission-checker.ts +1 -1
  183. package/src/tools/schedule/create.ts +3 -9
  184. package/src/tools/schedule/update.ts +2 -10
  185. package/src/tools/side-effects.ts +2 -17
  186. package/src/tools/skills/execute.ts +34 -0
  187. package/src/tools/subagent/spawn.ts +34 -9
  188. package/src/tools/tool-approval-handler.ts +1 -3
  189. package/src/tools/tool-manifest.ts +0 -2
  190. package/src/tools/ui-surface/definitions.ts +39 -1
  191. package/src/tools/workflows/run-workflow.test.ts +8 -18
  192. package/src/util/platform.ts +2 -2
  193. package/src/workflows/capabilities.ts +2 -3
  194. package/src/workflows/run-manager.test.ts +0 -25
  195. package/src/workflows/run-manager.ts +2 -24
  196. package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
  197. package/src/__tests__/credential-security-e2e.test.ts +0 -362
  198. package/src/__tests__/credential-vault-unit.test.ts +0 -1528
  199. package/src/__tests__/credential-vault.test.ts +0 -1706
  200. package/src/__tests__/identity-intro-cache.test.ts +0 -315
  201. package/src/__tests__/secret-onetime-send.test.ts +0 -182
  202. package/src/cli/commands/__tests__/task.test.ts +0 -914
  203. package/src/cli/commands/task.ts +0 -771
  204. package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
  205. package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
  206. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
  207. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
  208. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
  209. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
  210. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
  211. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
  212. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
  213. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
  214. package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
  215. package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
  216. package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
  217. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
  218. package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
  219. package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
  220. package/src/memory/preloaded-apps.ts +0 -116
  221. package/src/runtime/routes/identity-intro-cache.ts +0 -172
  222. package/src/tools/credentials/vault.ts +0 -712
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Tests for the default `surface-completion-nudge` plugin's hooks.
3
+ *
4
+ * Covers:
5
+ * - The `post-model-call` hook nudges (continue + canonical text appended as a
6
+ * `user` message) when a turn ends with a progress surface left open: a
7
+ * `task_progress` card shown and never advanced to a terminal status, and a
8
+ * `work_result` shown `in_progress`.
9
+ * - It does NOT nudge when the surface was completed via `ui_update`, dismissed
10
+ * via `ui_dismiss`, or was never a progress surface (a plain card / a form).
11
+ * - Outcomes it does not own are ignored: a provider rejection, a tool-bearing
12
+ * turn, and a non-main-agent call site.
13
+ * - The signal is scoped to the current response cycle — a surface left open in
14
+ * a prior cycle (before the last genuine user prompt) does not trigger it.
15
+ * - The one-shot bound is split across the two hooks: `post-model-call` marks it
16
+ * (nudging at most once per run) and `stop` clears it so the next run nudges
17
+ * afresh.
18
+ *
19
+ * The loop's actual continuation side-effects live in `agent/loop.ts` and are
20
+ * covered by integration tests. This file isolates the hook.
21
+ */
22
+
23
+ import { beforeEach, describe, expect, test } from "bun:test";
24
+
25
+ import type {
26
+ PluginLogger,
27
+ PostModelCallContext,
28
+ } from "../plugin-api/types.js";
29
+ import postModelCall, {
30
+ SURFACE_COMPLETION_NUDGE_TEXT,
31
+ } from "../plugins/defaults/surface-completion-nudge/hooks/post-model-call.js";
32
+ import stop from "../plugins/defaults/surface-completion-nudge/hooks/stop.js";
33
+ import {
34
+ isSurfaceCompletionNudged,
35
+ resetSurfaceCompletionNudgeStoreForTests,
36
+ } from "../plugins/defaults/surface-completion-nudge/nudge-state-store.js";
37
+ import type { ContentBlock, Message } from "../providers/types.js";
38
+
39
+ // ─── Fixtures ────────────────────────────────────────────────────────────────
40
+
41
+ const noopLogger: PluginLogger = {
42
+ info: () => {},
43
+ warn: () => {},
44
+ error: () => {},
45
+ debug: () => {},
46
+ };
47
+
48
+ const finalText: ContentBlock = { type: "text", text: "All set." };
49
+
50
+ let surfaceCounter = 0;
51
+
52
+ /**
53
+ * An assistant `ui_show` turn paired with its `{ surfaceId }` tool result.
54
+ * Returns both messages plus the assigned surface id.
55
+ */
56
+ function showSurface(input: Record<string, unknown>): {
57
+ messages: Message[];
58
+ surfaceId: string;
59
+ } {
60
+ surfaceCounter += 1;
61
+ const toolUseId = `tu_show_${surfaceCounter}`;
62
+ const surfaceId = `surface-${surfaceCounter}`;
63
+ return {
64
+ surfaceId,
65
+ messages: [
66
+ {
67
+ role: "assistant",
68
+ content: [{ type: "tool_use", id: toolUseId, name: "ui_show", input }],
69
+ },
70
+ {
71
+ role: "user",
72
+ content: [
73
+ {
74
+ type: "tool_result",
75
+ tool_use_id: toolUseId,
76
+ content: JSON.stringify({ surfaceId }),
77
+ },
78
+ ],
79
+ },
80
+ ],
81
+ };
82
+ }
83
+
84
+ function updateSurface(
85
+ surfaceId: string,
86
+ data: Record<string, unknown>,
87
+ ): Message {
88
+ return {
89
+ role: "assistant",
90
+ content: [
91
+ {
92
+ type: "tool_use",
93
+ id: `tu_update_${surfaceId}`,
94
+ name: "ui_update",
95
+ input: { surface_id: surfaceId, data },
96
+ },
97
+ ],
98
+ };
99
+ }
100
+
101
+ function dismissSurface(surfaceId: string): Message {
102
+ return {
103
+ role: "assistant",
104
+ content: [
105
+ {
106
+ type: "tool_use",
107
+ id: `tu_dismiss_${surfaceId}`,
108
+ name: "ui_dismiss",
109
+ input: { surface_id: surfaceId },
110
+ },
111
+ ],
112
+ };
113
+ }
114
+
115
+ function userPrompt(text: string): Message {
116
+ return { role: "user", content: [{ type: "text", text }] };
117
+ }
118
+
119
+ const taskProgressShow = (status?: string): Record<string, unknown> => ({
120
+ surface_type: "card",
121
+ title: "Working on X",
122
+ data: {
123
+ template: "task_progress",
124
+ templateData: {
125
+ title: "Working on X",
126
+ ...(status ? { status } : {}),
127
+ steps: [{ label: "Step 1", status: status ?? "in_progress" }],
128
+ },
129
+ },
130
+ });
131
+
132
+ function makeCtx(
133
+ overrides: Partial<PostModelCallContext> = {},
134
+ ): PostModelCallContext {
135
+ return {
136
+ conversationId: "conv-scn",
137
+ callSite: "mainAgent",
138
+ content: [finalText],
139
+ messages: [],
140
+ stopReason: null,
141
+ decision: "stop",
142
+ logger: noopLogger,
143
+ ...overrides,
144
+ };
145
+ }
146
+
147
+ beforeEach(() => {
148
+ resetSurfaceCompletionNudgeStoreForTests();
149
+ surfaceCounter = 0;
150
+ });
151
+
152
+ // ─── Nudges when a progress surface is left open ──────────────────────────────
153
+
154
+ describe("surface-completion-nudge — nudges on a dangling progress surface", () => {
155
+ test("task_progress card shown and never completed → continue with nudge", async () => {
156
+ const shown = showSurface(taskProgressShow("in_progress"));
157
+ const ctx = makeCtx({
158
+ messages: [userPrompt("do the thing"), ...shown.messages],
159
+ });
160
+
161
+ await postModelCall(ctx);
162
+
163
+ expect(ctx.decision).toBe("continue");
164
+ const last = ctx.messages[ctx.messages.length - 1];
165
+ expect(last.role).toBe("user");
166
+ expect(last.content[0]).toEqual({
167
+ type: "text",
168
+ text: SURFACE_COMPLETION_NUDGE_TEXT,
169
+ });
170
+ expect(isSurfaceCompletionNudged("conv-scn")).toBe(true);
171
+ });
172
+
173
+ test("task_progress card shown with no explicit status → continue with nudge", async () => {
174
+ const shown = showSurface(taskProgressShow());
175
+ const ctx = makeCtx({
176
+ messages: [userPrompt("go"), ...shown.messages],
177
+ });
178
+
179
+ await postModelCall(ctx);
180
+
181
+ expect(ctx.decision).toBe("continue");
182
+ });
183
+
184
+ test("work_result shown in_progress → continue with nudge", async () => {
185
+ const shown = showSurface({
186
+ surface_type: "work_result",
187
+ data: { status: "in_progress", summary: "Crunching" },
188
+ });
189
+ const ctx = makeCtx({
190
+ messages: [userPrompt("go"), ...shown.messages],
191
+ });
192
+
193
+ await postModelCall(ctx);
194
+
195
+ expect(ctx.decision).toBe("continue");
196
+ });
197
+ });
198
+
199
+ // ─── Stays quiet when there is nothing to close ───────────────────────────────
200
+
201
+ describe("surface-completion-nudge — no nudge when surface is closed or absent", () => {
202
+ test("task_progress completed via ui_update → stop", async () => {
203
+ const shown = showSurface(taskProgressShow("in_progress"));
204
+ const ctx = makeCtx({
205
+ messages: [
206
+ userPrompt("go"),
207
+ ...shown.messages,
208
+ updateSurface(shown.surfaceId, {
209
+ templateData: { status: "completed" },
210
+ }),
211
+ ],
212
+ });
213
+
214
+ await postModelCall(ctx);
215
+
216
+ expect(ctx.decision).toBe("stop");
217
+ expect(isSurfaceCompletionNudged("conv-scn")).toBe(false);
218
+ });
219
+
220
+ test("progress surface dismissed via ui_dismiss → stop", async () => {
221
+ const shown = showSurface(taskProgressShow("in_progress"));
222
+ const ctx = makeCtx({
223
+ messages: [
224
+ userPrompt("go"),
225
+ ...shown.messages,
226
+ dismissSurface(shown.surfaceId),
227
+ ],
228
+ });
229
+
230
+ await postModelCall(ctx);
231
+
232
+ expect(ctx.decision).toBe("stop");
233
+ });
234
+
235
+ test("work_result shown completed → stop", async () => {
236
+ const shown = showSurface({
237
+ surface_type: "work_result",
238
+ data: { status: "completed", summary: "Done" },
239
+ });
240
+ const ctx = makeCtx({
241
+ messages: [userPrompt("go"), ...shown.messages],
242
+ });
243
+
244
+ await postModelCall(ctx);
245
+
246
+ expect(ctx.decision).toBe("stop");
247
+ });
248
+
249
+ test("plain card (not a progress surface) → stop", async () => {
250
+ const shown = showSurface({
251
+ surface_type: "card",
252
+ title: "Weather",
253
+ data: { template: "weather_forecast", body: "Sunny" },
254
+ });
255
+ const ctx = makeCtx({
256
+ messages: [userPrompt("go"), ...shown.messages],
257
+ });
258
+
259
+ await postModelCall(ctx);
260
+
261
+ expect(ctx.decision).toBe("stop");
262
+ });
263
+
264
+ test("no surfaces shown at all → stop", async () => {
265
+ const ctx = makeCtx({
266
+ messages: [userPrompt("go")],
267
+ });
268
+
269
+ await postModelCall(ctx);
270
+
271
+ expect(ctx.decision).toBe("stop");
272
+ });
273
+ });
274
+
275
+ // ─── Outcomes the hook does not own ───────────────────────────────────────────
276
+
277
+ describe("surface-completion-nudge — ignores outcomes it does not own", () => {
278
+ test("provider rejection (error present) → stop", async () => {
279
+ const shown = showSurface(taskProgressShow("in_progress"));
280
+ const ctx = makeCtx({
281
+ messages: [userPrompt("go"), ...shown.messages],
282
+ error: new Error("provider exploded"),
283
+ });
284
+
285
+ await postModelCall(ctx);
286
+
287
+ expect(ctx.decision).toBe("stop");
288
+ });
289
+
290
+ test("tool-bearing turn (model still working) → stop", async () => {
291
+ const shown = showSurface(taskProgressShow("in_progress"));
292
+ const ctx = makeCtx({
293
+ messages: [userPrompt("go"), ...shown.messages],
294
+ content: [
295
+ { type: "tool_use", id: "tu_next", name: "read_file", input: {} },
296
+ ],
297
+ });
298
+
299
+ await postModelCall(ctx);
300
+
301
+ expect(ctx.decision).toBe("stop");
302
+ });
303
+
304
+ test("non-main-agent call site → stop", async () => {
305
+ const shown = showSurface(taskProgressShow("in_progress"));
306
+ const ctx = makeCtx({
307
+ messages: [userPrompt("go"), ...shown.messages],
308
+ callSite: "heartbeatAgent",
309
+ });
310
+
311
+ await postModelCall(ctx);
312
+
313
+ expect(ctx.decision).toBe("stop");
314
+ });
315
+ });
316
+
317
+ // ─── Cycle scoping and the one-shot bound ─────────────────────────────────────
318
+
319
+ describe("surface-completion-nudge — cycle scoping and one-shot bound", () => {
320
+ test("a surface left open in a prior cycle does not trigger this cycle", async () => {
321
+ const priorOpen = showSurface(taskProgressShow("in_progress"));
322
+ const ctx = makeCtx({
323
+ messages: [
324
+ userPrompt("first task"),
325
+ ...priorOpen.messages,
326
+ // New genuine user prompt opens a fresh cycle with no open surface.
327
+ userPrompt("second task"),
328
+ ],
329
+ });
330
+
331
+ await postModelCall(ctx);
332
+
333
+ expect(ctx.decision).toBe("stop");
334
+ });
335
+
336
+ test("nudges at most once per run; stop clears the bound", async () => {
337
+ const shown = showSurface(taskProgressShow("in_progress"));
338
+ const firstCtx = makeCtx({
339
+ messages: [userPrompt("go"), ...shown.messages],
340
+ });
341
+
342
+ await postModelCall(firstCtx);
343
+ expect(firstCtx.decision).toBe("continue");
344
+
345
+ // Same run, surface still open: the one-shot bound suppresses a second nudge.
346
+ const secondCtx = makeCtx({
347
+ messages: [userPrompt("go"), ...shown.messages],
348
+ });
349
+ await postModelCall(secondCtx);
350
+ expect(secondCtx.decision).toBe("stop");
351
+
352
+ // Terminal stop clears the bound so the next run nudges afresh.
353
+ await stop({
354
+ conversationId: "conv-scn",
355
+ messages: [],
356
+ exitReason: "no_tool_calls",
357
+ logger: noopLogger,
358
+ });
359
+ expect(isSurfaceCompletionNudged("conv-scn")).toBe(false);
360
+
361
+ const thirdCtx = makeCtx({
362
+ messages: [userPrompt("go"), ...shown.messages],
363
+ });
364
+ await postModelCall(thirdCtx);
365
+ expect(thirdCtx.decision).toBe("continue");
366
+ });
367
+ });
@@ -128,7 +128,6 @@ function makeSystemPrompt(size: "small" | "production" = "small"): string {
128
128
  "- `file_write` — Creating or overwriting files",
129
129
  "- `file_edit` — Modifying existing files",
130
130
  "- `file_delete` — Deleting files",
131
- "- `credential_store set` — Storing credentials",
132
131
  "",
133
132
  "### Medium-Risk Tools (require approval on first use per session)",
134
133
  "- `web_fetch` — Fetching external URLs",
@@ -349,7 +348,7 @@ function makeToolDefinitions(): Array<{
349
348
  input_schema: object;
350
349
  }> = [];
351
350
 
352
- // Core tools (11)
351
+ // Core tools (10)
353
352
  tools.push(
354
353
  {
355
354
  name: "bash",
@@ -566,33 +565,6 @@ function makeToolDefinitions(): Array<{
566
565
  required: ["query"],
567
566
  },
568
567
  },
569
- {
570
- name: "credential_store",
571
- description:
572
- "Securely store or retrieve credentials for external services. Credentials are encrypted at rest.",
573
- input_schema: {
574
- type: "object",
575
- properties: {
576
- action: {
577
- type: "string",
578
- enum: ["get", "set", "delete", "list"],
579
- },
580
- service: {
581
- type: "string",
582
- description: "The service name (e.g., 'github', 'slack')",
583
- },
584
- key: {
585
- type: "string",
586
- description: "Credential key within the service",
587
- },
588
- value: {
589
- type: "string",
590
- description: "Credential value (required for 'set')",
591
- },
592
- },
593
- required: ["action", "service"],
594
- },
595
- },
596
568
  );
597
569
 
598
570
  // Computer-use proxy tools (11)