@vellumai/assistant 0.9.1-staging.1 → 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 (433) hide show
  1. package/docs/activation-funnel-telemetry.md +24 -18
  2. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +97 -0
  3. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  4. package/node_modules/@vellumai/gateway-client/src/index.ts +15 -0
  5. package/openapi.yaml +852 -15
  6. package/package.json +1 -1
  7. package/src/__tests__/access-request-card-view.test.ts +98 -0
  8. package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
  9. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +59 -7
  10. package/src/__tests__/agent-loop-compaction-strip.test.ts +17 -16
  11. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
  12. package/src/__tests__/app-compiler.test.ts +15 -1
  13. package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
  14. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  15. package/src/__tests__/cancel-clears-processing.test.ts +89 -0
  16. package/src/__tests__/channel-approval-routes.test.ts +0 -4
  17. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
  18. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
  19. package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
  20. package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
  21. package/src/__tests__/config-loader-backfill.test.ts +174 -30
  22. package/src/__tests__/config-schema.test.ts +35 -0
  23. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
  24. package/src/__tests__/contact-store-user-file.test.ts +0 -6
  25. package/src/__tests__/contacts-tools.test.ts +29 -0
  26. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  27. package/src/__tests__/conversation-agent-loop.test.ts +58 -0
  28. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  29. package/src/__tests__/conversation-lifecycle.test.ts +7 -9
  30. package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
  31. package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
  32. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
  33. package/src/__tests__/conversation-title-service.test.ts +62 -0
  34. package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
  35. package/src/__tests__/credential-prompt-route.test.ts +1 -0
  36. package/src/__tests__/credential-security-invariants.test.ts +2 -0
  37. package/src/__tests__/disk-pressure-policy.test.ts +12 -0
  38. package/src/__tests__/disk-usage.test.ts +65 -0
  39. package/src/__tests__/dynamic-page-surface.test.ts +51 -0
  40. package/src/__tests__/gateway-flag-listener.test.ts +110 -1
  41. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  42. package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
  43. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  44. package/src/__tests__/guardian-grant-minting.test.ts +3 -35
  45. package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
  46. package/src/__tests__/guardian-routing-state.test.ts +0 -1
  47. package/src/__tests__/headless-browser-mode.test.ts +10 -0
  48. package/src/__tests__/headless-browser-navigate.test.ts +8 -3
  49. package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
  50. package/src/__tests__/host-browser-proxy.test.ts +87 -0
  51. package/src/__tests__/injector-v3-suppression.test.ts +27 -20
  52. package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
  53. package/src/__tests__/invite-redemption-service.test.ts +0 -3
  54. package/src/__tests__/llm-catalog-parity.test.ts +30 -1
  55. package/src/__tests__/llm-resolver.test.ts +21 -0
  56. package/src/__tests__/llm-schema.test.ts +1 -0
  57. package/src/__tests__/managed-profile-guard.test.ts +163 -4
  58. package/src/__tests__/mcp-health-check.test.ts +6 -7
  59. package/src/__tests__/media-stream-server-integration.test.ts +317 -13
  60. package/src/__tests__/path-policy.test.ts +34 -0
  61. package/src/__tests__/persona-resolver.test.ts +38 -0
  62. package/src/__tests__/plugin-api-provider.test.ts +24 -0
  63. package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
  64. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
  65. package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
  66. package/src/__tests__/reaction-persistence.test.ts +150 -29
  67. package/src/__tests__/relay-server.test.ts +285 -0
  68. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  69. package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
  70. package/src/__tests__/skill-execute-input.test.ts +5 -0
  71. package/src/__tests__/skills.test.ts +51 -0
  72. package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
  73. package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
  74. package/src/__tests__/subagent-tools.test.ts +150 -0
  75. package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
  76. package/src/__tests__/title-generate-hook.test.ts +100 -3
  77. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
  78. package/src/__tests__/tool-audit-listener.test.ts +7 -7
  79. package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
  80. package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
  81. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
  82. package/src/__tests__/trusted-contact-verification.test.ts +2 -4
  83. package/src/__tests__/twilio-routes.test.ts +81 -1
  84. package/src/__tests__/voice-invite-redemption.test.ts +0 -1
  85. package/src/__tests__/weak-open-model.test.ts +30 -0
  86. package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
  87. package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
  88. package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
  89. package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
  90. package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
  91. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
  92. package/src/agent/loop.ts +33 -33
  93. package/src/api/events/tool-result.ts +6 -0
  94. package/src/api/events/workflow-completed.ts +53 -0
  95. package/src/api/events/workflow-leaf-finished.ts +38 -0
  96. package/src/api/events/workflow-leaf-started.ts +35 -0
  97. package/src/api/events/workflow-progress.ts +32 -0
  98. package/src/api/events/workflow-started.ts +31 -0
  99. package/src/api/index.ts +40 -0
  100. package/src/api/responses/conversation-message.ts +26 -0
  101. package/src/api/responses/home.ts +26 -0
  102. package/src/api/responses/workflow-journal.ts +53 -0
  103. package/src/approvals/guardian-card-withdrawal.ts +145 -0
  104. package/src/approvals/guardian-decision-primitive.ts +26 -3
  105. package/src/approvals/guardian-request-resolvers.ts +181 -78
  106. package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
  107. package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
  108. package/src/calls/call-pointer-messages.ts +10 -4
  109. package/src/calls/channel-admission-reader.ts +104 -0
  110. package/src/calls/guardian-dispatch.ts +17 -45
  111. package/src/calls/media-stream-server.ts +84 -2
  112. package/src/calls/relay-server.ts +66 -0
  113. package/src/calls/relay-setup-router.ts +82 -1
  114. package/src/calls/twilio-routes.ts +17 -8
  115. package/src/cli/commands/clients.ts +3 -0
  116. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
  117. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
  118. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
  119. package/src/cli/commands/memory/index.ts +30 -0
  120. package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
  121. package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
  122. package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
  123. package/src/cli/commands/oauth/status.test.ts +36 -0
  124. package/src/cli/commands/oauth/status.ts +23 -3
  125. package/src/cli/commands/plugins.ts +57 -5
  126. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  127. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +134 -4
  128. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
  129. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +53 -11
  130. package/src/cli/lib/inspect-plugin.ts +12 -1
  131. package/src/cli/lib/merge-plugin-tree.ts +149 -49
  132. package/src/cli/lib/plugin-surfaces.ts +104 -0
  133. package/src/cli/lib/upgrade-plugin.ts +64 -36
  134. package/src/cli/program.ts +2 -4
  135. package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
  136. package/src/config/assistant-feature-flags.ts +22 -7
  137. package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
  138. package/src/config/bundled-skills/messaging/SKILL.md +6 -4
  139. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
  140. package/src/config/bundled-skills/workflows/SKILL.md +14 -7
  141. package/src/config/call-site-defaults.ts +3 -0
  142. package/src/config/feature-flag-registry.json +49 -18
  143. package/src/config/llm-resolver.ts +3 -0
  144. package/src/config/memory-v3-gate.ts +11 -0
  145. package/src/config/schema.ts +8 -6
  146. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  147. package/src/config/schemas/call-site-catalog.ts +7 -0
  148. package/src/config/schemas/channels.ts +11 -0
  149. package/src/config/schemas/llm.ts +31 -0
  150. package/src/config/schemas/memory-lifecycle.ts +3 -7
  151. package/src/config/schemas/memory-v3.ts +6 -0
  152. package/src/config/schemas/services.ts +18 -0
  153. package/src/config/seed-inference-profiles.ts +94 -34
  154. package/src/config/skills.ts +21 -0
  155. package/src/config/sync-gated-profiles.ts +220 -0
  156. package/src/contacts/contact-store.ts +2 -10
  157. package/src/contacts/contacts-write.ts +1 -2
  158. package/src/contacts/types.ts +0 -1
  159. package/src/context/compactor.ts +86 -52
  160. package/src/context/strip-injections.ts +58 -10
  161. package/src/context/token-estimator.ts +1 -1
  162. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
  163. package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
  164. package/src/daemon/conversation-agent-loop.ts +100 -19
  165. package/src/daemon/conversation-history.ts +1 -1
  166. package/src/daemon/conversation-lifecycle.ts +3 -5
  167. package/src/daemon/conversation-process.ts +13 -5
  168. package/src/daemon/conversation-runtime-assembly.ts +13 -15
  169. package/src/daemon/conversation-surfaces.ts +26 -0
  170. package/src/daemon/conversation-tool-setup.ts +16 -11
  171. package/src/daemon/conversation.ts +64 -14
  172. package/src/daemon/disk-pressure-policy.ts +5 -3
  173. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
  174. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
  175. package/src/daemon/handlers/config-a2a.ts +0 -2
  176. package/src/daemon/handlers/config-channels.ts +5 -10
  177. package/src/daemon/handlers/config-slack-channel.ts +20 -0
  178. package/src/daemon/handlers/conversations.ts +107 -0
  179. package/src/daemon/host-browser-proxy.ts +41 -0
  180. package/src/daemon/lifecycle.ts +55 -20
  181. package/src/daemon/message-provenance.ts +2 -0
  182. package/src/daemon/message-types/contacts.ts +0 -1
  183. package/src/daemon/message-types/web-activity.ts +7 -1
  184. package/src/daemon/message-types/workflows.ts +83 -1
  185. package/src/daemon/tool-setup-types.ts +4 -0
  186. package/src/daemon/trust-context.ts +1 -1
  187. package/src/events/tool-audit-listener.ts +2 -2
  188. package/src/home/feed-source-enrichment.test.ts +151 -0
  189. package/src/home/feed-source-enrichment.ts +176 -0
  190. package/src/instrument.ts +18 -6
  191. package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
  192. package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
  193. package/src/ipc/assistant-server.ts +37 -4
  194. package/src/ipc/gateway-flag-listener.ts +18 -2
  195. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
  196. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
  197. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
  198. package/src/memory/__tests__/memory-retrospective-job.test.ts +34 -0
  199. package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
  200. package/src/memory/auth-fallback-events-store.ts +2 -2
  201. package/src/memory/auto-analysis-enqueue.ts +3 -5
  202. package/src/memory/canonical-guardian-store.ts +39 -1
  203. package/src/memory/conversation-crud.ts +9 -4
  204. package/src/memory/conversation-key-store.ts +17 -2
  205. package/src/memory/conversation-title-service.ts +64 -7
  206. package/src/memory/db-init.ts +10 -0
  207. package/src/memory/embedding-backend.ts +15 -1
  208. package/src/memory/jobs-worker.ts +2 -1
  209. package/src/memory/lifecycle-events-store.ts +2 -2
  210. package/src/memory/memory-retrospective-enqueue.ts +31 -6
  211. package/src/memory/memory-retrospective-job.ts +9 -0
  212. package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
  213. package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
  214. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
  215. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +10 -0
  216. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
  217. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
  218. package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
  219. package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
  220. package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
  221. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
  222. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
  223. package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
  224. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
  225. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +30 -0
  226. package/src/memory/migrations/index.ts +5 -0
  227. package/src/memory/onboarding-events-store.ts +3 -3
  228. package/src/memory/schema/contacts.ts +0 -1
  229. package/src/memory/skill-loaded-events-store.test.ts +7 -15
  230. package/src/memory/skill-loaded-events-store.ts +2 -2
  231. package/src/memory/tool-executed-events-store.test.ts +7 -7
  232. package/src/memory/turn-trace-store.test.ts +736 -0
  233. package/src/memory/turn-trace-store.ts +364 -0
  234. package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
  235. package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
  236. package/src/memory/v2/consolidation-job.ts +2 -2
  237. package/src/memory/v2/skill-content.ts +25 -7
  238. package/src/memory/v2/skill-store.ts +7 -1
  239. package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
  240. package/src/memory/v3-eval/eval-packets.ts +546 -0
  241. package/src/messaging/providers/slack/api.ts +31 -0
  242. package/src/messaging/providers/slack/send.test.ts +114 -2
  243. package/src/messaging/providers/slack/send.ts +30 -7
  244. package/src/messaging/providers/slack/withdraw.test.ts +200 -0
  245. package/src/messaging/providers/slack/withdraw.ts +161 -0
  246. package/src/notifications/AGENTS.md +2 -0
  247. package/src/notifications/access-request-copy.ts +72 -59
  248. package/src/notifications/adapters/slack.ts +55 -73
  249. package/src/notifications/approval-card-data.ts +333 -0
  250. package/src/notifications/broadcaster.ts +6 -2
  251. package/src/notifications/canonical-delivery-recorder.ts +139 -0
  252. package/src/notifications/copy-composer.ts +3 -3
  253. package/src/notifications/decision-engine.ts +4 -2
  254. package/src/notifications/destination-resolver.ts +4 -6
  255. package/src/notifications/guardian-question-mode.ts +10 -0
  256. package/src/notifications/home-feed-side-effect.ts +3 -13
  257. package/src/notifications/notification-utils.ts +2 -1
  258. package/src/notifications/signal.ts +79 -43
  259. package/src/notifications/types.ts +98 -128
  260. package/src/permissions/checker.test.ts +51 -0
  261. package/src/permissions/checker.ts +185 -26
  262. package/src/permissions/ipc-risk-types.ts +24 -0
  263. package/src/permissions/question-prompter.test.ts +27 -0
  264. package/src/permissions/question-prompter.ts +4 -0
  265. package/src/platform/client.test.ts +119 -0
  266. package/src/platform/client.ts +66 -0
  267. package/src/platform/consent-cache.test.ts +267 -0
  268. package/src/platform/consent-cache.ts +174 -0
  269. package/src/plugin-api/index.ts +27 -0
  270. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
  271. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
  272. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
  273. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
  274. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
  275. package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
  276. package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
  277. package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
  278. package/src/plugins/defaults/advisor/config.ts +21 -0
  279. package/src/plugins/defaults/advisor/consult.ts +93 -0
  280. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
  281. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
  282. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
  283. package/src/plugins/defaults/advisor/package.json +14 -0
  284. package/src/plugins/defaults/advisor/steering.ts +67 -0
  285. package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
  286. package/src/plugins/defaults/advisor/transcript.ts +76 -0
  287. package/src/plugins/defaults/index.ts +35 -0
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
  289. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  290. package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
  291. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
  292. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
  293. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +75 -7
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
  297. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
  298. package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
  299. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +37 -4
  300. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
  301. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
  302. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
  303. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +2 -12
  304. package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
  305. package/src/prompts/persona-resolver.ts +12 -2
  306. package/src/prompts/templates/system-sections.ts +7 -2
  307. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  308. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  309. package/src/providers/__tests__/retry-callsite.test.ts +176 -0
  310. package/src/providers/atlascloud/client.ts +85 -0
  311. package/src/providers/fetch-provider-catalog.ts +85 -0
  312. package/src/providers/inference/adapter-factory.ts +3 -0
  313. package/src/providers/model-catalog.ts +58 -0
  314. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
  315. package/src/providers/openai/chat-completions-provider.ts +7 -0
  316. package/src/providers/openai/responses-provider.ts +10 -0
  317. package/src/providers/provider-send-message.ts +11 -3
  318. package/src/providers/retry.ts +53 -12
  319. package/src/providers/search-provider-catalog.ts +10 -0
  320. package/src/providers/weak-open-model.ts +22 -0
  321. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  322. package/src/runtime/__tests__/client-health.test.ts +44 -0
  323. package/src/runtime/access-request-helper.ts +21 -53
  324. package/src/runtime/actor-trust-resolver.ts +49 -21
  325. package/src/runtime/agent-wake.ts +52 -0
  326. package/src/runtime/assistant-event-hub.ts +18 -4
  327. package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
  328. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  329. package/src/runtime/capabilities.test.ts +120 -0
  330. package/src/runtime/capabilities.ts +197 -0
  331. package/src/runtime/channel-approval-types.ts +5 -1
  332. package/src/runtime/channel-retry-sweep.ts +1 -0
  333. package/src/runtime/channel-verification-service.ts +1 -2
  334. package/src/runtime/client-health.ts +26 -0
  335. package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
  336. package/src/runtime/effective-capabilities.test.ts +128 -0
  337. package/src/runtime/effective-capabilities.ts +84 -0
  338. package/src/runtime/guardian-reply-router.ts +106 -21
  339. package/src/runtime/invite-redemption-service.ts +6 -22
  340. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +123 -0
  341. package/src/runtime/migrations/vbundle-builder.ts +49 -20
  342. package/src/runtime/pending-interactions.ts +15 -0
  343. package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
  344. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
  345. package/src/runtime/routes/__tests__/plugins-routes.test.ts +35 -13
  346. package/src/runtime/routes/browser-tabs-routes.ts +9 -0
  347. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
  348. package/src/runtime/routes/client-routes.ts +10 -0
  349. package/src/runtime/routes/contact-routes.ts +31 -8
  350. package/src/runtime/routes/conversation-management-routes.ts +80 -1
  351. package/src/runtime/routes/conversation-query-routes.ts +68 -22
  352. package/src/runtime/routes/conversation-routes.ts +37 -12
  353. package/src/runtime/routes/events-routes.ts +1 -3
  354. package/src/runtime/routes/guardian-approval-interception.ts +14 -73
  355. package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
  356. package/src/runtime/routes/home-feed-routes.ts +8 -3
  357. package/src/runtime/routes/inbound-message-handler.ts +214 -228
  358. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +88 -6
  359. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
  360. package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
  361. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
  362. package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
  363. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
  364. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
  365. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
  366. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
  367. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
  368. package/src/runtime/routes/index.ts +2 -0
  369. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
  370. package/src/runtime/routes/integrations/slack/channel.ts +36 -0
  371. package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
  372. package/src/runtime/routes/mcp-auth-routes.ts +233 -41
  373. package/src/runtime/routes/memory-eval-routes.ts +87 -0
  374. package/src/runtime/routes/notification-routes.ts +122 -133
  375. package/src/runtime/routes/platform-routes.ts +2 -2
  376. package/src/runtime/routes/plugins-routes.ts +40 -7
  377. package/src/runtime/routes/secret-routes.ts +10 -0
  378. package/src/runtime/routes/surface-action-routes.ts +2 -1
  379. package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
  380. package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
  381. package/src/runtime/routes/workflow-routes.test.ts +225 -1
  382. package/src/runtime/routes/workflow-routes.ts +131 -1
  383. package/src/runtime/tool-grant-request-helper.ts +18 -16
  384. package/src/runtime/trust-context-resolver.ts +8 -5
  385. package/src/schedule/schedule-store.ts +1 -1
  386. package/src/schedule/scheduler-types.ts +5 -1
  387. package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
  388. package/src/security/secret-patterns.ts +3 -0
  389. package/src/subagent/manager.ts +11 -4
  390. package/src/telemetry/trace-collection-policy.test.ts +28 -0
  391. package/src/telemetry/trace-collection-policy.ts +30 -0
  392. package/src/telemetry/types.ts +89 -0
  393. package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
  394. package/src/telemetry/usage-telemetry-reporter.ts +148 -41
  395. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
  396. package/src/tools/browser/browser-execution.ts +29 -18
  397. package/src/tools/document/document-tool.ts +2 -3
  398. package/src/tools/executor.ts +5 -3
  399. package/src/tools/host-terminal/host-shell.ts +5 -4
  400. package/src/tools/memory/register.ts +2 -2
  401. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
  402. package/src/tools/network/__tests__/web-search.test.ts +143 -0
  403. package/src/tools/network/web-fetch.ts +372 -1
  404. package/src/tools/network/web-search.ts +213 -10
  405. package/src/tools/permission-checker.ts +3 -2
  406. package/src/tools/registry.ts +20 -0
  407. package/src/tools/schedule/create.ts +4 -3
  408. package/src/tools/schedule/update.ts +2 -1
  409. package/src/tools/shared/filesystem/path-policy.ts +39 -13
  410. package/src/tools/skills/execute.ts +1 -2
  411. package/src/tools/subagent/spawn.ts +37 -13
  412. package/src/tools/terminal/shell.ts +10 -4
  413. package/src/tools/tool-approval-handler.ts +17 -10
  414. package/src/tools/types.ts +9 -0
  415. package/src/tools/ui-surface/definitions.ts +25 -2
  416. package/src/tools/verification-control-plane-policy.ts +3 -1
  417. package/src/tools/workflows/run-workflow.ts +1 -0
  418. package/src/util/disk-usage.ts +78 -23
  419. package/src/util/platform.ts +8 -1
  420. package/src/watcher/telemetry.ts +2 -2
  421. package/src/workflows/engine.test.ts +175 -1
  422. package/src/workflows/engine.ts +82 -0
  423. package/src/workflows/journal-store.test.ts +70 -0
  424. package/src/workflows/journal-store.ts +18 -3
  425. package/src/workflows/run-manager.test.ts +171 -3
  426. package/src/workflows/run-manager.ts +64 -0
  427. package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
  428. package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
  429. package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
  430. package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
  431. package/src/workspace/migrations/registry.ts +8 -0
  432. package/src/notifications/tool-approval-copy.ts +0 -142
  433. package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
