@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
@@ -36,6 +36,10 @@ import {
36
36
  fetchMarketplaceEntries,
37
37
  type MarketplaceEntry,
38
38
  } from "./plugin-marketplace.js";
39
+ import {
40
+ detectPluginSurfaces,
41
+ type PluginSurfaces,
42
+ } from "./plugin-surfaces.js";
39
43
 
40
44
  /** Full commit SHA (40 hex SHA-1 or 64 hex SHA-256). */
41
45
  const FULL_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
@@ -131,6 +135,12 @@ export interface PluginInspection {
131
135
  readonly remote: PluginRemoteInfo | null;
132
136
  /** Marketplace fetch error message, when the catalog could not be read. */
133
137
  readonly remoteError: string | null;
138
+ /**
139
+ * Surfaces the installed copy contributes (skills, hooks, tools), read from
140
+ * its on-disk tree. `null` when the plugin is not installed — there is no
141
+ * tree to inspect, and the marketplace metadata does not enumerate surfaces.
142
+ */
143
+ readonly surfaces: PluginSurfaces | null;
134
144
  }
135
145
 
136
146
  /** Neither an installed copy nor a marketplace entry claims the name. */
@@ -266,6 +276,7 @@ export async function inspectPlugin(
266
276
  });
267
277
  const installed = entry !== null;
268
278
  const local = entry ? readLocal(entry, readInstallMeta(entry.target)) : null;
279
+ const surfaces = entry ? detectPluginSurfaces(entry.target) : null;
269
280
 
270
281
  let remote: PluginRemoteInfo | null = null;
271
282
  let remoteError: string | null = null;
@@ -294,7 +305,7 @@ export async function inspectPlugin(
294
305
  }
295
306
 
296
307
  const status = classify(installed, local, remote, remoteError);
297
- return { name, installed, status, local, remote, remoteError };
308
+ return { name, installed, status, local, remote, remoteError, surfaces };
298
309
  }
299
310
 
