@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
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Tests for {@link detectPluginSurfaces}.
3
+ *
4
+ * Surface detection is a pure walk of an installed plugin's on-disk tree, so
5
+ * the fixtures materialize the `hooks/`, `tools/`, and `skills/` directory
6
+ * conventions in a real temp dir and assert the derived listing matches what
7
+ * the runtime loader / skills catalog would discover.
8
+ */
9
+
10
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
14
+
15
+ import { detectPluginSurfaces } from "../plugin-surfaces.js";
16
+
17
+ let pluginDir: string;
18
+
19
+ beforeEach(() => {
20
+ pluginDir = mkdtempSync(join(tmpdir(), "plugin-surfaces-"));
21
+ });
22
+
23
+ afterEach(() => {
24
+ rmSync(pluginDir, { recursive: true, force: true });
25
+ });
26
+
27
+ /** Create `<pluginDir>/<rel>` with empty contents, making parents as needed. */
28
+ function touch(rel: string): void {
29
+ const path = join(pluginDir, rel);
30
+ mkdirSync(join(path, ".."), { recursive: true });
31
+ writeFileSync(path, "");
32
+ }
33
+
34
+ describe("detectPluginSurfaces", () => {
35
+ test("lists hooks, tools, and skills from the directory conventions", () => {
36
+ // GIVEN a plugin shipping two hooks, one tool, and two skills
37
+ touch("hooks/post-model-call.ts");
38
+ touch("hooks/init.ts");
39
+ touch("tools/summarize.ts");
40
+ touch("skills/first-skill/SKILL.md");
41
+ touch("skills/second-skill/SKILL.md");
42
+
43
+ // WHEN its surfaces are detected
44
+ const surfaces = detectPluginSurfaces(pluginDir);
45
+
46
+ // THEN each surface type lists its items, sorted
47
+ expect(surfaces.hooks).toEqual(["init", "post-model-call"]);
48
+ expect(surfaces.tools).toEqual(["summarize"]);
49
+ expect(surfaces.skills).toEqual(["first-skill", "second-skill"]);
50
+ });
51
+
52
+ test("reports tools under their registered name, not the raw filename", () => {
53
+ // GIVEN a tool whose filename is not already a valid tool-name segment
54
+ touch("tools/create-issue.ts");
55
+
56
+ // WHEN its surfaces are detected
57
+ const surfaces = detectPluginSurfaces(pluginDir);
58
+
59
+ // THEN the derived (registered) name is reported, matching the loader
60
+ expect(surfaces.tools).toEqual(["create_issue"]);
61
+ });
62
+
63
+ test("omits surface types the plugin does not contribute", () => {
64
+ // GIVEN a plugin that ships only a hook
65
+ touch("hooks/post-model-call.ts");
66
+
67
+ // WHEN its surfaces are detected
68
+ const surfaces = detectPluginSurfaces(pluginDir);
69
+
70
+ // THEN the contributed type is listed and the others are empty
71
+ expect(surfaces.hooks).toEqual(["post-model-call"]);
72
+ expect(surfaces.tools).toEqual([]);
73
+ expect(surfaces.skills).toEqual([]);
74
+ });
75
+
76
+ test("prefers .js over .ts for the same basename and skips .d.ts declarations", () => {
77
+ // GIVEN a compiled hook shipping both .ts and .js plus a .d.ts declaration
78
+ touch("hooks/post-model-call.ts");
79
+ touch("hooks/post-model-call.js");
80
+ touch("hooks/post-model-call.d.ts");
81
+
82
+ // WHEN its surfaces are detected
83
+ const surfaces = detectPluginSurfaces(pluginDir);
84
+
85
+ // THEN the basename appears once and the declaration file is ignored
86
+ expect(surfaces.hooks).toEqual(["post-model-call"]);
87
+ });
88
+
89
+ test("ignores skill directories without a SKILL.md", () => {
90
+ // GIVEN a skills dir with one real skill and one stray subdirectory
91
+ touch("skills/real-skill/SKILL.md");
92
+ touch("skills/not-a-skill/README.md");
93
+
94
+ // WHEN its surfaces are detected
95
+ const surfaces = detectPluginSurfaces(pluginDir);
96
+
97
+ // THEN only the directory carrying a SKILL.md is reported
98
+ expect(surfaces.skills).toEqual(["real-skill"]);
99
+ });
100
+
101
+ test("returns empty surfaces for a plugin with no surface directories", () => {
102
+ // GIVEN a plugin tree with only a package.json
103
+ touch("package.json");
104
+
105
+ // WHEN its surfaces are detected
106
+ const surfaces = detectPluginSurfaces(pluginDir);
107
+
108
+ // THEN every surface type is empty
109
+ expect(surfaces).toEqual({ skills: [], hooks: [], tools: [] });
110
+ });
111
+ });
@@ -31,7 +31,6 @@ import { PluginNotInstalledError } from "../uninstall-plugin.js";
31
31
  import {
32
32
  PluginMergeBaselineError,
33
33
  PluginNotUpgradableError,
34
- PluginUpgradeStrategyUnsupportedError,
35
34
  upgradePlugin,
36
35
  } from "../upgrade-plugin.js";
