@vellumai/assistant 0.9.0 → 0.10.0-staging.2

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 (572) hide show
  1. package/ARCHITECTURE.md +18 -34
  2. package/bun.lock +7 -8
  3. package/docs/activation-funnel-telemetry.md +28 -22
  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/admission-policy-contract.ts +97 -0
  12. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  13. package/node_modules/@vellumai/gateway-client/src/index.ts +32 -6
  14. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
  15. package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
  16. package/openapi.yaml +976 -63
  17. package/package.json +2 -1
  18. package/scripts/sync-llm-catalog.ts +6 -15
  19. package/scripts/sync-web-search-catalog.ts +3 -11
  20. package/src/__tests__/access-request-card-view.test.ts +98 -0
  21. package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
  22. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +72 -32
  23. package/src/__tests__/agent-loop-compaction-strip.test.ts +241 -0
  24. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
  25. package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
  26. package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
  27. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
  28. package/src/__tests__/app-compiler.test.ts +15 -1
  29. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  30. package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
  31. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
  32. package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
  33. package/src/__tests__/avatar-identity-sync.test.ts +2 -27
  34. package/src/__tests__/btw-routes.test.ts +6 -8
  35. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  36. package/src/__tests__/cancel-clears-processing.test.ts +89 -0
  37. package/src/__tests__/channel-approval-routes.test.ts +0 -4
  38. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
  39. package/src/__tests__/checker.test.ts +0 -3
  40. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
  41. package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
  42. package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
  43. package/src/__tests__/config-loader-backfill.test.ts +268 -27
  44. package/src/__tests__/config-schema.test.ts +35 -0
  45. package/src/__tests__/config-watcher.test.ts +0 -18
  46. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
  47. package/src/__tests__/contact-store-user-file.test.ts +0 -6
  48. package/src/__tests__/contacts-tools.test.ts +29 -0
  49. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
  50. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  51. package/src/__tests__/conversation-agent-loop.test.ts +58 -0
  52. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  53. package/src/__tests__/conversation-lifecycle.test.ts +7 -9
  54. package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
  55. package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
  56. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
  57. package/src/__tests__/conversation-title-service.test.ts +62 -0
  58. package/src/__tests__/credential-broker.test.ts +449 -1
  59. package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
  60. package/src/__tests__/credential-execution-tools.test.ts +0 -1
  61. package/src/__tests__/credential-prompt-route.test.ts +4 -4
  62. package/src/__tests__/credential-routes.test.ts +360 -0
  63. package/src/__tests__/credential-security-invariants.test.ts +4 -13
  64. package/src/__tests__/disk-pressure-policy.test.ts +12 -0
  65. package/src/__tests__/disk-usage.test.ts +65 -0
  66. package/src/__tests__/dynamic-page-surface.test.ts +152 -1
  67. package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
  68. package/src/__tests__/gateway-flag-listener.test.ts +110 -1
  69. package/src/__tests__/gateway-only-guard.test.ts +3 -7
  70. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  71. package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
  72. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  73. package/src/__tests__/guardian-grant-minting.test.ts +3 -35
  74. package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
  75. package/src/__tests__/guardian-routing-state.test.ts +0 -1
  76. package/src/__tests__/headless-browser-mode.test.ts +10 -0
  77. package/src/__tests__/headless-browser-navigate.test.ts +8 -3
  78. package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
  79. package/src/__tests__/host-browser-proxy.test.ts +87 -0
  80. package/src/__tests__/identity-routes.test.ts +0 -189
  81. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  82. package/src/__tests__/injector-v3-suppression.test.ts +27 -20
  83. package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
  84. package/src/__tests__/invite-redemption-service.test.ts +4 -7
  85. package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
  86. package/src/__tests__/llm-catalog-parity.test.ts +30 -23
  87. package/src/__tests__/llm-resolver.test.ts +70 -24
  88. package/src/__tests__/llm-schema.test.ts +1 -0
  89. package/src/__tests__/managed-profile-guard.test.ts +163 -4
  90. package/src/__tests__/mcp-health-check.test.ts +6 -7
  91. package/src/__tests__/media-stream-server-integration.test.ts +317 -13
  92. package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
  93. package/src/__tests__/onboarding-persona-write.test.ts +1 -1
  94. package/src/__tests__/path-policy.test.ts +34 -0
  95. package/src/__tests__/persona-resolver.test.ts +49 -14
  96. package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
  97. package/src/__tests__/plugin-api-provider.test.ts +24 -0
  98. package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
  99. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
  100. package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
  101. package/src/__tests__/reaction-persistence.test.ts +150 -29
  102. package/src/__tests__/registry.test.ts +2 -7
  103. package/src/__tests__/relay-server.test.ts +285 -0
  104. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  105. package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
  106. package/src/__tests__/schedule-routes.test.ts +0 -30
  107. package/src/__tests__/schedule-tools.test.ts +2 -18
  108. package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
  109. package/src/__tests__/skill-execute-input.test.ts +51 -1
  110. package/src/__tests__/skill-runtime-path.test.ts +2 -3
  111. package/src/__tests__/skills.test.ts +51 -0
  112. package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
  113. package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
  114. package/src/__tests__/subagent-tools.test.ts +266 -0
  115. package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
  116. package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
  117. package/src/__tests__/title-generate-hook.test.ts +100 -3
  118. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
  119. package/src/__tests__/token-manager.test.ts +519 -0
  120. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
  121. package/src/__tests__/tool-audit-listener.test.ts +7 -7
  122. package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
  123. package/src/__tests__/tool-executor.test.ts +0 -79
  124. package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
  125. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
  126. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  127. package/src/__tests__/trusted-contact-verification.test.ts +8 -10
  128. package/src/__tests__/twilio-routes.test.ts +81 -1
  129. package/src/__tests__/voice-invite-redemption.test.ts +2 -3
  130. package/src/__tests__/weak-open-model.test.ts +30 -0
  131. package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
  132. package/src/__tests__/workspace-greetings.test.ts +152 -0
  133. package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
  134. package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
  135. package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
  136. package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
  137. package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
  138. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
  139. package/src/agent/loop.ts +49 -29
  140. package/src/api/README.md +6 -6
  141. package/src/api/events/tool-result.ts +6 -0
  142. package/src/api/events/workflow-completed.ts +53 -0
  143. package/src/api/events/workflow-leaf-finished.ts +38 -0
  144. package/src/api/events/workflow-leaf-started.ts +35 -0
  145. package/src/api/events/workflow-progress.ts +32 -0
  146. package/src/api/events/workflow-started.ts +31 -0
  147. package/src/api/index.ts +40 -0
  148. package/src/api/responses/conversation-message.ts +28 -4
  149. package/src/api/responses/home.ts +26 -4
  150. package/src/api/responses/workflow-journal.ts +53 -0
  151. package/src/approvals/guardian-card-withdrawal.ts +145 -0
  152. package/src/approvals/guardian-decision-primitive.ts +26 -3
  153. package/src/approvals/guardian-request-resolvers.ts +183 -80
  154. package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
  155. package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
  156. package/src/calls/call-pointer-messages.ts +10 -4
  157. package/src/calls/channel-admission-reader.ts +104 -0
  158. package/src/calls/guardian-dispatch.ts +17 -45
  159. package/src/calls/media-stream-server.ts +84 -2
  160. package/src/calls/relay-access-wait.ts +1 -1
  161. package/src/calls/relay-server.ts +66 -0
  162. package/src/calls/relay-setup-router.ts +82 -1
  163. package/src/calls/twilio-routes.ts +17 -8
  164. package/src/calls/voice-session-bridge.ts +2 -2
  165. package/src/cli/commands/clients.ts +3 -0
  166. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
  167. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
  168. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
  169. package/src/cli/commands/memory/index.ts +30 -0
  170. package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
  171. package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
  172. package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
  173. package/src/cli/commands/oauth/status.test.ts +36 -0
  174. package/src/cli/commands/oauth/status.ts +23 -3
  175. package/src/cli/commands/plugins.ts +197 -4
  176. package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
  177. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  178. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +443 -0
  179. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
  180. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +295 -2
  181. package/src/cli/lib/diff-plugin.ts +346 -0
  182. package/src/cli/lib/inspect-plugin.ts +12 -1
  183. package/src/cli/lib/install-from-github.ts +105 -17
  184. package/src/cli/lib/merge-plugin-tree.ts +328 -0
  185. package/src/cli/lib/plugin-fingerprint.ts +14 -0
  186. package/src/cli/lib/plugin-surfaces.ts +104 -0
  187. package/src/cli/lib/upgrade-plugin.ts +298 -10
  188. package/src/cli/program.ts +2 -6
  189. package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
  190. package/src/config/assistant-feature-flags.ts +22 -7
  191. package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
  192. package/src/config/bundled-skills/messaging/SKILL.md +6 -4
  193. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
  194. package/src/config/bundled-skills/subagent/SKILL.md +4 -0
  195. package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
  196. package/src/config/bundled-skills/workflows/SKILL.md +14 -8
  197. package/src/config/bundled-tool-registry.ts +2 -7
  198. package/src/config/call-site-defaults.ts +15 -2
  199. package/src/config/feature-flag-registry.json +46 -31
  200. package/src/config/inference-profile-validation.ts +26 -0
  201. package/src/config/llm-resolver.ts +3 -0
  202. package/src/config/loader.ts +4 -0
  203. package/src/config/memory-v3-gate.ts +11 -0
  204. package/src/config/profile-order.ts +28 -0
  205. package/src/config/schema.ts +8 -6
  206. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  207. package/src/config/schemas/call-site-catalog.ts +7 -0
  208. package/src/config/schemas/channels.ts +11 -0
  209. package/src/config/schemas/elevenlabs.ts +0 -1
  210. package/src/config/schemas/llm.ts +31 -0
  211. package/src/config/schemas/memory-lifecycle.ts +3 -7
  212. package/src/config/schemas/memory-v3.ts +6 -0
  213. package/src/config/schemas/platform.ts +0 -8
  214. package/src/config/schemas/services.ts +18 -0
  215. package/src/config/seed-inference-profiles.ts +109 -44
  216. package/src/config/skills.ts +21 -0
  217. package/src/config/sync-gated-profiles.ts +220 -0
  218. package/src/contacts/contact-store.ts +89 -106
  219. package/src/contacts/contacts-write.ts +5 -22
  220. package/src/contacts/types.ts +0 -1
  221. package/src/context/compactor.ts +88 -54
  222. package/src/context/strip-injections.ts +58 -10
  223. package/src/context/token-estimator.ts +1 -1
  224. package/src/credential-execution/process-manager.ts +55 -14
  225. package/src/credential-execution/prompted-credential.ts +2 -3
  226. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
  227. package/src/daemon/config-watcher.ts +0 -4
  228. package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
  229. package/src/daemon/conversation-agent-loop.ts +114 -22
  230. package/src/daemon/conversation-history.ts +1 -1
  231. package/src/daemon/conversation-lifecycle.ts +3 -5
  232. package/src/daemon/conversation-process.ts +13 -5
  233. package/src/daemon/conversation-runtime-assembly.ts +13 -15
  234. package/src/daemon/conversation-slash.ts +2 -23
  235. package/src/daemon/conversation-surfaces.ts +26 -0
  236. package/src/daemon/conversation-tool-setup.ts +27 -14
  237. package/src/daemon/conversation.ts +66 -14
  238. package/src/daemon/disk-pressure-policy.ts +5 -3
  239. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
  240. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
  241. package/src/daemon/handlers/config-a2a.ts +0 -2
  242. package/src/daemon/handlers/config-channels.ts +15 -16
  243. package/src/daemon/handlers/config-slack-channel.ts +22 -3
  244. package/src/daemon/handlers/conversations.ts +107 -0
  245. package/src/daemon/host-browser-proxy.ts +41 -0
  246. package/src/daemon/lifecycle.ts +55 -27
  247. package/src/daemon/message-provenance.ts +2 -0
  248. package/src/daemon/message-types/contacts.ts +0 -1
  249. package/src/daemon/message-types/conversations.ts +3 -3
  250. package/src/daemon/message-types/sync.ts +0 -1
  251. package/src/daemon/message-types/web-activity.ts +7 -1
  252. package/src/daemon/message-types/workflows.ts +83 -1
  253. package/src/daemon/orphan-reaper.test.ts +0 -19
  254. package/src/daemon/orphan-reaper.ts +2 -24
  255. package/src/daemon/server.ts +0 -10
  256. package/src/daemon/tool-setup-types.ts +4 -0
  257. package/src/daemon/trust-context.ts +1 -1
  258. package/src/events/tool-audit-listener.ts +2 -2
  259. package/src/home/feed-source-enrichment.test.ts +151 -0
  260. package/src/home/feed-source-enrichment.ts +176 -0
  261. package/src/home/relationship-state.ts +2 -4
  262. package/src/instrument.ts +18 -6
  263. package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
  264. package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
  265. package/src/ipc/assistant-server.ts +37 -4
  266. package/src/ipc/gateway-flag-listener.ts +18 -2
  267. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
  268. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
  269. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
  270. package/src/memory/__tests__/memory-retrospective-job.test.ts +229 -401
  271. package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
  272. package/src/memory/auth-fallback-events-store.ts +2 -2
  273. package/src/memory/auto-analysis-enqueue.ts +3 -5
  274. package/src/memory/bookmark-crud.ts +1 -2
  275. package/src/memory/canonical-guardian-store.ts +39 -1
  276. package/src/memory/conversation-crud.ts +9 -4
  277. package/src/memory/conversation-key-store.ts +17 -2
  278. package/src/memory/conversation-title-service.ts +64 -7
  279. package/src/memory/db-init.ts +17 -17
  280. package/src/memory/embedding-backend.ts +38 -1
  281. package/src/memory/embedding-billing-breaker.ts +96 -0
  282. package/src/memory/jobs-store.ts +25 -13
  283. package/src/memory/jobs-worker.ts +54 -1
  284. package/src/memory/lifecycle-events-store.ts +2 -2
  285. package/src/memory/memory-retrospective-constants.ts +4 -4
  286. package/src/memory/memory-retrospective-enqueue.ts +31 -6
  287. package/src/memory/memory-retrospective-job.ts +28 -227
  288. package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
  289. package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
  290. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
  291. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +72 -0
  292. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
  293. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
  294. package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
  295. package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
  296. package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
  297. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
  298. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
  299. package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
  300. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
  301. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +341 -0
  302. package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
  303. package/src/memory/migrations/index.ts +6 -0
  304. package/src/memory/migrations/run-migrations.ts +41 -0
  305. package/src/memory/migrations/validate-migration-state.ts +1 -1
  306. package/src/memory/onboarding-events-store.ts +3 -3
  307. package/src/memory/schema/contacts.ts +0 -5
  308. package/src/memory/skill-loaded-events-store.test.ts +7 -15
  309. package/src/memory/skill-loaded-events-store.ts +2 -2
  310. package/src/memory/tool-executed-events-store.test.ts +7 -7
  311. package/src/memory/turn-trace-store.test.ts +736 -0
  312. package/src/memory/turn-trace-store.ts +364 -0
  313. package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
  314. package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
  315. package/src/memory/v2/consolidation-job.ts +2 -2
  316. package/src/memory/v2/skill-content.ts +25 -7
  317. package/src/memory/v2/skill-store.ts +7 -1
  318. package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
  319. package/src/memory/v3-eval/eval-packets.ts +546 -0
  320. package/src/messaging/providers/slack/adapter.ts +1 -1
  321. package/src/messaging/providers/slack/api.ts +31 -0
  322. package/src/messaging/providers/slack/send.test.ts +114 -2
  323. package/src/messaging/providers/slack/send.ts +30 -7
  324. package/src/messaging/providers/slack/withdraw.test.ts +200 -0
  325. package/src/messaging/providers/slack/withdraw.ts +161 -0
  326. package/src/notifications/AGENTS.md +2 -0
  327. package/src/notifications/access-request-copy.ts +72 -59
  328. package/src/notifications/adapters/shared.ts +29 -0
  329. package/src/notifications/adapters/slack.ts +58 -103
  330. package/src/notifications/adapters/telegram.ts +2 -20
  331. package/src/notifications/approval-card-data.ts +333 -0
  332. package/src/notifications/broadcaster.ts +16 -3
  333. package/src/notifications/canonical-delivery-recorder.ts +139 -0
  334. package/src/notifications/copy-composer.ts +3 -3
  335. package/src/notifications/decision-engine.ts +4 -2
  336. package/src/notifications/destination-resolver.ts +4 -6
  337. package/src/notifications/guardian-question-mode.ts +10 -0
  338. package/src/notifications/home-feed-side-effect.ts +7 -16
  339. package/src/notifications/notification-utils.ts +19 -20
  340. package/src/notifications/signal.ts +79 -43
  341. package/src/notifications/types.ts +98 -121
  342. package/src/oauth/AGENTS.md +5 -24
  343. package/src/permissions/checker.test.ts +51 -0
  344. package/src/permissions/checker.ts +185 -26
  345. package/src/permissions/ipc-risk-types.ts +24 -0
  346. package/src/permissions/question-prompter.test.ts +27 -0
  347. package/src/permissions/question-prompter.ts +4 -0
  348. package/src/platform/client.test.ts +119 -0
  349. package/src/platform/client.ts +66 -0
  350. package/src/platform/consent-cache.test.ts +267 -0
  351. package/src/platform/consent-cache.ts +174 -0
  352. package/src/plugin-api/constants.ts +1 -1
  353. package/src/plugin-api/index.ts +33 -1
  354. package/src/plugin-api/model-profiles.ts +33 -0
  355. package/src/plugin-api/types.ts +50 -2
  356. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
  357. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
  358. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
  359. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
  360. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
  361. package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
  362. package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
  363. package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
  364. package/src/plugins/defaults/advisor/config.ts +21 -0
  365. package/src/plugins/defaults/advisor/consult.ts +93 -0
  366. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
  367. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
  368. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
  369. package/src/plugins/defaults/advisor/package.json +14 -0
  370. package/src/plugins/defaults/advisor/steering.ts +67 -0
  371. package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
  372. package/src/plugins/defaults/advisor/transcript.ts +76 -0
  373. package/src/plugins/defaults/index.ts +60 -0
  374. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
  375. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  376. package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
  377. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
  378. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
  379. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  380. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +129 -9
  381. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
  382. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
  383. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
  384. package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
  385. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +144 -11
  386. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
  387. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
  388. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
  389. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
  390. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
  391. package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
  392. package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
  393. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +3 -13
  394. package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
  395. package/src/prompts/persona-resolver.ts +14 -4
  396. package/src/prompts/templates/system-sections.ts +7 -2
  397. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  398. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  399. package/src/providers/__tests__/retry-callsite.test.ts +176 -0
  400. package/src/providers/atlascloud/client.ts +85 -0
  401. package/src/providers/fetch-provider-catalog.ts +85 -0
  402. package/src/providers/inference/adapter-factory.ts +3 -0
  403. package/src/providers/model-catalog.ts +58 -0
  404. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
  405. package/src/providers/openai/chat-completions-provider.ts +7 -0
  406. package/src/providers/openai/responses-provider.ts +10 -0
  407. package/src/providers/provider-send-message.ts +11 -3
  408. package/src/providers/retry.ts +53 -12
  409. package/src/providers/search-provider-catalog.ts +10 -0
  410. package/src/providers/weak-open-model.ts +22 -0
  411. package/src/runtime/AGENTS.md +0 -1
  412. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  413. package/src/runtime/__tests__/client-health.test.ts +44 -0
  414. package/src/runtime/access-request-helper.ts +21 -53
  415. package/src/runtime/actor-trust-resolver.ts +59 -63
  416. package/src/runtime/agent-wake.ts +52 -0
  417. package/src/runtime/assistant-event-hub.ts +18 -4
  418. package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
  419. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  420. package/src/runtime/btw-sidechain.ts +3 -6
  421. package/src/runtime/capabilities.test.ts +120 -0
  422. package/src/runtime/capabilities.ts +197 -0
  423. package/src/runtime/channel-approval-types.ts +22 -45
  424. package/src/runtime/channel-invite-transports/telegram.ts +4 -4
  425. package/src/runtime/channel-retry-sweep.ts +1 -0
  426. package/src/runtime/channel-verification-service.ts +3 -3
  427. package/src/runtime/client-health.ts +26 -0
  428. package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
  429. package/src/runtime/effective-capabilities.test.ts +128 -0
  430. package/src/runtime/effective-capabilities.ts +84 -0
  431. package/src/runtime/guardian-reply-router.ts +106 -21
  432. package/src/runtime/invite-redemption-service.ts +9 -25
  433. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +123 -0
  434. package/src/runtime/migrations/vbundle-builder.ts +49 -20
  435. package/src/runtime/pending-interactions.ts +15 -0
  436. package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
  437. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
  438. package/src/runtime/routes/__tests__/plugins-routes.test.ts +240 -1
  439. package/src/runtime/routes/app-routes.ts +1 -1
  440. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
  441. package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
  442. package/src/runtime/routes/browser-tabs-routes.ts +9 -0
  443. package/src/runtime/routes/btw-routes.ts +1 -27
  444. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
  445. package/src/runtime/routes/client-routes.ts +10 -0
  446. package/src/runtime/routes/contact-routes.ts +31 -8
  447. package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
  448. package/src/runtime/routes/conversation-management-routes.ts +80 -1
  449. package/src/runtime/routes/conversation-query-routes.ts +68 -22
  450. package/src/runtime/routes/conversation-routes.ts +39 -14
  451. package/src/runtime/routes/credential-routes.ts +40 -16
  452. package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
  453. package/src/runtime/routes/events-routes.ts +1 -3
  454. package/src/runtime/routes/guardian-approval-interception.ts +14 -73
  455. package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
  456. package/src/runtime/routes/home-feed-routes.ts +8 -3
  457. package/src/runtime/routes/identity-routes.ts +1 -296
  458. package/src/runtime/routes/inbound-message-handler.ts +214 -228
  459. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +89 -7
  460. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
  461. package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
  462. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
  463. package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
  464. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
  465. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
  466. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
  467. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
  468. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
  469. package/src/runtime/routes/index.ts +2 -0
  470. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
  471. package/src/runtime/routes/integrations/slack/channel.ts +36 -0
  472. package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
  473. package/src/runtime/routes/mcp-auth-routes.ts +233 -41
  474. package/src/runtime/routes/memory-eval-routes.ts +87 -0
  475. package/src/runtime/routes/notification-routes.ts +122 -133
  476. package/src/runtime/routes/platform-routes.ts +2 -2
  477. package/src/runtime/routes/plugins-routes.ts +202 -3
  478. package/src/runtime/routes/schedule-routes.ts +0 -22
  479. package/src/runtime/routes/secret-routes.ts +10 -0
  480. package/src/runtime/routes/surface-action-routes.ts +2 -1
  481. package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
  482. package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
  483. package/src/runtime/routes/workflow-routes.test.ts +229 -44
  484. package/src/runtime/routes/workflow-routes.ts +131 -29
  485. package/src/runtime/routes/workspace-greetings.ts +55 -0
  486. package/src/runtime/sync/resource-sync-events.ts +1 -11
  487. package/src/runtime/tool-grant-request-helper.ts +18 -16
  488. package/src/runtime/trust-context-resolver.ts +8 -5
  489. package/src/schedule/inference-profile.ts +2 -14
  490. package/src/schedule/schedule-store.ts +1 -1
  491. package/src/schedule/scheduler-types.ts +5 -1
  492. package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
  493. package/src/security/secret-patterns.ts +3 -0
  494. package/src/subagent/manager.ts +17 -4
  495. package/src/subagent/types.ts +6 -0
  496. package/src/telemetry/trace-collection-policy.test.ts +28 -0
  497. package/src/telemetry/trace-collection-policy.ts +30 -0
  498. package/src/telemetry/types.ts +89 -0
  499. package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
  500. package/src/telemetry/usage-telemetry-reporter.ts +148 -41
  501. package/src/tools/AGENTS.md +3 -3
  502. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
  503. package/src/tools/browser/browser-execution.ts +30 -19
  504. package/src/tools/document/document-tool.ts +2 -3
  505. package/src/tools/executor.ts +5 -3
  506. package/src/tools/host-terminal/host-shell.ts +5 -4
  507. package/src/tools/memory/register.ts +2 -2
  508. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
  509. package/src/tools/network/__tests__/web-search.test.ts +143 -0
  510. package/src/tools/network/web-fetch.ts +372 -1
  511. package/src/tools/network/web-search-error.ts +1 -1
  512. package/src/tools/network/web-search.ts +213 -10
  513. package/src/tools/permission-checker.ts +4 -3
  514. package/src/tools/registry.ts +20 -0
  515. package/src/tools/schedule/create.ts +7 -12
  516. package/src/tools/schedule/update.ts +4 -11
  517. package/src/tools/shared/filesystem/path-policy.ts +39 -13
  518. package/src/tools/side-effects.ts +2 -17
  519. package/src/tools/skills/execute.ts +33 -0
  520. package/src/tools/subagent/spawn.ts +61 -12
  521. package/src/tools/terminal/shell.ts +10 -4
  522. package/src/tools/tool-approval-handler.ts +18 -13
  523. package/src/tools/tool-manifest.ts +0 -2
  524. package/src/tools/types.ts +9 -0
  525. package/src/tools/ui-surface/definitions.ts +64 -3
  526. package/src/tools/verification-control-plane-policy.ts +3 -1
  527. package/src/tools/workflows/run-workflow.test.ts +8 -18
  528. package/src/tools/workflows/run-workflow.ts +1 -0
  529. package/src/util/disk-usage.ts +78 -23
  530. package/src/util/platform.ts +10 -3
  531. package/src/watcher/telemetry.ts +2 -2
  532. package/src/workflows/capabilities.ts +2 -3
  533. package/src/workflows/engine.test.ts +175 -1
  534. package/src/workflows/engine.ts +82 -0
  535. package/src/workflows/journal-store.test.ts +70 -0
  536. package/src/workflows/journal-store.ts +18 -3
  537. package/src/workflows/run-manager.test.ts +171 -28
  538. package/src/workflows/run-manager.ts +66 -24
  539. package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
  540. package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
  541. package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
  542. package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
  543. package/src/workspace/migrations/registry.ts +8 -0
  544. package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
  545. package/src/__tests__/credential-security-e2e.test.ts +0 -362
  546. package/src/__tests__/credential-vault-unit.test.ts +0 -1528
  547. package/src/__tests__/credential-vault.test.ts +0 -1706
  548. package/src/__tests__/identity-intro-cache.test.ts +0 -315
  549. package/src/__tests__/secret-onetime-send.test.ts +0 -182
  550. package/src/cli/commands/__tests__/task.test.ts +0 -914
  551. package/src/cli/commands/task.ts +0 -771
  552. package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
  553. package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
  554. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
  555. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
  556. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
  557. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
  558. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
  559. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
  560. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
  561. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
  562. package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
  563. package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
  564. package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
  565. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
  566. package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
  567. package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
  568. package/src/memory/preloaded-apps.ts +0 -116
  569. package/src/notifications/tool-approval-copy.ts +0 -142
  570. package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
  571. package/src/runtime/routes/identity-intro-cache.ts +0 -172
  572. package/src/tools/credentials/vault.ts +0 -712