@@ -18,8 +18,8 @@
18
18
 
19
19
  import type { Command } from "commander";
20
20
 
21
- import { cliIpcCall } from "../../ipc/cli-client.js";
22
- import type { ComparisonReport } from "../../memory/v2/harness/runner.js";
21
+ import { cliIpcCall } from "../../../ipc/cli-client.js";
22
+ import type { ComparisonReport } from "../../../memory/v2/harness/runner.js";
23
23
  import type {
24
24
  MemoryV2BackfillOp,
25
25
  MemoryV2BackfillResult,
@@ -27,9 +27,9 @@ import type {
27
27
  MemoryV2ReembedSkillsResult,
28
28
  MemoryV2SimulateRouterResult,
29
29
  MemoryV2ValidateResult,
30
- } from "../../runtime/routes/memory-v2-routes.js";
31
- import { registerCommand } from "../lib/register-command.js";
32
- import { log } from "../logger.js";
30
+ } from "../../../runtime/routes/memory-v2-routes.js";
31
+ import { registerCommand } from "../../lib/register-command.js";
32
+ import { log } from "../../logger.js";
33
33
  import {
34
34
  renderComparisonReport,
35
35
  renderTurnTrace,
@@ -63,16 +63,7 @@ async function runBackfillOp(op: MemoryV2BackfillOp): Promise<void> {
63
63
  // Registration
64
64
  // ---------------------------------------------------------------------------
65
65
 
66
- export function registerMemoryV2Command(program: Command): void {
67
- // Reuse an existing `memory` parent if some other registrar attached to it
68
- // first; otherwise create one. This keeps the registration order between
69
- // sibling memory registrars unconstrained.
70
- const memory =
71
- program.commands.find((c) => c.name() === "memory") ??
72
- program
73
- .command("memory")
74
- .description("Manage the v2 memory subsystem (concept-page model)");
75
-
66
+ export function registerMemoryV2Command(memory: Command): void {
76
67
  registerCommand(memory, {
77
68
  name: "v2",
78
69
  transport: "ipc",
@@ -17,13 +17,14 @@
17
17
 
18
18
  import type { Command } from "commander";
19
19
 
20
- import { cliIpcCall } from "../../ipc/cli-client.js";
20
+ import { cliIpcCall } from "../../../ipc/cli-client.js";
21
+ import type { MemoryEvalRunResult } from "../../../runtime/routes/memory-eval-routes.js";
21
22
  import type {
22
23
  MemoryV3BackfillSectionsResult,
23
24
  MemoryV3RebuildIndexResult,
24
- } from "../../runtime/routes/memory-v3-routes.js";
25
- import { registerCommand } from "../lib/register-command.js";
26
- import { log } from "../logger.js";
25
+ } from "../../../runtime/routes/memory-v3-routes.js";
26
+ import { registerCommand } from "../../lib/register-command.js";
27
+ import { log } from "../../logger.js";
27
28
 
28
29
  /**
29
30
  * IPC timeout for `backfill-sections`. The one-time full-corpus section embed
@@ -34,14 +35,14 @@ import { log } from "../logger.js";
34
35
  */
35
36
  const BACKFILL_IPC_TIMEOUT_MS = 30 * 60 * 1000;
36
37
 
37
- export function registerMemoryV3Command(program: Command): void {
38
- // Reuse an existing `memory` parent if some other registrar attached to it
39
- // first; otherwise create one. This keeps the registration order between
40
- // sibling memory registrars unconstrained.
41
- const memory =
42
- program.commands.find((c) => c.name() === "memory") ??
43
- program.command("memory").description("Manage the memory subsystem");
38
+ /**
39
+ * IPC timeout for `eval`. Embedding both corpora's sections runs through the
40
+ * embedder and easily outlasts the default 60s, so allow the same generous
41
+ * ceiling as the backfill.
42
+ */
43
+ const EVAL_IPC_TIMEOUT_MS = 30 * 60 * 1000;
44
44
 
45
+ export function registerMemoryV3Command(memory: Command): void {
45
46
  registerCommand(memory, {
46
47
  name: "v3",
47
48
  transport: "ipc",
@@ -134,6 +135,91 @@ Examples:
134
135
  : "."),
135
136
  );
136
137
  });
138
+
139
+ // ── eval ──────────────────────────────────────────────────────────────
140
+
141
+ v3.command("eval")
142
+ .description(
143
+ "Build blinded A/B retrieval-eval packets (snapshot corpus vs staged wiki)",
144
+ )
145
+ .requiredOption(
146
+ "--staging <dir>",
147
+ "Staged v3 wiki dir (relative to the workspace, or absolute)",
148
+ )
149
+ .requiredOption(
150
+ "--snapshot <dir>",
151
+ "Read-only v2 snapshot dir (relative to the workspace, or absolute)",
152
+ )
153
+ .requiredOption("--out <dir>", "Output dir for packets.json + key.json")
154
+ .option("--turns <n>", "Number of recent turns to mine", "30")
155
+ .option("--k <n>", "Pages per memory set", "8")
156
+ .option(
157
+ "--seed <n>",
158
+ "Blinding seed (reproducible A/B assignment)",
159
+ "1",
160
+ )
161
+ .option(
162
+ "--no-dense",
163
+ "Needle-only: skip section embedding (fast, cheaper, lower fidelity)",
164
+ )
165
+ .option("--json", "Emit raw JSON instead of a formatted summary")
166
+ .addHelpText(
167
+ "after",
168
+ `
169
+ Mines recent user turns, retrieves the top pages from each corpus per turn, and
170
+ writes blinded A/B packets (plus a separate unblinding key) for a blind-judge
171
+ workflow. Both corpora are read in memory — nothing in the live lanes or Qdrant
172
+ is touched. With the dense lane on (default) it embeds every section of both
173
+ corpora, which can take a while on a large corpus; use --no-dense for a fast
174
+ lexical-only pass.
175
+
176
+ Examples:
177
+ $ assistant memory v3 eval --snapshot .mv3/snapshot/concepts --staging .mv3/staging --out .mv3/eval
178
+ $ assistant memory v3 eval --snapshot .mv3/snapshot/concepts --staging .mv3/staging --out .mv3/eval --turns 50 --no-dense`,
179
+ )
180
+ .action(
181
+ async (opts: {
182
+ staging: string;
183
+ snapshot: string;
184
+ out: string;
185
+ turns: string;
186
+ k: string;
187
+ seed: string;
188
+ dense: boolean;
189
+ json?: boolean;
190
+ }) => {
191
+ const result = await cliIpcCall<MemoryEvalRunResult>(
192
+ "memory_eval_run",
193
+ {
194
+ body: {
195
+ stagingDir: opts.staging,
196
+ snapshotDir: opts.snapshot,
197
+ outDir: opts.out,
198
+ turns: Number(opts.turns),
199
+ k: Number(opts.k),
200
+ seed: Number(opts.seed),
201
+ dense: opts.dense,
202
+ },
203
+ },
204
+ { timeoutMs: EVAL_IPC_TIMEOUT_MS },
205
+ );
206
+ if (!result.ok) {
207
+ log.error(result.error ?? "Failed to build eval packets");
208
+ process.exitCode = 1;
209
+ return;
210
+ }
211
+ const payload = result.result!;
212
+ if (opts.json === true) {
213
+ log.info(JSON.stringify(payload, null, 2));
214
+ return;
215
+ }
216
+ log.info(
217
+ `Wrote ${payload.packetsWritten} packets from ${payload.turnsMined} turns ` +
218
+ `(snapshot ${payload.snapshotPages} pages vs staged ${payload.stagingPages} pages, ` +
219
+ `dense=${payload.dense}).\n packets: ${payload.packetsPath}\n key: ${payload.keyPath}`,
220
+ );
221
+ },
222
+ );
137
223
  },
138
224
  });
139
225
  }
@@ -0,0 +1,36 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+
3
+ import { noConnectionsMessage } from "./status.js";
4
+
5
+ const original = process.env.__RESOLVED_MODEL;
6
+
7
+ afterEach(() => {
8
+ if (original === undefined) delete process.env.__RESOLVED_MODEL;
9
+ else process.env.__RESOLVED_MODEL = original;
10
+ });
11
+
12
+ describe("noConnectionsMessage", () => {
13
+ test("capable models get the terse default", () => {
14
+ process.env.__RESOLVED_MODEL = "claude-opus-4-8";
15
+ const msg = noConnectionsMessage("google");
16
+ expect(msg).toBe(
17
+ "No active connections for google.\n" +
18
+ "Connect with `assistant oauth connect google`.\n",
19
+ );
20
+ });
21
+
22
+ test("no resolved model falls back to the terse default", () => {
23
+ delete process.env.__RESOLVED_MODEL;
24
+ expect(noConnectionsMessage("google")).toContain(
25
+ "Connect with `assistant oauth connect google`.",
26
+ );
27
+ });
28
+
29
+ test("weak open models are steered to the oauth_connect surface", () => {
30
+ process.env.__RESOLVED_MODEL = "accounts/fireworks/models/minimax-m3";
31
+ const msg = noConnectionsMessage("google");
32
+ expect(msg).toContain('surface_type "oauth_connect"');
33
+ expect(msg).toContain('data.providerKey "google"');
34
+ expect(msg).not.toContain("assistant oauth connect google");
35
+ });
36
+ });
@@ -1,8 +1,30 @@
1
1
  import type { Command } from "commander";
2
2
 
3
3
  import { cliIpcCall, exitFromIpcResult } from "../../../ipc/cli-client.js";
4
+ import { isWeakOpenModel } from "../../../providers/weak-open-model.js";
4
5
  import { shouldOutputJson, writeOutput } from "../../output.js";
5
6
 
7
+ /**
8
+ * Message shown when a provider has no active connections. Weak open models
9
+ * loop through redundant discovery commands (channel checks, `oauth providers
10
+ * get`, loading the OAuth setup skill) before rendering the connect button, so
11
+ * they get an explicit single next action: render the core `oauth_connect`
12
+ * surface directly. Capable models keep the terse default.
13
+ */
14
+ export function noConnectionsMessage(provider: string): string {
15
+ const base = `No active connections for ${provider}.`;
16
+ if (isWeakOpenModel(process.env.__RESOLVED_MODEL)) {
17
+ return (
18
+ `${base}\nTo let the user connect, render the connect button: call ` +
19
+ `\`ui_show\` with surface_type "oauth_connect" and ` +
20
+ `data.providerKey "${provider}". That surface is always available — do ` +
21
+ `not run further \`oauth\`/\`channels\` commands or load a setup skill ` +
22
+ `just to display it.\n`
23
+ );
24
+ }
25
+ return `${base}\nConnect with \`assistant oauth connect ${provider}\`.\n`;
26
+ }
27
+
6
28
  // ---------------------------------------------------------------------------
7
29
  // Types
8
30
  // ---------------------------------------------------------------------------
@@ -91,9 +113,7 @@ Examples:
91
113
 
92
114
  // Text output
93
115
  if (connections.length === 0) {
94
- process.stdout.write(
95
- `No active connections for ${provider}.\nConnect with \`assistant oauth connect ${provider}\`.\n`,
96
- );
116
+ process.stdout.write(noConnectionsMessage(provider));
97
117
  return;
98
118
  }
99
119
 
@@ -46,7 +46,6 @@ import {
46
46
  PluginNotUpgradableError,
47
47
  type PluginUpgradeResult,
48
48
  type PluginUpgradeStrategy,
49
- PluginUpgradeStrategyUnsupportedError,
50
49
  upgradePlugin,
51
50
  } from "../lib/upgrade-plugin.js";
52
51
  import { getCliLogger } from "../logger.js";
@@ -76,6 +75,7 @@ Examples:
76
75
  $ assistant plugins upgrade example --dry-run
77
76
  $ assistant plugins upgrade example --strategy ours
78
77
  $ assistant plugins upgrade example --strategy theirs
78
+ $ assistant plugins upgrade example --strategy assistant
79
79
  $ assistant plugins search example
80
80
  $ assistant plugins search "^example"
81
81
  $ assistant plugins search example --json
@@ -180,7 +180,7 @@ Examples:
180
180
  plugins
181
181
  .command("inspect <name>")
182
182
  .description(
183
- "Show a plugin's local install metadata and the marketplace pin, and whether an update is available",
183
+ "Show a plugin's local install metadata, the marketplace pin, whether an update is available, and the surfaces (skills, hooks, tools) it contributes",
184
184
  )
185
185
  .option("--json", "Emit machine-readable JSON instead of a summary")
186
186
  .action(async (name: string, opts: { json?: boolean }) => {
@@ -465,7 +465,6 @@ Examples:
465
465
  if (
466
466
  err instanceof PluginNotInstalledError ||
467
467
  err instanceof PluginNotUpgradableError ||
468
- err instanceof PluginUpgradeStrategyUnsupportedError ||
469
468
  err instanceof PluginMergeBaselineError ||
470
469
  err instanceof InvalidPluginNameError
471
470
  ) {
@@ -550,12 +549,20 @@ function remoteLocation(remote: PluginRemoteInfo): string {
550
549
  * `location`, with a `drift` line under the installed copy.
551
550
  */
552
551
  function formatInspection(inspection: PluginInspection): string[] {
553
- const { name, status, local, remote, remoteError } = inspection;
552
+ const { name, status, local, remote, remoteError, surfaces } = inspection;
554
553
  const lines: string[] = [name, "─".repeat(44)];
555
554
  const topRow = (label: string, value: string) =>
556
555
  lines.push(`${label.padEnd(11)} ${value}`);
557
556
  const blockRow = (label: string, value: string) =>
558
557
  lines.push(` ${label.padEnd(9)} ${value}`);
558
+ // A surface block: the surface type as a heading, then its items indented
559
+ // under it. Omitted entirely when the plugin contributes none of that type,
560
+ // so the listing only ever shows what the plugin actually contributes.
561
+ const surfaceBlock = (label: string, items: readonly string[]) => {
562
+ if (items.length === 0) return;
563
+ lines.push(label);
564
+ for (const item of items) lines.push(` ${item}`);
565
+ };
559
566
 
560
567
  topRow("status", statusLine(status));
561
568
 
@@ -590,6 +597,12 @@ function formatInspection(inspection: PluginInspection): string[] {
590
597
  const description = remote?.description ?? local?.description ?? null;
591
598
  if (description) topRow("description", description);
592
599
 
600
+ if (surfaces) {
601
+ surfaceBlock("skills", surfaces.skills);
602
+ surfaceBlock("hooks", surfaces.hooks);
603
+ surfaceBlock("tools", surfaces.tools);
604
+ }
605
+
593
606
  for (const issue of local?.issues ?? []) topRow("issue", issue);
594
607
 
595
608
  return lines;
@@ -628,10 +641,49 @@ function formatUpgrade(result: PluginUpgradeResult): string[] {
628
641
  result.fileCount === null
629
642
  ? ""
630
643
  : `(${result.fileCount} file${result.fileCount === 1 ? "" : "s"}) `;
644
+ const hasConflicts =
645
+ result.conflicts.length > 0 || result.binaryConflicts.length > 0;
646
+
647
+ // Under `assistant`, an unresolved merge leaves conflicts in the tree —
648
+ // the plugin would fail to load while any remain, so the usual "restart
649
+ // now" guidance is replaced with resolution instructions. Most conflicts
650
+ // carry git markers, but a modify/delete divergence (a file edited on one
651
+ // side and deleted on the other) keeps the surviving content with no
652
+ // markers, so the guidance covers both rather than assuming markers.
653
+ if (result.strategy === "assistant" && hasConflicts) {
654
+ const lines = [
655
+ `Merged "${name}" ${move} with conflicts`,
656
+ "",
657
+ `${count}→ ${result.target}`,
658
+ ];
659
+ if (result.conflicts.length > 0) {
660
+ lines.push(
661
+ "",
662
+ `Resolve ${result.conflicts.length} conflicted file${result.conflicts.length === 1 ? "" : "s"} (open each: resolve its git conflict markers, or — if a modify/delete divergence kept the file with none — decide whether to keep or remove it):`,
663
+ ...result.conflicts.map((p) => ` ${p}`),
664
+ );
665
+ }
666
+ if (result.binaryConflicts.length > 0) {
667
+ lines.push(
668
+ "",
669
+ `Binary conflict${result.binaryConflicts.length === 1 ? "" : "s"} (kept the local copy — choose a version manually):`,
670
+ ...result.binaryConflicts.map((p) => ` ${p}`),
671
+ );
672
+ }
673
+ lines.push(
674
+ "",
675
+ "Resolve the conflicts, then restart the assistant to pick up the upgrade.",
676
+ );
677
+ if (provenanceNote) lines.push(provenanceNote);
678
+ return lines;
679
+ }
680
+
631
681
  const mergeNote =
632
682
  result.strategy === "ours" || result.strategy === "theirs"
633
683
  ? `Local edits were merged (--strategy ${result.strategy}).`
634
- : null;
684
+ : result.strategy === "assistant"
685
+ ? "Local edits were merged with no conflicts (--strategy assistant)."
686
+ : null;
635
687
  const lines = [
636
688
  `Upgraded "${name}" ${move}`,
637
689
  "",
@@ -90,6 +90,8 @@ function installPlugin(
90
90
  } | null;
91
91
  /** Embed a content fingerprint of the materialized tree in the sidecar. */
92
92
  fingerprint?: boolean;
93
+ /** Materialize surface directories (`hooks/<name>.ts`, `skills/<id>/SKILL.md`). */
94
+ surfaces?: { hooks?: string[]; tools?: string[]; skills?: string[] };
93
95
  } = {},
94
96
  ): void {
95
97
  const dir = join(workspace, name);
@@ -102,6 +104,18 @@ function installPlugin(
102
104
  description: "Installed copy.",
103
105
  }),
104
106
  );
107
+ for (const hook of opts.surfaces?.hooks ?? []) {
108
+ mkdirSync(join(dir, "hooks"), { recursive: true });
109
+ writeFileSync(join(dir, "hooks", `${hook}.ts`), "export default () => {};");
110
+ }
111
+ for (const tool of opts.surfaces?.tools ?? []) {
112
+ mkdirSync(join(dir, "tools"), { recursive: true });
113
+ writeFileSync(join(dir, "tools", `${tool}.ts`), "export default {};");
114
+ }
115
+ for (const skill of opts.surfaces?.skills ?? []) {
116
+ mkdirSync(join(dir, "skills", skill), { recursive: true });
117
+ writeFileSync(join(dir, "skills", skill, "SKILL.md"), `# ${skill}`);
118
+ }
105
119
  if (opts.sidecar !== null) {
106
120
  const sidecar = opts.sidecar ?? { commit: SHA_A };
107
121
  // Mirror install: fingerprint the tree before the sidecar is written so it
@@ -367,6 +381,46 @@ describe("inspectPlugin", () => {
367
381
  expect(result.local?.localChanges).toBeNull();
368
382
  });
369
383
 
384
+ test("reports the surfaces an installed copy contributes", async () => {
385
+ // GIVEN an installed plugin shipping hooks and skills but no tools
386
+ installPlugin(workspace, "level-up", {
387
+ sidecar: { commit: SHA_A },
388
+ surfaces: {
389
+ hooks: ["post-model-call", "init"],
390
+ skills: ["second-skill", "first-skill"],
391
+ },
392
+ });
393
+ const fetch = makeFetch({ marketplace: manifestWith("level-up", SHA_A) });
394
+
395
+ // WHEN it is inspected
396
+ const result = await inspectPlugin(
397
+ { name: "level-up" },
398
+ { fetch, workspacePluginsDir: workspace },
399
+ );
400
+
401
+ // THEN the contributed surfaces are listed (sorted) and tools is empty
402
+ expect(result.surfaces).toEqual({
403
+ skills: ["first-skill", "second-skill"],
404
+ hooks: ["init", "post-model-call"],
405
+ tools: [],
406
+ });
407
+ });
408
+
409
+ test("leaves surfaces null when the plugin is not installed", async () => {
410
+ // GIVEN a marketplace entry with no local copy to walk
411
+ const fetch = makeFetch({ marketplace: manifestWith("level-up", SHA_B) });
412
+
413
+ // WHEN it is inspected
414
+ const result = await inspectPlugin(
415
+ { name: "level-up" },
416
+ { fetch, workspacePluginsDir: workspace },
417
+ );
418
+
419
+ // THEN there is no tree to read surfaces from
420
+ expect(result.installed).toBe(false);
421
+ expect(result.surfaces).toBeNull();
422
+ });
423
+
370
424
  test("throws when the plugin is neither installed nor in the marketplace", async () => {
371
425
  // GIVEN no local copy and an empty catalog
372
426
  const fetch = makeFetch({ marketplace: undefined });
@@ -4,8 +4,9 @@
4
4
  * The merge is exercised against three real temp directories (base/ours/theirs)
5
5
  * and the merged tree is read back from a fourth (dest). `git merge-file` runs
6
6
  * for real — these tests assert the line-level merge behavior (non-conflicting
7
- * hunks from both sides survive; conflicts resolve toward the strategy), the
8
- * file-level add/delete resolution, and binary whole-file handling.
7
+ * hunks from both sides survive; conflicts resolve toward `ours`/`theirs` or
8
+ * get conflict markers under `assistant`), the file-level add/delete
9
+ * resolution, and binary whole-file handling.
9
10
  */
10
11
 
11
12
  import {
@@ -97,7 +98,9 @@ describe("mergePluginTree", () => {
97
98
  expect(read("local-only.txt")).toBe("local\n");
98
99
  expect(read("remote-only.txt")).toBe("remote\n");
99
100
  expect(read("conflict.txt")).toBe("ours\n");
100
- expect(count).toBe(4);
101
+ expect(count.fileCount).toBe(4);
102
+ expect(count.conflicts).toEqual([]);
103
+ expect(count.binaryConflicts).toEqual([]);
101
104
  });
102
105
 
103
106
  test("resolves conflicts toward the pin under the theirs strategy", async () => {
@@ -289,7 +292,7 @@ describe("mergePluginTree", () => {
289
292
 
290
293
  // THEN the sidecar is never carried through (the caller rewrites it)
291
294
  expect(read("install-meta.json")).toBeNull();
292
- expect(count).toBe(1);
295
+ expect(count.fileCount).toBe(1);
293
296
  });
294
297
 
295
298
  test("merges files in nested directories", async () => {
@@ -310,4 +313,131 @@ describe("mergePluginTree", () => {
310
313
  // THEN the nested path is preserved and resolved
311
314
  expect(read("src/a/b.txt")).toBe("ours\n");
312
315
  });
316
+
317
+ test("writes git conflict markers and reports the path under the assistant strategy", async () => {
318
+ // GIVEN a file edited on disjoint lines by each side plus one true conflict
319
+ writeTree(baseDir, { "common.txt": "a\nb\nc\n", "conflict.txt": "base\n" });
320
+ writeTree(oursDir, { "common.txt": "A\nb\nc\n", "conflict.txt": "ours\n" });
321
+ writeTree(theirsDir, {
322
+ "common.txt": "a\nb\nC\n",
323
+ "conflict.txt": "theirs\n",
324
+ });
325
+
326
+ // WHEN merged with the `assistant` strategy
327
+ const result = await mergePluginTree({
328
+ baseDir,
329
+ oursDir,
330
+ theirsDir,
331
+ destDir,
332
+ strategy: "assistant",
333
+ });
334
+
335
+ // THEN the non-conflicting edits still auto-merge and only the true
336
+ // conflict carries markers naming both sides, and its path is reported
337
+ expect(read("common.txt")).toBe("A\nb\nC\n");
338
+ const conflict = read("conflict.txt") ?? "";
339
+ expect(conflict).toContain("<<<<<<<");
340
+ expect(conflict).toContain("=======");
341
+ expect(conflict).toContain(">>>>>>>");
342
+ expect(conflict).toContain("ours\n");
343
+ expect(conflict).toContain("theirs\n");
344
+ expect(result.conflicts).toEqual(["conflict.txt"]);
345
+ expect(result.binaryConflicts).toEqual([]);
346
+ });
347
+
348
+ test("uses the supplied conflict labels in the markers", async () => {
349
+ // GIVEN a true conflict and custom marker labels
350
+ writeTree(baseDir, { "f.txt": "base\n" });
351
+ writeTree(oursDir, { "f.txt": "ours\n" });
352
+ writeTree(theirsDir, { "f.txt": "theirs\n" });
353
+
354
+ // WHEN merged with the `assistant` strategy and explicit labels
355
+ await mergePluginTree({
356
+ baseDir,
357
+ oursDir,
358
+ theirsDir,
359
+ destDir,
360
+ strategy: "assistant",
361
+ conflictLabels: {
362
+ ours: "local edits (was abc1234)",
363
+ base: "install baseline abc1234",
364
+ theirs: "upgrade pin def5678",
365
+ },
366
+ });
367
+
368
+ // THEN the marker lines carry those labels
369
+ const merged = read("f.txt") ?? "";
370
+ expect(merged).toContain("<<<<<<< local edits (was abc1234)");
371
+ expect(merged).toContain(">>>>>>> upgrade pin def5678");
372
+ });
373
+
374
+ test("auto-merges with no markers when the assistant strategy hits no true conflict", async () => {
375
+ // GIVEN edits on disjoint lines of the same file (no overlapping hunk)
376
+ writeTree(baseDir, { "f.txt": "a\nb\nc\n" });
377
+ writeTree(oursDir, { "f.txt": "A\nb\nc\n" });
378
+ writeTree(theirsDir, { "f.txt": "a\nb\nC\n" });
379
+
380
+ // WHEN merged with the `assistant` strategy
381
+ const result = await mergePluginTree({
382
+ baseDir,
383
+ oursDir,
384
+ theirsDir,
385
+ destDir,
386
+ strategy: "assistant",
387
+ });
388
+
389
+ // THEN both edits merge cleanly with no markers and nothing is reported
390
+ const merged = read("f.txt") ?? "";
391
+ expect(merged).toBe("A\nb\nC\n");
392
+ expect(merged).not.toContain("<<<<<<<");
393
+ expect(result.conflicts).toEqual([]);
394
+ expect(result.binaryConflicts).toEqual([]);
395
+ });
396
+
397
+ test("keeps the local copy and reports a binary conflict under the assistant strategy", async () => {
398
+ // GIVEN a binary file (NUL bytes) diverged on both sides
399
+ const base = Buffer.from([0x00, 0x01, 0x02]);
400
+ const ours = Buffer.from([0x00, 0xaa, 0xbb]);
401
+ const theirs = Buffer.from([0x00, 0xcc, 0xdd]);
402
+ writeTree(baseDir, { "img.bin": base });
403
+ writeTree(oursDir, { "img.bin": ours });
404
+ writeTree(theirsDir, { "img.bin": theirs });
405
+
406
+ // WHEN merged with the `assistant` strategy
407
+ const result = await mergePluginTree({
408
+ baseDir,
409
+ oursDir,
410
+ theirsDir,
411
+ destDir,
412
+ strategy: "assistant",
413
+ });
414
+
415
+ // THEN the local copy is kept byte-for-byte (markers are impossible) and
416
+ // the path is surfaced as a binary conflict, not a marker conflict
417
+ expect(readFileSync(join(destDir, "img.bin")).equals(ours)).toBe(true);
418
+ expect(result.binaryConflicts).toEqual(["img.bin"]);
419
+ expect(result.conflicts).toEqual([]);
420
+ });
421
+
422
+ test("keeps the surviving content and reports a modify/delete conflict under the assistant strategy", async () => {
423
+ // GIVEN a file edited locally but deleted at the pin
424
+ writeTree(baseDir, { "f.txt": "base\n" });
425
+ writeTree(oursDir, { "f.txt": "local edit\n" });
426
+ writeTree(theirsDir, {});
427
+
428
+ // WHEN merged with the `assistant` strategy
429
+ const result = await mergePluginTree({
430
+ baseDir,
431
+ oursDir,
432
+ theirsDir,
433
+ destDir,
434
+ strategy: "assistant",
435
+ });
436
+
437
+ // THEN the local edit is preserved (never silently dropped) and the
438
+ // modify/delete divergence is reported for resolution
439
+ expect(read("f.txt")).toBe("local edit\n");
440
+ expect(result.conflicts).toEqual(["f.txt"]);
441
+ expect(result.binaryConflicts).toEqual([]);
442
+ });
313
443
  });