37
36
 
@@ -582,19 +581,62 @@ describe("upgradePlugin --strategy", () => {
582
581
  expect(installedFile("level-up", "local-only.txt")).toBeNull();
583
582
  });
584
583
 
585
- test("throws PluginUpgradeStrategyUnsupportedError for the assistant strategy", async () => {
586
- // GIVEN an installed copy and an advanced pin
584
+ test("--strategy assistant writes conflict markers and reports the conflicted path", async () => {
585
+ // GIVEN an install at SHA_A with local edits, and a pin at SHA_B
587
586
  installMergeCopy("level-up", OURS, SHA_A, BASE);
588
587
  const fetch = makeFetch({ manifest: manifestWith("level-up", SHA_B) });
588
+ const runGit = treeGitRunner({ [SHA_A]: BASE, [SHA_B]: THEIRS });
589
589
 
590
- // WHEN an upgrade requests the not-yet-implemented `assistant` strategy
591
- // THEN it is rejected before any clone runs
592
- await expect(
593
- upgradePlugin(
594
- { name: "level-up", strategy: "assistant" },
595
- { fetch, runGit: unusedGitRunner, workspacePluginsDir: pluginsDir },
596
- ),
597
- ).rejects.toBeInstanceOf(PluginUpgradeStrategyUnsupportedError);
590
+ // WHEN the plugin is upgraded with the `assistant` strategy
591
+ const result = await upgradePlugin(
592
+ { name: "level-up", strategy: "assistant" },
593
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
594
+ );
595
+
596
+ // THEN it moves to the pin and records the strategy
597
+ expect(result.outcome).toBe("upgraded");
598
+ expect(result.toCommit).toBe(SHA_B);
599
+ expect(result.strategy).toBe("assistant");
600
+ // AND non-conflicting edits from both sides still auto-merge
601
+ expect(installedFile("level-up", "common.txt")).toBe("A\nb\nC\n");
602
+ expect(installedFile("level-up", "local-only.txt")).toBe("added locally\n");
603
+ expect(installedFile("level-up", "remote-only.txt")).toBe(
604
+ "added upstream\n",
605
+ );
606
+ // AND the true conflict carries git markers naming both commits
607
+ const conflict = installedFile("level-up", "conflict.txt") ?? "";
608
+ expect(conflict).toContain("<<<<<<<");
609
+ expect(conflict).toContain(SHA_A.slice(0, 7));
610
+ expect(conflict).toContain(SHA_B.slice(0, 7));
611
+ expect(conflict).toContain("ours\n");
612
+ expect(conflict).toContain("theirs\n");
613
+ // AND the conflicted path is surfaced for the assistant to resolve
614
+ expect(result.conflicts).toEqual(["conflict.txt"]);
615
+ expect(result.binaryConflicts).toEqual([]);
616
+ });
617
+
618
+ test("--strategy assistant reports no conflicts on a clean three-way merge", async () => {
619
+ // GIVEN an install whose only divergence from the pin auto-merges cleanly
620
+ const cleanOurs: Tree = { "common.txt": "A\nb\nc\n" };
621
+ const cleanBase: Tree = { "common.txt": "a\nb\nc\n" };
622
+ const cleanTheirs: Tree = { "common.txt": "a\nb\nC\n" };
623
+ installMergeCopy("level-up", cleanOurs, SHA_A, cleanBase);
624
+ const fetch = makeFetch({ manifest: manifestWith("level-up", SHA_B) });
625
+ const runGit = treeGitRunner({ [SHA_A]: cleanBase, [SHA_B]: cleanTheirs });
626
+
627
+ // WHEN the plugin is upgraded with the `assistant` strategy
628
+ const result = await upgradePlugin(
629
+ { name: "level-up", strategy: "assistant" },
630
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
631
+ );
632
+
633
+ // THEN both edits merge with no markers and nothing needs resolution
634
+ expect(result.strategy).toBe("assistant");
635
+ expect(result.conflicts).toEqual([]);
636
+ expect(result.binaryConflicts).toEqual([]);
637
+ const merged = installedFile("level-up", "common.txt") ?? "";
638
+ expect(merged).toBe("A\nb\nC\n");
639
+ expect(merged).not.toContain("<<<<<<<");
598
640
  });
599
641
 
600
642
  test("throws PluginMergeBaselineError when no fingerprint was recorded", async () => {
@@ -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(
@@ -11,18 +11,25 @@
11
11
  * Per file the merge is delegated to `git merge-file`, the same line-level
12
12
  * three-way algorithm git itself uses: a hunk edited on only one side is taken
13
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 by the
15
- * caller's strategy — `--ours` keeps the local hunk, `--theirs` the pinned one.
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
+ *
16
22
  * File-level add/delete divergence (a file added, removed, or modified on only
17
23
  * one side) is resolved here before any line merge, since `git merge-file`
18
24
  * operates on three existing blobs.
19
25
  *
20
26
  * Binary files cannot be line-merged, so a binary file that diverged on both
21
- * sides is resolved whole-file by the strategy rather than corrupted with
22
- * conflict markers.
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.
23
30
  *
24
31
  * The `overwrite` strategy never reaches here — it discards local edits and is
25
- * a plain re-install at the pin. The `assistant` strategy is not yet supported.
32
+ * a plain re-install at the pin.
26
33
  */
27
34
 
28
35
  import { execFile } from "node:child_process";
@@ -46,11 +53,25 @@ const execFileAsync = promisify(execFile);
46
53
  const MERGE_TIMEOUT_MS = 30_000;
47
54
 
48
55
  /**
49
- * Conflict-resolution strategy for the hunks `git merge-file` cannot
50
- * auto-resolve. `overwrite` and `assistant` are not merge strategies and are
51
- * handled by the caller before a merge is attempted.
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.
52
60
  */
53
- export type MergeConflictStrategy = "ours" | "theirs";
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
+ };
54
75
 
55
76
  /** Inputs for a three-way plugin-tree merge. */
56
77
  export interface MergePluginTreeOptions {
@@ -63,7 +84,36 @@ export interface MergePluginTreeOptions {
63
84
  /** Empty directory the merged tree is written into. */
64
85
  readonly destDir: string;
65
86
  /** How to resolve hunks edited differently on both sides. */
66
- readonly strategy: MergeConflictStrategy;
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;
67
117
  }
68
118
 
69
119
  /** A NUL byte in the leading bytes is the heuristic git uses to flag a blob as binary. */
@@ -83,23 +133,36 @@ function writeInto(destDir: string, rel: string, content: Buffer): void {
83
133
  }
84
134
 
85
135
  /**
86
- * Line-merge three blobs with `git merge-file`, resolving conflicting hunks
87
- * toward `strategy`. Non-conflicting hunks from both sides are always kept.
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
+ *
88
141
  * Binary input cannot be line-merged: a side that matches the base did not
89
- * change, so the other side's edit is taken; only a blob changed on *both*
90
- * sides is a true conflict resolved whole-file by `strategy`, avoiding a
91
- * marker-corrupted blob.
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).
92
145
  */
93
146
  async function threeWayMergeFile(
94
147
  ours: Buffer,
95
148
  base: Buffer,
96
149
  theirs: Buffer,
97
- strategy: MergeConflictStrategy,
98
- ): Promise<Buffer> {
150
+ strategy: PluginMergeStrategy,
151
+ labels: ConflictLabels,
152
+ ): Promise<MergedFile> {
99
153
  if (isBinary(ours) || isBinary(base) || isBinary(theirs)) {
100
- if (ours.equals(base)) return theirs;
101
- if (theirs.equals(base)) return ours;
102
- return strategy === "ours" ? ours : 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
+ };
103
166
  }
104
167
 
105
168
  const scratch = mkdtempSync(join(tmpdir(), "plugin-merge-file-"));
@@ -112,18 +175,16 @@ async function threeWayMergeFile(
112
175
  writeFileSync(theirsPath, theirs);
113
176
 
114
177
  // `-p` prints the merged result to stdout instead of editing `ours` in
115
- // place; `--ours`/`--theirs` auto-resolve conflicting hunks toward that
116
- // side (so no markers are ever written). `git merge-file` exits non-zero
117
- // when conflicts remained impossible with a resolving flag, but stdout
118
- // still carries the merged bytes, so capture it from the error too.
119
- const args = [
120
- "merge-file",
121
- "-p",
122
- `--${strategy}`,
123
- oursPath,
124
- basePath,
125
- theirsPath,
126
- ];
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);
127
188
  try {
128
189
  const { stdout } = await execFileAsync("git", args, {
129
190
  cwd: scratch,
@@ -131,10 +192,31 @@ async function threeWayMergeFile(
131
192
  timeout: MERGE_TIMEOUT_MS,
132
193
  maxBuffer: 64 * 1024 * 1024,
133
194
  });
134
- return stdout;
195
+ return { content: stdout, conflicted: false, binaryConflicted: false };
135
196
  } catch (err) {
136
- const stdout = (err as { stdout?: Buffer }).stdout;
137
- if (Buffer.isBuffer(stdout)) return stdout;
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
+ }
138
220
  throw err;
139
221
  }
140
222
  } finally {
@@ -143,12 +225,14 @@ async function threeWayMergeFile(
143
225
  }
144
226
 
145
227
  /**
146
- * Three-way merge `oursDir`/`theirsDir` against `baseDir` into `destDir`,
147
- * resolving conflicts toward `strategy`. The provenance sidecar is excluded on
148
- * every side — it is rewritten by the caller after the swap and must not be
149
- * carried through the merge.
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.
150
232
  *
151
- * Returns the number of files written into `destDir`.
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.
152
236
  */
153
237
  export async function mergePluginTree({
154
238
  baseDir,
@@ -156,7 +240,9 @@ export async function mergePluginTree({
156
240
  theirsDir,
157
241
  destDir,
158
242
  strategy,
159
- }: MergePluginTreeOptions): Promise<number> {
243
+ conflictLabels,
244
+ }: MergePluginTreeOptions): Promise<PluginMergeResult> {
245
+ const labels = conflictLabels ?? DEFAULT_CONFLICT_LABELS;
160
246
  const exclude = [INSTALL_META_FILENAME];
161
247
  const base = computeFingerprint(baseDir, exclude).files;
162
248
  const ours = computeFingerprint(oursDir, exclude).files;
@@ -173,6 +259,8 @@ export async function mergePluginTree({
173
259
  const readTheirs = (rel: string) => readFileSync(join(theirsDir, rel));
174
260
 
175
261
  let fileCount = 0;
262
+ const conflicts: string[] = [];
263
+ const binaryConflicts: string[] = [];
176
264
  const keep = (rel: string, content: Buffer): void => {
177
265
  writeInto(destDir, rel, content);
178
266
  fileCount++;
@@ -194,18 +282,26 @@ export async function mergePluginTree({
194
282
  b !== undefined ? readBase(rel) : Buffer.alloc(0),
195
283
  readTheirs(rel),
196
284
  strategy,
285
+ labels,
197
286
  );
198
- keep(rel, merged);
287
+ keep(rel, merged.content);
288
+ if (merged.conflicted) conflicts.push(rel);
289
+ if (merged.binaryConflicted) binaryConflicts.push(rel);
199
290
  }
200
291
  continue;
201
292
  }
202
293
 
203
294
  if (o !== undefined) {
204
295
  // Present only locally. A local-only addition (no base) always survives.
205
- // A file deleted upstream is a delete: drop it when unchanged locally,
206
- // and on a modify/delete conflict let the strategy decide.
207
- if (b === undefined || (b !== o && strategy === "ours")) {
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")) {
208
301
  keep(rel, readOurs(rel));
302
+ if (b !== undefined && b !== o && strategy === "assistant") {
303
+ conflicts.push(rel);
304
+ }
209
305
  }
210
306
  continue;
211
307
  }
@@ -213,10 +309,14 @@ export async function mergePluginTree({
213
309
  if (t !== undefined) {
214
310
  // Present only at the pin. A remote-only addition (no base) always lands.
215
311
  // A file deleted locally is a delete: keep upstream's removal when the
216
- // pin left it unchanged, and on a delete/modify conflict let the strategy
217
- // decide.
218
- if (b === undefined || (b !== t && strategy === "theirs")) {
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")) {
219
316
  keep(rel, readTheirs(rel));
317
+ if (b !== undefined && b !== t && strategy === "assistant") {
318
+ conflicts.push(rel);
319
+ }
220
320
  }
221
321
  continue;
222
322
  }
@@ -224,5 +324,5 @@ export async function mergePluginTree({
224
324
  // Present only in the base: removed on both sides, so it stays removed.
225
325
  }
226
326
 
227
- return fileCount;
327
+ return { fileCount, conflicts, binaryConflicts };
228
328
  }
@@ -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
+ }