@@ -18,7 +18,7 @@ import {
18
18
  writeFileSync,
19
19
  } from "node:fs";
20
20
  import { tmpdir } from "node:os";
21
- import { join } from "node:path";
21
+ import { dirname, join } from "node:path";
22
22
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
23
23
 
24
24
  import {
@@ -26,8 +26,13 @@ import {
26
26
  type GitRunner,
27
27
  PluginSourceUnavailableError,
28
28
  } from "../install-from-github.js";
29
+ import { computeFingerprint } from "../plugin-fingerprint.js";
29
30
  import { PluginNotInstalledError } from "../uninstall-plugin.js";
30
- import { PluginNotUpgradableError, upgradePlugin } from "../upgrade-plugin.js";
31
+ import {
32
+ PluginMergeBaselineError,
33
+ PluginNotUpgradableError,
34
+ upgradePlugin,
35
+ } from "../upgrade-plugin.js";
31
36
 
32
37
  const SHA_A = "a".repeat(40);
33
38
  const SHA_B = "b".repeat(40);
@@ -382,3 +387,291 @@ describe("upgradePlugin", () => {
382
387
  expect(sidecarCommit(pluginsDir, "level-up")).toBe(SHA_A);
383
388
  });
384
389
  });
390
+
391
+ /** A file tree keyed by POSIX-relative path. */
392
+ type Tree = Record<string, string>;
393
+
394
+ /**
395
+ * A fake clone whose materialized tree depends on the fetched ref, so a merge
396
+ * upgrade's base clone (`source.ref` = the recorded install commit) and pin
397
+ * clone (`source.ref` = the marketplace pin) yield different trees. The ref is
398
+ * the last `git fetch` argument; `rev-parse HEAD` echoes it back so the
399
+ * checked-out commit matches the requested ref.
400
+ */
401
+ function treeGitRunner(treesByRef: Record<string, Tree>): GitRunner {
402
+ const refByCwd = new Map<string, string>();
403
+ return async (args, { cwd }) => {
404
+ switch (args[0]) {
405
+ case "fetch": {
406
+ const ref = args[args.length - 1];
407
+ refByCwd.set(cwd, ref);
408
+ const tree = treesByRef[ref] ?? {};
409
+ for (const [rel, content] of Object.entries(tree)) {
410
+ const abs = join(cwd, rel);
411
+ mkdirSync(dirname(abs), { recursive: true });
412
+ writeFileSync(abs, content);
413
+ }
414
+ // `.git` is stripped during materialization; create it so the strip
415
+ // path is exercised, matching a real clone.
416
+ mkdirSync(join(cwd, ".git"), { recursive: true });
417
+ writeFileSync(join(cwd, ".git", "config"), "[core]\n");
418
+ return { stdout: "" };
419
+ }
420
+ case "rev-parse":
421
+ return { stdout: `${refByCwd.get(cwd) ?? ""}\n` };
422
+ default:
423
+ return { stdout: "" };
424
+ }
425
+ };
426
+ }
427
+
428
+ /**
429
+ * Materialize an installed copy of `ours` on disk plus a provenance sidecar
430
+ * recording `commit` and a fingerprint. By default the fingerprint is computed
431
+ * over `base` (what install materialized); pass `fingerprintTree` to record a
432
+ * mismatching baseline.
433
+ */
434
+ function installMergeCopy(
435
+ name: string,
436
+ ours: Tree,
437
+ commit: string,
438
+ fingerprintTree: Tree | null,
439
+ ): void {
440
+ const dir = join(pluginsDir, name);
441
+ mkdirSync(dir, { recursive: true });
442
+ for (const [rel, content] of Object.entries(ours)) {
443
+ const abs = join(dir, rel);
444
+ mkdirSync(dirname(abs), { recursive: true });
445
+ writeFileSync(abs, content);
446
+ }
447
+ const sidecar: Record<string, unknown> = {
448
+ origin: "vellum",
449
+ name,
450
+ source: { kind: "github", owner: "example-org", repo: name, ref: commit },
451
+ commit,
452
+ installedAt: "2026-06-10T12:00:00.000Z",
453
+ };
454
+ if (fingerprintTree !== null) {
455
+ const refDir = mkdtempSync(join(tmpdir(), "merge-fingerprint-"));
456
+ try {
457
+ for (const [rel, content] of Object.entries(fingerprintTree)) {
458
+ const abs = join(refDir, rel);
459
+ mkdirSync(dirname(abs), { recursive: true });
460
+ writeFileSync(abs, content);
461
+ }
462
+ sidecar.fingerprint = computeFingerprint(refDir, ["install-meta.json"]);
463
+ } finally {
464
+ rmSync(refDir, { recursive: true, force: true });
465
+ }
466
+ }
467
+ writeFileSync(join(dir, "install-meta.json"), JSON.stringify(sidecar));
468
+ }
469
+
470
+ /** Read an installed file's contents, or null when absent. */
471
+ function installedFile(name: string, rel: string): string | null {
472
+ const path = join(pluginsDir, name, rel);
473
+ return existsSync(path) ? readFileSync(path, "utf-8") : null;
474
+ }
475
+
476
+ describe("upgradePlugin --strategy", () => {
477
+ // base → ours / theirs trees shared by the three-way merge tests:
478
+ // - common.txt: edited on disjoint lines by each side (a clean auto-merge)
479
+ // - conflict.txt: edited differently on both sides (a true conflict)
480
+ // - local-only.txt / remote-only.txt: added on one side only
481
+ const BASE: Tree = {
482
+ "common.txt": "a\nb\nc\n",
483
+ "conflict.txt": "base\n",
484
+ };
485
+ const OURS: Tree = {
486
+ "common.txt": "A\nb\nc\n",
487
+ "conflict.txt": "ours\n",
488
+ "local-only.txt": "added locally\n",
489
+ };
490
+ const THEIRS: Tree = {
491
+ "common.txt": "a\nb\nC\n",
492
+ "conflict.txt": "theirs\n",
493
+ "remote-only.txt": "added upstream\n",
494
+ };
495
+
496
+ test("--strategy ours carries both sides' edits and resolves conflicts toward local", async () => {
497
+ // GIVEN an install at SHA_A with local edits, and a pin at SHA_B
498
+ installMergeCopy("level-up", OURS, SHA_A, BASE);
499
+ const fetch = makeFetch({ manifest: manifestWith("level-up", SHA_B) });
500
+ const runGit = treeGitRunner({ [SHA_A]: BASE, [SHA_B]: THEIRS });
501
+
502
+ // WHEN the plugin is upgraded with the `ours` strategy
503
+ const result = await upgradePlugin(
504
+ { name: "level-up", strategy: "ours" },
505
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
506
+ );
507
+
508
+ // THEN it moves to the pin and records the strategy
509
+ expect(result.outcome).toBe("upgraded");
510
+ expect(result.toCommit).toBe(SHA_B);
511
+ expect(result.strategy).toBe("ours");
512
+ // AND non-conflicting edits from both sides survive
513
+ expect(installedFile("level-up", "common.txt")).toBe("A\nb\nC\n");
514
+ expect(installedFile("level-up", "local-only.txt")).toBe("added locally\n");
515
+ expect(installedFile("level-up", "remote-only.txt")).toBe(
516
+ "added upstream\n",
517
+ );
518
+ // AND the conflicting file resolves toward the local edit
519
+ expect(installedFile("level-up", "conflict.txt")).toBe("ours\n");
520
+ });
521
+
522
+ test("--strategy theirs carries both sides' edits and resolves conflicts toward the pin", async () => {
523
+ // GIVEN an install at SHA_A with local edits, and a pin at SHA_B
524
+ installMergeCopy("level-up", OURS, SHA_A, BASE);
525
+ const fetch = makeFetch({ manifest: manifestWith("level-up", SHA_B) });
526
+ const runGit = treeGitRunner({ [SHA_A]: BASE, [SHA_B]: THEIRS });
527
+
528
+ // WHEN the plugin is upgraded with the `theirs` strategy
529
+ const result = await upgradePlugin(
530
+ { name: "level-up", strategy: "theirs" },
531
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
532
+ );
533
+
534
+ // THEN non-conflicting edits from both sides still survive
535
+ expect(result.strategy).toBe("theirs");
536
+ expect(installedFile("level-up", "common.txt")).toBe("A\nb\nC\n");
537
+ expect(installedFile("level-up", "local-only.txt")).toBe("added locally\n");
538
+ expect(installedFile("level-up", "remote-only.txt")).toBe(
539
+ "added upstream\n",
540
+ );
541
+ // AND the conflicting file resolves toward the pin
542
+ expect(installedFile("level-up", "conflict.txt")).toBe("theirs\n");
543
+ });
544
+
545
+ test("--strategy overwrite discards local edits and re-installs the pin wholesale", async () => {
546
+ // GIVEN an install at SHA_A with a local-only file, and a pin at SHA_B
547
+ installMergeCopy("level-up", OURS, SHA_A, BASE);
548
+ const fetch = makeFetch({ manifest: manifestWith("level-up", SHA_B) });
549
+ const runGit = treeGitRunner({ [SHA_A]: BASE, [SHA_B]: THEIRS });
550
+
551
+ // WHEN the plugin is upgraded with the `overwrite` strategy
552
+ const result = await upgradePlugin(
553
+ { name: "level-up", strategy: "overwrite" },
554
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
555
+ );
556
+
557
+ // THEN the on-disk tree is exactly the pin — local edits are gone
558
+ expect(result.strategy).toBe("overwrite");
559
+ expect(installedFile("level-up", "common.txt")).toBe("a\nb\nC\n");
560
+ expect(installedFile("level-up", "conflict.txt")).toBe("theirs\n");
561
+ expect(installedFile("level-up", "remote-only.txt")).toBe(
562
+ "added upstream\n",
563
+ );
564
+ expect(installedFile("level-up", "local-only.txt")).toBeNull();
565
+ });
566
+
567
+ test("defaults to overwrite when no strategy is given", async () => {
568
+ // GIVEN an install with a local-only file and an advanced pin
569
+ installMergeCopy("level-up", OURS, SHA_A, BASE);
570
+ const fetch = makeFetch({ manifest: manifestWith("level-up", SHA_B) });
571
+ const runGit = treeGitRunner({ [SHA_A]: BASE, [SHA_B]: THEIRS });
572
+
573
+ // WHEN the plugin is upgraded without a strategy
574
+ const result = await upgradePlugin(
575
+ { name: "level-up" },
576
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
577
+ );
578
+
579
+ // THEN it overwrites: the local-only file is dropped
580
+ expect(result.strategy).toBe("overwrite");
581
+ expect(installedFile("level-up", "local-only.txt")).toBeNull();
582
+ });
583
+
584
+ test("--strategy assistant writes conflict markers and reports the conflicted path", async () => {
585
+ // GIVEN an install at SHA_A with local edits, and a pin at SHA_B
586
+ installMergeCopy("level-up", OURS, SHA_A, BASE);
587
+ const fetch = makeFetch({ manifest: manifestWith("level-up", SHA_B) });
588
+ const runGit = treeGitRunner({ [SHA_A]: BASE, [SHA_B]: THEIRS });
589
+
590
+ // WHEN the plugin is upgraded with the `assistant` strategy
591
+ const result = await upgradePlugin(
592
+ { name: "level-up", strategy: "assistant" },
593
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
594
+ );
595
+
596
+ // THEN it moves to the pin and records the strategy
597
+ expect(result.outcome).toBe("upgraded");
598
+ expect(result.toCommit).toBe(SHA_B);
599
+ expect(result.strategy).toBe("assistant");
600
+ // AND non-conflicting edits from both sides still auto-merge
601
+ expect(installedFile("level-up", "common.txt")).toBe("A\nb\nC\n");
602
+ expect(installedFile("level-up", "local-only.txt")).toBe("added locally\n");
603
+ expect(installedFile("level-up", "remote-only.txt")).toBe(
604
+ "added upstream\n",
605
+ );
606
+ // AND the true conflict carries git markers naming both commits
607
+ const conflict = installedFile("level-up", "conflict.txt") ?? "";
608
+ expect(conflict).toContain("<<<<<<<");
609
+ expect(conflict).toContain(SHA_A.slice(0, 7));
610
+ expect(conflict).toContain(SHA_B.slice(0, 7));
611
+ expect(conflict).toContain("ours\n");
612
+ expect(conflict).toContain("theirs\n");
613
+ // AND the conflicted path is surfaced for the assistant to resolve
614
+ expect(result.conflicts).toEqual(["conflict.txt"]);
615
+ expect(result.binaryConflicts).toEqual([]);
616
+ });
617
+
618
+ test("--strategy assistant reports no conflicts on a clean three-way merge", async () => {
619
+ // GIVEN an install whose only divergence from the pin auto-merges cleanly
620
+ const cleanOurs: Tree = { "common.txt": "A\nb\nc\n" };
621
+ const cleanBase: Tree = { "common.txt": "a\nb\nc\n" };
622
+ const cleanTheirs: Tree = { "common.txt": "a\nb\nC\n" };
623
+ installMergeCopy("level-up", cleanOurs, SHA_A, cleanBase);
624
+ const fetch = makeFetch({ manifest: manifestWith("level-up", SHA_B) });
625
+ const runGit = treeGitRunner({ [SHA_A]: cleanBase, [SHA_B]: cleanTheirs });
626
+
627
+ // WHEN the plugin is upgraded with the `assistant` strategy
628
+ const result = await upgradePlugin(
629
+ { name: "level-up", strategy: "assistant" },
630
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
631
+ );
632
+
633
+ // THEN both edits merge with no markers and nothing needs resolution
634
+ expect(result.strategy).toBe("assistant");
635
+ expect(result.conflicts).toEqual([]);
636
+ expect(result.binaryConflicts).toEqual([]);
637
+ const merged = installedFile("level-up", "common.txt") ?? "";
638
+ expect(merged).toBe("A\nb\nC\n");
639
+ expect(merged).not.toContain("<<<<<<<");
640
+ });
641
+
642
+ test("throws PluginMergeBaselineError when no fingerprint was recorded", async () => {
643
+ // GIVEN an install whose sidecar records no fingerprint (older install)
644
+ installMergeCopy("level-up", OURS, SHA_A, null);
645
+ const fetch = makeFetch({ manifest: manifestWith("level-up", SHA_B) });
646
+ const runGit = treeGitRunner({ [SHA_A]: BASE, [SHA_B]: THEIRS });
647
+
648
+ // WHEN a merge strategy is requested
649
+ // THEN the baseline cannot be trusted, so the merge is refused
650
+ await expect(
651
+ upgradePlugin(
652
+ { name: "level-up", strategy: "ours" },
653
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
654
+ ),
655
+ ).rejects.toBeInstanceOf(PluginMergeBaselineError);
656
+ });
657
+
658
+ test("throws PluginMergeBaselineError when the re-materialized base drifts from the recorded fingerprint", async () => {
659
+ // GIVEN an install whose recorded fingerprint describes a tree that differs
660
+ // from what re-materializing the install commit produces (an adapter
661
+ // overlay that moved since install)
662
+ installMergeCopy("level-up", OURS, SHA_A, {
663
+ "common.txt": "totally different baseline\n",
664
+ });
665
+ const fetch = makeFetch({ manifest: manifestWith("level-up", SHA_B) });
666
+ const runGit = treeGitRunner({ [SHA_A]: BASE, [SHA_B]: THEIRS });
667
+
668
+ // WHEN a merge strategy is requested
669
+ // THEN the baseline is rejected rather than producing a corrupt merge
670
+ await expect(
671
+ upgradePlugin(
672
+ { name: "level-up", strategy: "theirs" },
673
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
674
+ ),
675
+ ).rejects.toBeInstanceOf(PluginMergeBaselineError);
676
+ });
677
+ });
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Show the unified diff of local edits to an installed plugin, against the
3
+ * exact commit it was installed at.
4
+ *
5
+ * `plugins inspect` already reports *which* files drifted via the install-time
6
+ * per-file fingerprint (see {@link ./plugin-fingerprint}), but that digest is
7
+ * one-way — it cannot reconstruct the original bytes, so it can't show *what*
8
+ * changed. Drift here is classified the same way `inspect` does — against the
9
+ * fingerprint recorded at install — so the two surfaces always agree and a
10
+ * curated adapter overlay that changed since install never misreads as local
11
+ * drift.
12
+ *
13
+ * Showing *what* changed still needs the baseline *bytes*, which the fingerprint
14
+ * cannot reconstruct. Those are re-derived by re-materializing the recorded
15
+ * commit (from `install-meta.json`) through the *same* pipeline install used
16
+ * (see {@link ./install-from-github.materializePluginTree}): a shallow clone at
17
+ * the immutable SHA plus the curated adapter overlay. The adapter overlay is
18
+ * fetched from the canonical repo's current ref, not the install-time ref (which
19
+ * is not recorded), so a re-materialized file can diverge from what install
20
+ * produced. Each baseline file is therefore verified against the recorded
21
+ * fingerprint digest before it is diffed: on a mismatch the install-time bytes
22
+ * cannot be faithfully reconstructed, so the file is flagged rather than diffed
23
+ * against a fabricated baseline.
24
+ *
25
+ * The baseline is always the recorded install commit, not the marketplace's
26
+ * current pin: this answers "what local changes have been made since install",
27
+ * independent of marketplace movement. Comparing against the latest pin is the
28
+ * separate concern `plugins upgrade --dry-run` already covers.
29
+ *
30
+ * Designed for direct programmatic use with injected dependencies, mirroring
31
+ * the sibling plugin libraries. The CLI command `assistant plugins diff <name>`
32
+ * is a thin wrapper that supplies production deps and formats the result.
33
+ */
34
+
35
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
36
+ import { tmpdir } from "node:os";
37
+ import { join } from "node:path";
38
+
39
+ import { createTwoFilesPatch } from "diff";
40
+
41
+ import { getWorkspacePluginsDir } from "../../util/platform.js";
42
+ import {
43
+ DEFAULT_PLUGIN_REF,
44
+ type FetchLike,
45
+ type GitRunner,
46
+ INSTALL_META_FILENAME,
47
+ materializePluginTree,
48
+ type PluginFetchSource,
49
+ PluginNotFoundError,
50
+ type PostinstallRunner,
51
+ readInstallMeta,
52
+ sanitizePluginName,
53
+ } from "./install-from-github.js";
54
+ import { readInstalledPlugin } from "./list-installed-plugins.js";
55
+ import {
56
+ compareFingerprint,
57
+ computeFingerprint,
58
+ type Fingerprint,
59
+ } from "./plugin-fingerprint.js";
60
+ import { PluginNotInstalledError } from "./uninstall-plugin.js";
61
+
62
+ /** How a file drifted from the install-time baseline. */
63
+ export type PluginFileDiffStatus = "modified" | "added" | "removed";
64
+
65
+ /** Unified diff of a single drifted file. */
66
+ export interface PluginFileDiff {
67
+ /** POSIX-relative path within the plugin root. */
68
+ readonly path: string;
69
+ /** Whether the file was edited, newly added, or deleted since install. */
70
+ readonly status: PluginFileDiffStatus;
71
+ /**
72
+ * Unified diff (`--- a/… / +++ b/…`) of the file's bytes. For a binary file
73
+ * this is a short `Binary files differ` marker instead of a line diff, since
74
+ * a line-based patch of non-text content is noise. When {@link
75
+ * PluginFileDiff.reconstructed} is false this is a short explanatory marker
76
+ * rather than a patch, since the install-time bytes are unavailable.
77
+ */
78
+ readonly diff: string;
79
+ /** True when either side was detected as binary (NUL byte present). */
80
+ readonly binary: boolean;
81
+ /**
82
+ * True when the install-time baseline for this file was faithfully recovered
83
+ * (its re-materialized bytes hash-match the digest recorded at install).
84
+ * False when the baseline could not be reconstructed — e.g. the curated
85
+ * adapter overlay the file was built from has changed since install — in
86
+ * which case {@link PluginFileDiff.diff} is a marker, not a real patch.
87
+ * Always true for `added` files, whose baseline is the empty side.
88
+ */
89
+ readonly reconstructed: boolean;
90
+ }
91
+
92
+ /** Resolved diff of an installed plugin against its install-time baseline. */
93
+ export interface PluginDiffResult {
94
+ /** Install name. Matches `assistant plugins install <name>`. */
95
+ readonly name: string;
96
+ /** Absolute path to the installed plugin directory. */
97
+ readonly target: string;
98
+ /** Commit the baseline was re-materialized from (the recorded install SHA). */
99
+ readonly commit: string;
100
+ /** ISO-8601 committer timestamp (UTC) of {@link PluginDiffResult.commit}; `null` when unrecorded. */
101
+ readonly committedAt: string | null;
102
+ /** True when the on-disk tree exactly matches the re-materialized baseline. */
103
+ readonly clean: boolean;
104
+ /** One entry per drifted file, sorted by path. Empty when `clean`. */
105
+ readonly files: readonly PluginFileDiff[];
106
+ }
107
+
108
+ /**
109
+ * The installed copy carries no resolvable commit, so there is no immutable
110
+ * baseline to re-materialize and diff against.
111
+ */
112
+ export class PluginDiffUnavailableError extends Error {
113
+ constructor(
114
+ readonly pluginName: string,
115
+ reason: string,
116
+ ) {
117
+ super(`Plugin "${pluginName}" cannot be diffed: ${reason}.`);
118
+ this.name = "PluginDiffUnavailableError";
119
+ }
120
+ }
121
+
122
+ /** Options that control which plugin to diff. */
123
+ export interface DiffPluginOptions {
124
+ /** Install name (kebab-case directory name). */
125
+ readonly name: string;
126
+ }
127
+
128
+ /** Dependencies injected by the caller. */
129
+ export interface DiffPluginDeps {
130
+ /** HTTP client used to fetch any curated adapter stub. Production callers pass `globalThis.fetch.bind(globalThis)`. */
131
+ readonly fetch: FetchLike;
132
+ /** Override the workspace plugins directory. Falls back to the live workspace. */
133
+ readonly workspacePluginsDir?: string;
134
+ /** Override the git runner used to clone the baseline. Forwarded to {@link materializePluginTree}. */
135
+ readonly runGit?: GitRunner;
136
+ /** Override the postinstall adapter runner. Forwarded to {@link materializePluginTree}. */
137
+ readonly runPostinstall?: PostinstallRunner;
138
+ }
139
+
140
+ /** A NUL byte in the leading bytes is the heuristic git uses to flag a blob as binary. */
141
+ function isBinary(buf: Buffer): boolean {
142
+ const len = Math.min(buf.length, 8000);
143
+ for (let i = 0; i < len; i++) {
144
+ if (buf[i] === 0) return true;
145
+ }
146
+ return false;
147
+ }
148
+
149
+ interface FileContent {
150
+ readonly text: string;
151
+ readonly binary: boolean;
152
+ }
153
+
154
+ function readContent(absPath: string): FileContent {
155
+ const buf = readFileSync(absPath);
156
+ const binary = isBinary(buf);
157
+ return { text: binary ? "" : buf.toString("utf8"), binary };
158
+ }
159
+
160
+ function makeDiff(
161
+ path: string,
162
+ status: PluginFileDiffStatus,
163
+ before: FileContent | null,
164
+ after: FileContent | null,
165
+ reconstructed: boolean,
166
+ ): PluginFileDiff {
167
+ if (!reconstructed) {
168
+ return {
169
+ path,
170
+ status,
171
+ diff: `Baseline unavailable (${status}): the install-time content of this file could not be reconstructed — the curated adapter overlay it was built from has changed since install. Reinstall with 'plugins install <name> --force' to refresh the baseline.`,
172
+ binary: false,
173
+ reconstructed: false,
174
+ };
175
+ }
176
+ const binary = (before?.binary ?? false) || (after?.binary ?? false);
177
+ if (binary) {
178
+ return {
179
+ path,
180
+ status,
181
+ diff: `Binary files differ (${status})`,
182
+ binary: true,
183
+ reconstructed: true,
184
+ };
185
+ }
186
+ // `/dev/null` on the absent side and `a/`–`b/` prefixes mirror `git diff`, so
187
+ // the output is familiar and consumable by tools that parse unified diffs.
188
+ const oldName = status === "added" ? "/dev/null" : `a/${path}`;
189
+ const newName = status === "removed" ? "/dev/null" : `b/${path}`;
190
+ const diff = createTwoFilesPatch(
191
+ oldName,
192
+ newName,
193
+ before?.text ?? "",
194
+ after?.text ?? "",
195
+ undefined,
196
+ undefined,
197
+ { context: 3 },
198
+ );
199
+ return { path, status, diff, binary: false, reconstructed: true };
200
+ }
201
+
202
+ /**
203
+ * Recover the install-time bytes of `path` from the re-materialized baseline
204
+ * tree, but only when they faithfully match what install produced: the
205
+ * re-materialized digest must equal the digest `recorded` at install. A
206
+ * mismatch (or a file the re-materialization did not produce) means the
207
+ * install-time content cannot be reconstructed — typically because the curated
208
+ * adapter overlay the file was built from changed since install — so `null` is
209
+ * returned and the caller flags the file instead of diffing fabricated bytes.
210
+ */
211
+ function baselineContent(
212
+ path: string,
213
+ baselineRoot: string,
214
+ recorded: Fingerprint,
215
+ materialized: Fingerprint,
216
+ ): FileContent | null {
217
+ if (materialized.files[path] !== recorded.files[path]) return null;
218
+ return readContent(join(baselineRoot, path));
219
+ }
220
+
221
+ /**
222
+ * Build a per-file unified diff for the on-disk install, classifying drift
223
+ * against the fingerprint `recorded` at install — the same baseline `inspect`
224
+ * uses, so the two surfaces always agree on which files changed and an adapter
225
+ * overlay that moved since install never reads as drift. The re-materialized
226
+ * tree supplies the baseline *bytes* for the diff, verified per file against
227
+ * `recorded`. The provenance sidecar is excluded on both sides — it never
228
+ * exists in the baseline and must not read as a local addition.
229
+ */
230
+ function buildFileDiffs(
231
+ baselineRoot: string,
232
+ target: string,
233
+ recorded: Fingerprint,
234
+ ): PluginFileDiff[] {
235
+ const comparison = compareFingerprint(target, recorded, [
236
+ INSTALL_META_FILENAME,
237
+ ]);
238
+ const materialized = computeFingerprint(baselineRoot, [
239
+ INSTALL_META_FILENAME,
240
+ ]);
241
+
242
+ const files: PluginFileDiff[] = [];
243
+ for (const path of comparison.modified) {
244
+ const before = baselineContent(path, baselineRoot, recorded, materialized);
245
+ files.push(
246
+ makeDiff(
247
+ path,
248
+ "modified",
249
+ before,
250
+ readContent(join(target, path)),
251
+ before !== null,
252
+ ),
253
+ );
254
+ }
255
+ for (const path of comparison.added) {
256
+ files.push(
257
+ makeDiff(path, "added", null, readContent(join(target, path)), true),
258
+ );
259
+ }
260
+ for (const path of comparison.removed) {
261
+ const before = baselineContent(path, baselineRoot, recorded, materialized);
262
+ files.push(makeDiff(path, "removed", before, null, before !== null));
263
+ }
264
+ files.sort((a, b) => a.path.localeCompare(b.path));
265
+ return files;
266
+ }
267
+
268
+ /**
269
+ * Resolve the unified diff of an installed plugin against its install-time
270
+ * baseline.
271
+ *
272
+ * Throws {@link PluginNotInstalledError} when no copy is installed,
273
+ * {@link PluginDiffUnavailableError} when the install recorded no commit to
274
+ * re-materialize, {@link PluginNotFoundError} when the recorded commit can no
275
+ * longer be fetched (e.g. the source repo or commit was removed), and
276
+ * propagates {@link materializePluginTree}'s errors (e.g. source unavailable)
277
+ * when the baseline clone itself fails.
278
+ */
279
+ export async function diffPlugin(
280
+ opts: DiffPluginOptions,
281
+ deps: DiffPluginDeps,
282
+ ): Promise<PluginDiffResult> {
283
+ const name = sanitizePluginName(opts.name);
284
+ const dir = deps.workspacePluginsDir ?? getWorkspacePluginsDir();
285
+ const target = join(dir, name);
286
+
287
+ if (!readInstalledPlugin(name, { workspacePluginsDir: dir })) {
288
+ throw new PluginNotInstalledError(name, target);
289
+ }
290
+
291
+ const meta = readInstallMeta(target);
292
+ const commit = meta?.commit ?? null;
293
+ if (!meta || !commit) {
294
+ throw new PluginDiffUnavailableError(
295
+ name,
296
+ `no install commit was recorded (an older or manually-copied install); reinstall with 'assistant plugins install ${name} --force' to record provenance`,
297
+ );
298
+ }
299
+ // Drift is classified against the install-time fingerprint (as `inspect`
300
+ // does), so without it there is no trustworthy baseline to diff against.
301
+ const recorded = meta.fingerprint;
302
+ if (!recorded) {
303
+ throw new PluginDiffUnavailableError(
304
+ name,
305
+ `no install-time fingerprint was recorded (an older or manually-copied install); reinstall with 'assistant plugins install ${name} --force' to record provenance`,
306
+ );
307
+ }
308
+
309
+ const source: PluginFetchSource = {
310
+ owner: meta.source.owner,
311
+ repo: meta.source.repo,
312
+ rootPath: meta.source.path ?? "",
313
+ ref: commit,
314
+ };
315
+
316
+ const baselineRoot = mkdtempSync(join(tmpdir(), `plugin-diff-${name}-`));
317
+ try {
318
+ const materialized = await materializePluginTree(
319
+ { source, name, stubRef: DEFAULT_PLUGIN_REF, destDir: baselineRoot },
320
+ {
321
+ fetch: deps.fetch,
322
+ runGit: deps.runGit,
323
+ runPostinstall: deps.runPostinstall,
324
+ },
325
+ );
326
+ if (materialized.fileCount === 0) {
327
+ throw new PluginNotFoundError(
328
+ name,
329
+ commit,
330
+ `${source.owner}/${source.repo}`,
331
+ );
332
+ }
333
+
334
+ const files = buildFileDiffs(baselineRoot, target, recorded);
335
+ return {
336
+ name,
337
+ target,
338
+ commit,
339
+ committedAt: meta.committedAt ?? materialized.committedAt ?? null,
340
+ clean: files.length === 0,
341
+ files,
342
+ };
343
+ } finally {
344
+ rmSync(baselineRoot, { recursive: true, force: true });
345
+ }
346
+ }