300
311
  function classify(
@@ -221,7 +221,7 @@ function isTransientUpstreamStatus(res: Response): boolean {
221
221
  }
222
222
 
223
223
  /** Resolved GitHub coordinates a plugin name is fetched from. */
224
- interface PluginFetchSource {
224
+ export interface PluginFetchSource {
225
225
  readonly owner: string;
226
226
  readonly repo: string;
227
227
  /** Repo-relative directory holding the plugin root; `""` = repo root. */
@@ -376,22 +376,13 @@ export async function installPlugin(
376
376
  let commit: string | null = null;
377
377
  let committedAt: string | null = null;
378
378
  try {
379
- const cloned = await copyExternalViaGit(
380
- source,
381
- stagingDir,
382
- deps.runGit ?? defaultGitRunner,
379
+ const materialized = await materializePluginTree(
380
+ { source, name, stubRef: marketplaceRef, destDir: stagingDir },
381
+ deps,
383
382
  );
384
- fileCount = cloned.fileCount;
385
- commit = cloned.commit;
386
- committedAt = cloned.committedAt;
387
- // An external clone is often a foreign-ecosystem plugin (e.g. a Claude
388
- // Code plugin) that the Vellum loader can't run as-is. When we curate an
389
- // adapter stub for it, overlay the stub and run its transform so the
390
- // materialized tree is a valid Vellum plugin. Raw clones (no stub) are
391
- // left untouched.
392
- if (fileCount > 0) {
393
- await applyAdapterStub(name, marketplaceRef, stagingDir, deps);
394
- }
383
+ fileCount = materialized.fileCount;
384
+ commit = materialized.commit;
385
+ committedAt = materialized.committedAt;
395
386
  } catch (err) {
396
387
  rmSync(stagingDir, { recursive: true, force: true });
397
388
  throw err;
@@ -402,6 +393,52 @@ export async function installPlugin(
402
393
  throw new PluginNotFoundError(name, ref, sourceLabel(source));
403
394
  }
404
395
 
396
+ finalizeStagedInstall(stagingDir, {
397
+ name,
398
+ source,
399
+ ref,
400
+ commit,
401
+ committedAt,
402
+ pluginsDir,
403
+ });
404
+
405
+ return { name, target, fileCount, ref, commit, committedAt };
406
+ }
407
+
408
+ /** Inputs for {@link finalizeStagedInstall}. */
409
+ export interface FinalizeStagedInstallParams {
410
+ readonly name: string;
411
+ /** Source coordinates recorded in the provenance sidecar. */
412
+ readonly source: PluginFetchSource;
413
+ /** Ref recorded in the sidecar (the resolved commit SHA for marketplace installs). */
414
+ readonly ref: string;
415
+ readonly commit: string | null;
416
+ /** ISO-8601 committer timestamp of {@link FinalizeStagedInstallParams.commit} (UTC); null when unknown. */
417
+ readonly committedAt: string | null;
418
+ /** Served plugins directory; the staging dir is swapped into `<pluginsDir>/<name>`. */
419
+ readonly pluginsDir: string;
420
+ }
421
+
422
+ /**
423
+ * Fingerprint a fully-populated `stagingDir`, write its provenance sidecar, and
424
+ * atomically swap it into `<pluginsDir>/<name>`. Returns the final install path
425
+ * and the fingerprint that was recorded.
426
+ *
427
+ * Shared by {@link installPlugin} (fresh materialization) and the merge-based
428
+ * `plugins upgrade --strategy` path so both record identical provenance and use
429
+ * the same atomic rm+rename swap.
430
+ */
431
+ export function finalizeStagedInstall(
432
+ stagingDir: string,
433
+ {
434
+ name,
435
+ source,
436
+ ref,
437
+ commit,
438
+ committedAt,
439
+ pluginsDir,
440
+ }: FinalizeStagedInstallParams,
441
+ ): { target: string; fingerprint: Fingerprint } {
405
442
  // Hash the materialized tree before the sidecar is written (so the sidecar
406
443
  // never hashes itself) — the baseline `plugins inspect` uses to detect later
407
444
  // local edits. The per-file fingerprint answers "which files changed"; the
@@ -428,13 +465,14 @@ export async function installPlugin(
428
465
  // rm and the rename — and at that point the staging dir is fully populated.
429
466
  // Ensure the served `plugins/` directory exists: staging now lives outside
430
467
  // it, so the target's parent is no longer created as a side effect.
468
+ const target = join(pluginsDir, name);
431
469
  mkdirSync(pluginsDir, { recursive: true });
432
470
  if (existsSync(target)) {
433
471
  rmSync(target, { recursive: true, force: true });
434
472
  }
435
473
  renameSync(stagingDir, target);
436
474
 
437
- return { name, target, fileCount, ref, commit, committedAt };
475
+ return { target, fingerprint };
438
476
  }
439
477
 
440
478
  /** Cap on any single git invocation; a shallow fetch is well under this. */
@@ -523,6 +561,56 @@ export interface InstallMeta {
523
561
  readonly fingerprint: Fingerprint | null;
524
562
  }
525
563
 
564
+ /** Outcome of materializing an external plugin tree into a directory. */
565
+ export interface MaterializedTree {
566
+ /** Number of regular files written into the destination. */
567
+ readonly fileCount: number;
568
+ /** Commit SHA the source was cloned at; `null` when it could not be read. */
569
+ readonly commit: string | null;
570
+ /** ISO-8601 committer timestamp of {@link MaterializedTree.commit}; `null` when unread. */
571
+ readonly committedAt: string | null;
572
+ }
573
+
574
+ /**
575
+ * Produce the exact plugin tree an install stages, into `destDir`: clone the
576
+ * source at `source.ref`, then overlay the curated adapter stub (when one
577
+ * exists) so a foreign-ecosystem clone is translated into Vellum shape.
578
+ *
579
+ * `installPlugin` calls this and then fingerprints, records provenance, and
580
+ * swaps the result into the workspace. `diffPlugin` (see {@link ./diff-plugin})
581
+ * calls it with the *recorded install commit* to reconstruct the install-time
582
+ * baseline. Routing both through one path guarantees a re-materialized commit
583
+ * is byte-identical to what the original install produced, so an install-time
584
+ * adapter transform never reads as local drift when diffing.
585
+ */
586
+ export async function materializePluginTree(
587
+ opts: {
588
+ /** Source coordinates; `source.ref` selects the commit to clone. */
589
+ readonly source: PluginFetchSource;
590
+ /** Install name, used to locate the curated adapter stub. */
591
+ readonly name: string;
592
+ /** Ref the curated adapter stub is fetched at (the canonical repo ref). */
593
+ readonly stubRef: string;
594
+ /** Directory the tree is written into. */
595
+ readonly destDir: string;
596
+ },
597
+ deps: InstallPluginDeps,
598
+ ): Promise<MaterializedTree> {
599
+ const cloned = await copyExternalViaGit(
600
+ opts.source,
601
+ opts.destDir,
602
+ deps.runGit ?? defaultGitRunner,
603
+ );
604
+ // An external clone is often a foreign-ecosystem plugin (e.g. a Claude Code
605
+ // plugin) that the Vellum loader can't run as-is. When we curate an adapter
606
+ // stub for it, overlay the stub and run its transform so the materialized
607
+ // tree is a valid Vellum plugin. Raw clones (no stub) are left untouched.
608
+ if (cloned.fileCount > 0) {
609
+ await applyAdapterStub(opts.name, opts.stubRef, opts.destDir, deps);
610
+ }
611
+ return cloned;
612
+ }
613
+
526
614
  /**
527
615
  * Materialize an external plugin by shallow-cloning its repo at the pinned ref.
528
616
  *
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Three-way merge of a plugin tree, used by `plugins upgrade --strategy` to
3
+ * carry local edits forward across an upgrade instead of discarding them.
4
+ *
5
+ * An upgrade has three inputs, exactly like a git merge:
6
+ * - **base** — the tree the plugin was installed at (the recorded commit,
7
+ * re-materialized through the install pipeline; see {@link ./diff-plugin}).
8
+ * - **ours** — the current on-disk install, carrying any local edits.
9
+ * - **theirs** — the marketplace's current pin, the tree being upgraded to.
10
+ *
11
+ * Per file the merge is delegated to `git merge-file`, the same line-level
12
+ * three-way algorithm git itself uses: a hunk edited on only one side is taken
13
+ * from that side, so non-conflicting edits from *both* sides survive. Only a
14
+ * hunk edited differently on both sides is a true conflict, resolved per the
15
+ * caller's strategy:
16
+ * - `ours` keeps the local hunk (`git merge-file --ours`),
17
+ * - `theirs` keeps the pinned hunk (`git merge-file --theirs`),
18
+ * - `assistant` writes standard git conflict markers into the file (no resolve
19
+ * flag) and reports the path, leaving the conflict for the assistant to
20
+ * resolve — exactly the mid-merge working-tree UX.
21
+ *
22
+ * File-level add/delete divergence (a file added, removed, or modified on only
23
+ * one side) is resolved here before any line merge, since `git merge-file`
24
+ * operates on three existing blobs.
25
+ *
26
+ * Binary files cannot be line-merged, so a binary file that diverged on both
27
+ * sides is resolved whole-file by the strategy (`ours`/`theirs`); under
28
+ * `assistant` the local copy is kept and the path is reported as a binary
29
+ * conflict, since markers cannot be written into binary content.
30
+ *
31
+ * The `overwrite` strategy never reaches here — it discards local edits and is
32
+ * a plain re-install at the pin.
33
+ */
34
+
35
+ import { execFile } from "node:child_process";
36
+ import {
37
+ mkdirSync,
38
+ mkdtempSync,
39
+ readFileSync,
40
+ rmSync,
41
+ writeFileSync,
42
+ } from "node:fs";
43
+ import { tmpdir } from "node:os";
44
+ import { dirname, join } from "node:path";
45
+ import { promisify } from "node:util";
46
+
47
+ import { INSTALL_META_FILENAME } from "./install-from-github.js";
48
+ import { computeFingerprint } from "./plugin-fingerprint.js";
49
+
50
+ const execFileAsync = promisify(execFile);
51
+
52
+ /** Cap on a single `git merge-file`; a per-file line merge is near-instant. */
53
+ const MERGE_TIMEOUT_MS = 30_000;
54
+
55
+ /**
56
+ * How hunks edited differently on both sides are reconciled. `ours`/`theirs`
57
+ * auto-resolve toward that side; `assistant` writes conflict markers and
58
+ * reports the conflict for later resolution. `overwrite` is not a merge
59
+ * strategy — the caller re-installs the pin wholesale instead of merging.
60
+ */
61
+ export type PluginMergeStrategy = "ours" | "theirs" | "assistant";
62
+
63
+ /** Human-readable labels for the conflict markers `assistant` writes. */
64
+ export interface ConflictLabels {
65
+ readonly ours: string;
66
+ readonly base: string;
67
+ readonly theirs: string;
68
+ }
69
+
70
+ const DEFAULT_CONFLICT_LABELS: ConflictLabels = {
71
+ ours: "local edits",
72
+ base: "install baseline",
73
+ theirs: "upgrade pin",
74
+ };
75
+
76
+ /** Inputs for a three-way plugin-tree merge. */
77
+ export interface MergePluginTreeOptions {
78
+ /** Re-materialized install-commit tree (the merge base). */
79
+ readonly baseDir: string;
80
+ /** Current on-disk install, carrying local edits (`ours`). */
81
+ readonly oursDir: string;
82
+ /** Marketplace-pinned tree being upgraded to (`theirs`). */
83
+ readonly theirsDir: string;
84
+ /** Empty directory the merged tree is written into. */
85
+ readonly destDir: string;
86
+ /** How to resolve hunks edited differently on both sides. */
87
+ readonly strategy: PluginMergeStrategy;
88
+ /** Marker labels for the `assistant` strategy. Defaults are used when omitted. */
89
+ readonly conflictLabels?: ConflictLabels;
90
+ }
91
+
92
+ /** Outcome of a three-way plugin-tree merge. */
93
+ export interface PluginMergeResult {
94
+ /** Number of files written into the destination tree. */
95
+ readonly fileCount: number;
96
+ /**
97
+ * Paths (relative to the tree root) left for the assistant to resolve.
98
+ * Text files carry git conflict markers; modify/delete divergences keep the
99
+ * surviving content. Empty for `ours`/`theirs`, which auto-resolve.
100
+ */
101
+ readonly conflicts: readonly string[];
102
+ /**
103
+ * Paths of binary files that diverged on both sides. The local copy is kept
104
+ * (markers cannot be written into binary content), so the assistant must
105
+ * choose a version rather than edit markers. Empty for `ours`/`theirs`.
106
+ */
107
+ readonly binaryConflicts: readonly string[];
108
+ }
109
+
110
+ /** Result of merging a single file. */
111
+ interface MergedFile {
112
+ readonly content: Buffer;
113
+ /** Conflict markers were written into `content` (text, `assistant` only). */
114
+ readonly conflicted: boolean;
115
+ /** A binary file conflicted; `content` is the kept local copy (`assistant`). */
116
+ readonly binaryConflicted: boolean;
117
+ }
118
+
119
+ /** A NUL byte in the leading bytes is the heuristic git uses to flag a blob as binary. */
120
+ function isBinary(buf: Buffer): boolean {
121
+ const len = Math.min(buf.length, 8000);
122
+ for (let i = 0; i < len; i++) {
123
+ if (buf[i] === 0) return true;
124
+ }
125
+ return false;
126
+ }
127
+
128
+ /** Write `content` to `destDir/rel`, creating parent directories as needed. */
129
+ function writeInto(destDir: string, rel: string, content: Buffer): void {
130
+ const abs = join(destDir, rel);
131
+ mkdirSync(dirname(abs), { recursive: true });
132
+ writeFileSync(abs, content);
133
+ }
134
+
135
+ /**
136
+ * Line-merge three blobs with `git merge-file`. Non-conflicting hunks from both
137
+ * sides are always kept. `ours`/`theirs` auto-resolve conflicting hunks toward
138
+ * that side (no markers); `assistant` writes git conflict markers and flags the
139
+ * file as conflicted.
140
+ *
141
+ * Binary input cannot be line-merged: a side that matches the base did not
142
+ * change, so the other side's edit is taken. A binary blob changed on *both*
143
+ * sides is a true conflict — resolved whole-file by `ours`/`theirs`, or kept as
144
+ * the local copy and flagged under `assistant` (no markers possible).
145
+ */
146
+ async function threeWayMergeFile(
147
+ ours: Buffer,
148
+ base: Buffer,
149
+ theirs: Buffer,
150
+ strategy: PluginMergeStrategy,
151
+ labels: ConflictLabels,
152
+ ): Promise<MergedFile> {
153
+ if (isBinary(ours) || isBinary(base) || isBinary(theirs)) {
154
+ if (ours.equals(base))
155
+ return { content: theirs, conflicted: false, binaryConflicted: false };
156
+ if (theirs.equals(base))
157
+ return { content: ours, conflicted: false, binaryConflicted: false };
158
+ if (strategy === "assistant") {
159
+ return { content: ours, conflicted: false, binaryConflicted: true };
160
+ }
161
+ return {
162
+ content: strategy === "ours" ? ours : theirs,
163
+ conflicted: false,
164
+ binaryConflicted: false,
165
+ };
166
+ }
167
+
168
+ const scratch = mkdtempSync(join(tmpdir(), "plugin-merge-file-"));
169
+ try {
170
+ const oursPath = join(scratch, "ours");
171
+ const basePath = join(scratch, "base");
172
+ const theirsPath = join(scratch, "theirs");
173
+ writeFileSync(oursPath, ours);
174
+ writeFileSync(basePath, base);
175
+ writeFileSync(theirsPath, theirs);
176
+
177
+ // `-p` prints the merged result to stdout instead of editing `ours` in
178
+ // place. `--ours`/`--theirs` auto-resolve conflicting hunks toward that
179
+ // side; with neither (the `assistant` strategy) git writes conflict
180
+ // markers, labelled via `-L` so the assistant can tell the sides apart.
181
+ const args = ["merge-file", "-p"];
182
+ if (strategy === "ours" || strategy === "theirs") {
183
+ args.push(`--${strategy}`);
184
+ } else {
185
+ args.push("-L", labels.ours, "-L", labels.base, "-L", labels.theirs);
186
+ }
187
+ args.push(oursPath, basePath, theirsPath);
188
+ try {
189
+ const { stdout } = await execFileAsync("git", args, {
190
+ cwd: scratch,
191
+ encoding: "buffer",
192
+ timeout: MERGE_TIMEOUT_MS,
193
+ maxBuffer: 64 * 1024 * 1024,
194
+ });
195
+ return { content: stdout, conflicted: false, binaryConflicted: false };
196
+ } catch (err) {
197
+ // `git merge-file` exits with the (positive) conflict count when markers
198
+ // were left, still printing the full merged bytes to stdout — that is the
199
+ // only rejection we may install. A killed process (timeout) or a
200
+ // `maxBuffer` overflow also rejects here, but with truncated/partial
201
+ // stdout we must NOT install it, or a large conflicted file would be
202
+ // silently corrupted; surface those as errors. A resolving flag never
203
+ // leaves conflicts, so any non-zero exit there is likewise a real
204
+ // failure.
205
+ const e = err as {
206
+ stdout?: Buffer;
207
+ code?: unknown;
208
+ killed?: boolean;
209
+ signal?: string | null;
210
+ };
211
+ const isConflictExit =
212
+ strategy === "assistant" &&
213
+ e.killed !== true &&
214
+ e.signal == null &&
215
+ typeof e.code === "number" &&
216
+ e.code > 0;
217
+ if (isConflictExit && Buffer.isBuffer(e.stdout)) {
218
+ return { content: e.stdout, conflicted: true, binaryConflicted: false };
219
+ }
220
+ throw err;
221
+ }
222
+ } finally {
223
+ rmSync(scratch, { recursive: true, force: true });
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Three-way merge `oursDir`/`theirsDir` against `baseDir` into `destDir` per
229
+ * `strategy`. The provenance sidecar is excluded on every side — it is
230
+ * rewritten by the caller after the swap and must not be carried through the
231
+ * merge.
232
+ *
233
+ * Returns the file count plus, for the `assistant` strategy, the paths left for
234
+ * the assistant to resolve (text files with conflict markers and modify/delete
235
+ * divergences) and the binary files that conflicted.
236
+ */
237
+ export async function mergePluginTree({
238
+ baseDir,
239
+ oursDir,
240
+ theirsDir,
241
+ destDir,
242
+ strategy,
243
+ conflictLabels,
244
+ }: MergePluginTreeOptions): Promise<PluginMergeResult> {
245
+ const labels = conflictLabels ?? DEFAULT_CONFLICT_LABELS;
246
+ const exclude = [INSTALL_META_FILENAME];
247
+ const base = computeFingerprint(baseDir, exclude).files;
248
+ const ours = computeFingerprint(oursDir, exclude).files;
249
+ const theirs = computeFingerprint(theirsDir, exclude).files;
250
+
251
+ const paths = new Set([
252
+ ...Object.keys(base),
253
+ ...Object.keys(ours),
254
+ ...Object.keys(theirs),
255
+ ]);
256
+
257
+ const readBase = (rel: string) => readFileSync(join(baseDir, rel));
258
+ const readOurs = (rel: string) => readFileSync(join(oursDir, rel));
259
+ const readTheirs = (rel: string) => readFileSync(join(theirsDir, rel));
260
+
261
+ let fileCount = 0;
262
+ const conflicts: string[] = [];
263
+ const binaryConflicts: string[] = [];
264
+ const keep = (rel: string, content: Buffer): void => {
265
+ writeInto(destDir, rel, content);
266
+ fileCount++;
267
+ };
268
+
269
+ for (const rel of paths) {
270
+ const b = base[rel];
271
+ const o = ours[rel];
272
+ const t = theirs[rel];
273
+
274
+ if (o !== undefined && t !== undefined) {
275
+ // Present on both sides: identical needs no merge; otherwise line-merge
276
+ // (an empty base when the file was added on both sides).
277
+ if (o === t) {
278
+ keep(rel, readOurs(rel));
279
+ } else {
280
+ const merged = await threeWayMergeFile(
281
+ readOurs(rel),
282
+ b !== undefined ? readBase(rel) : Buffer.alloc(0),
283
+ readTheirs(rel),
284
+ strategy,
285
+ labels,
286
+ );
287
+ keep(rel, merged.content);
288
+ if (merged.conflicted) conflicts.push(rel);
289
+ if (merged.binaryConflicted) binaryConflicts.push(rel);
290
+ }
291
+ continue;
292
+ }
293
+
294
+ if (o !== undefined) {
295
+ // Present only locally. A local-only addition (no base) always survives.
296
+ // A file deleted upstream is a delete: drop it when unchanged locally.
297
+ // On a modify/delete conflict `ours` keeps the edit and `theirs` honors
298
+ // the deletion; `assistant` keeps the edit and flags it (a whole-file
299
+ // conflict markers can't express).
300
+ if (b === undefined || (b !== o && strategy !== "theirs")) {
301
+ keep(rel, readOurs(rel));
302
+ if (b !== undefined && b !== o && strategy === "assistant") {
303
+ conflicts.push(rel);
304
+ }
305
+ }
306
+ continue;
307
+ }
308
+
309
+ if (t !== undefined) {
310
+ // Present only at the pin. A remote-only addition (no base) always lands.
311
+ // A file deleted locally is a delete: keep upstream's removal when the
312
+ // pin left it unchanged. On a delete/modify conflict `theirs` keeps the
313
+ // pin's edit and `ours` honors the local deletion; `assistant` keeps the
314
+ // pin's edit and flags it.
315
+ if (b === undefined || (b !== t && strategy !== "ours")) {
316
+ keep(rel, readTheirs(rel));
317
+ if (b !== undefined && b !== t && strategy === "assistant") {
318
+ conflicts.push(rel);
319
+ }
320
+ }
321
+ continue;
322
+ }
323
+
324
+ // Present only in the base: removed on both sides, so it stays removed.
325
+ }
326
+
327
+ return { fileCount, conflicts, binaryConflicts };
328
+ }
@@ -120,6 +120,20 @@ export function compareFingerprint(
120
120
  };
121
121
  }
122
122
 
123
+ /**
124
+ * Whether two fingerprints cover the same files with identical digests. Used to
125
+ * confirm a re-materialized tree faithfully reproduces a recorded baseline
126
+ * before it is trusted as a merge base.
127
+ */
128
+ export function fingerprintsEqual(a: Fingerprint, b: Fingerprint): boolean {
129
+ const aKeys = Object.keys(a.files);
130
+ if (aKeys.length !== Object.keys(b.files).length) return false;
131
+ for (const key of aKeys) {
132
+ if (a.files[key] !== b.files[key]) return false;
133
+ }
134
+ return true;
135
+ }
136
+
123
137
  /**
124
138
  * Parse a fingerprint from already-decoded JSON. Lenient by design — any shape
125
139
  * problem yields `null` so an older or hand-edited sidecar simply reports "no
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Detect the surfaces an installed plugin contributes to the running assistant,
3
+ * read directly from its on-disk tree.
4
+ *
5
+ * The host discovers a plugin's contributions from fixed directory conventions:
6
+ *
7
+ * - `hooks/<name>.{ts,js}` → a lifecycle hook keyed by the file basename
8
+ * (see the external plugin loader's `loadHooks`).
9
+ * - `tools/<name>.{ts,js}` → a tool, also keyed by the file basename
10
+ * (see the external plugin loader's tool walk).
11
+ * - `skills/<id>/SKILL.md` → a skill owned by the plugin (see the skills
12
+ * catalog's `discoverPluginResidentSkills`).
13
+ *
14
+ * This module re-derives those same sets so `plugins inspect` can report exactly
15
+ * what a plugin contributes. Detection is intentionally a self-contained walk of
16
+ * the install tree — `cli/lib` does not reach into the daemon-internal loader or
17
+ * skills catalog — but it mirrors their conventions so inspect agrees with what
18
+ * the runtime actually loads.
19
+ */
20
+
21
+ import { existsSync, readdirSync, statSync } from "node:fs";
22
+ import { join } from "node:path";
23
+
24
+ /** The surfaces an installed plugin contributes, each sorted and de-duplicated. */
25
+ export interface PluginSurfaces {
26
+ /** Skill ids shipped at `skills/<id>/SKILL.md`. */
27
+ readonly skills: readonly string[];
28
+ /** Lifecycle hook names from `hooks/<name>.{ts,js}` (e.g. `pre-model-call`). */
29
+ readonly hooks: readonly string[];
30
+ /**
31
+ * Registered tool names from `tools/<name>.{ts,js}`. The loader derives a
32
+ * tool's name from its filename via {@link deriveToolName} (e.g.
33
+ * `create-issue.ts` registers as `create_issue`), so the derived form is
34
+ * reported rather than the raw basename. A tool module that overrides its own
35
+ * name via an exported `name` is not reflected here: that would require
36
+ * importing and executing untrusted plugin code, which inspection avoids.
37
+ */
38
+ readonly tools: readonly string[];
39
+ }
40
+
41
+ /**
42
+ * Derive a tool's registered name from its file basename, mirroring the
43
+ * external plugin loader's `deriveToolName`: non-alphanumeric runs collapse to
44
+ * `_`, leading/trailing `_` are trimmed, and an empty result falls back to
45
+ * `tool`. Keeps the inspected tool name aligned with the callable tool name.
46
+ */
47
+ function deriveToolName(basename: string): string {
48
+ return (
49
+ basename.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "tool"
50
+ );
51
+ }
52
+
53
+ /**
54
+ * List the basenames of every `.ts`/`.js` module directly under `dir`,
55
+ * preferring `.js` over `.ts` for the same basename (compiled-binary semantics)
56
+ * and skipping `.d.ts` declaration files. Returns names sorted for a
57
+ * deterministic listing. A missing or non-directory path yields `[]`.
58
+ *
59
+ * Mirrors the external plugin loader's `listSurfaceDir`, the gate it uses to
60
+ * turn a `hooks/`/`tools/` directory into loadable surfaces.
61
+ */
62
+ function listModuleBasenames(dir: string): string[] {
63
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
64
+ const bases = new Set<string>();
65
+ for (const entry of readdirSync(dir)) {
66
+ if (entry.endsWith(".d.ts")) continue;
67
+ if (!entry.endsWith(".ts") && !entry.endsWith(".js")) continue;
68
+ bases.add(entry.slice(0, -3));
69
+ }
70
+ return [...bases].sort();
71
+ }
72
+
73
+ /**
74
+ * List the skill ids a plugin ships: each subdirectory of `skills/` that
75
+ * contains a `SKILL.md`. Mirrors the skills catalog's plugin-resident skill
76
+ * discovery so inspect reports the same set the runtime would surface.
77
+ */
78
+ function listSkillIds(skillsDir: string): string[] {
79
+ if (!existsSync(skillsDir) || !statSync(skillsDir).isDirectory()) return [];
80
+ const ids: string[] = [];
81
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
82
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
83
+ if (existsSync(join(skillsDir, entry.name, "SKILL.md"))) {
84
+ ids.push(entry.name);
85
+ }
86
+ }
87
+ return ids.sort();
88
+ }
89
+
90
+ /**
91
+ * Detect the {@link PluginSurfaces} an installed plugin contributes by walking
92
+ * its install tree at `pluginDir`. Surface types with no contributions come
93
+ * back as empty arrays; callers omit empty types from the rendered output.
94
+ */
95
+ export function detectPluginSurfaces(pluginDir: string): PluginSurfaces {
96
+ const toolNames = listModuleBasenames(join(pluginDir, "tools")).map(
97
+ deriveToolName,
98
+ );
99
+ return {
100
+ skills: listSkillIds(join(pluginDir, "skills")),
101
+ hooks: listModuleBasenames(join(pluginDir, "hooks")),
102
+ tools: [...new Set(toolNames)].sort(),
103
+ };
104
+ }