@vellumai/assistant 0.5.1 → 0.5.3

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 (405) hide show
  1. package/ARCHITECTURE.md +163 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/docs/skills.md +100 -0
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-loop.test.ts +111 -0
  7. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  8. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  9. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  10. package/src/__tests__/app-executors.test.ts +1 -291
  11. package/src/__tests__/app-git-history.test.ts +4 -4
  12. package/src/__tests__/app-routes-csp.test.ts +1 -0
  13. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  14. package/src/__tests__/attachments-store.test.ts +169 -21
  15. package/src/__tests__/attachments.test.ts +115 -1
  16. package/src/__tests__/btw-routes.test.ts +1 -0
  17. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  18. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  19. package/src/__tests__/checker.test.ts +54 -0
  20. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  21. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  22. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  23. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  24. package/src/__tests__/config-schema.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +156 -5
  26. package/src/__tests__/conversation-agent-loop.test.ts +297 -2
  27. package/src/__tests__/conversation-attachments.test.ts +17 -19
  28. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  29. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  30. package/src/__tests__/conversation-error.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  32. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  33. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  34. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  35. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  36. package/src/__tests__/conversation-memory-dirty-tail.test.ts +150 -0
  37. package/src/__tests__/conversation-provider-retry-repair.test.ts +7 -0
  38. package/src/__tests__/conversation-queue.test.ts +36 -1
  39. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  40. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  41. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  42. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  43. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  44. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  45. package/src/__tests__/conversation-store.test.ts +24 -21
  46. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  47. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  48. package/src/__tests__/conversation-title-service.test.ts +137 -0
  49. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  50. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  51. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  52. package/src/__tests__/conversation-wipe.test.ts +226 -0
  53. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  54. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  55. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  56. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  57. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  58. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  59. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  60. package/src/__tests__/db-memory-archive-migration.test.ts +372 -0
  61. package/src/__tests__/db-memory-brief-state-migration.test.ts +213 -0
  62. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +273 -0
  63. package/src/__tests__/diagnostics-export.test.ts +70 -1
  64. package/src/__tests__/first-greeting.test.ts +80 -0
  65. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  66. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  67. package/src/__tests__/history-repair.test.ts +32 -10
  68. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  69. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  70. package/src/__tests__/inline-command-runner.test.ts +311 -0
  71. package/src/__tests__/inline-skill-authoring-guard.test.ts +220 -0
  72. package/src/__tests__/inline-skill-load-permissions.test.ts +435 -0
  73. package/src/__tests__/list-messages-attachments.test.ts +96 -0
  74. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  75. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  76. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  77. package/src/__tests__/media-generate-image.test.ts +47 -94
  78. package/src/__tests__/memory-brief-open-loops.test.ts +530 -0
  79. package/src/__tests__/memory-brief-time.test.ts +285 -0
  80. package/src/__tests__/memory-brief-wrapper.test.ts +311 -0
  81. package/src/__tests__/memory-chunk-archive.test.ts +400 -0
  82. package/src/__tests__/memory-chunk-dual-write.test.ts +453 -0
  83. package/src/__tests__/memory-episode-archive.test.ts +370 -0
  84. package/src/__tests__/memory-episode-dual-write.test.ts +626 -0
  85. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  86. package/src/__tests__/memory-observation-archive.test.ts +375 -0
  87. package/src/__tests__/memory-observation-dual-write.test.ts +318 -0
  88. package/src/__tests__/memory-recall-quality.test.ts +7 -7
  89. package/src/__tests__/memory-reducer-store.test.ts +728 -0
  90. package/src/__tests__/memory-reducer-types.test.ts +699 -0
  91. package/src/__tests__/memory-reducer.test.ts +698 -0
  92. package/src/__tests__/memory-regressions.test.ts +6 -4
  93. package/src/__tests__/memory-simplified-config.test.ts +281 -0
  94. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  95. package/src/__tests__/migration-export-http.test.ts +3 -1
  96. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  97. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  98. package/src/__tests__/mime-builder.test.ts +3 -2
  99. package/src/__tests__/non-member-access-request.test.ts +12 -1
  100. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  101. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  102. package/src/__tests__/oauth-store.test.ts +115 -0
  103. package/src/__tests__/parse-identity-fields.test.ts +129 -0
  104. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  105. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  106. package/src/__tests__/recording-handler.test.ts +17 -0
  107. package/src/__tests__/registry.test.ts +3 -8
  108. package/src/__tests__/relay-server.test.ts +1 -1
  109. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  110. package/src/__tests__/schema-transforms.test.ts +165 -5
  111. package/src/__tests__/server-history-render.test.ts +2 -2
  112. package/src/__tests__/skill-load-inline-command.test.ts +598 -0
  113. package/src/__tests__/skill-load-inline-includes.test.ts +644 -0
  114. package/src/__tests__/skills-inline-command-expansions.test.ts +301 -0
  115. package/src/__tests__/skills-transitive-hash.test.ts +333 -0
  116. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  117. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  118. package/src/__tests__/starter-task-flow.test.ts +1 -0
  119. package/src/__tests__/suggestion-routes.test.ts +443 -0
  120. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  121. package/src/__tests__/swarm-recursion.test.ts +1 -0
  122. package/src/__tests__/swarm-tool.test.ts +1 -0
  123. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  124. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  125. package/src/__tests__/top-level-renderer.test.ts +22 -0
  126. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  127. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +320 -0
  128. package/src/__tests__/web-fetch.test.ts +6 -2
  129. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  130. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  131. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  132. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  133. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  134. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  135. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  136. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  137. package/src/agent/attachments.ts +27 -1
  138. package/src/agent/loop.ts +29 -1
  139. package/src/avatar/traits-png-sync.ts +80 -25
  140. package/src/bundler/app-bundler.ts +4 -4
  141. package/src/calls/call-domain.ts +1 -0
  142. package/src/calls/voice-session-bridge.ts +1 -0
  143. package/src/cli/commands/auth.ts +92 -0
  144. package/src/cli/commands/avatar.ts +7 -6
  145. package/src/cli/commands/config.ts +2 -0
  146. package/src/cli/commands/oauth/providers.ts +29 -0
  147. package/src/cli/program.ts +12 -0
  148. package/src/cli.ts +15 -48
  149. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  150. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  151. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  152. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  153. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  154. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  155. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  156. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  157. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  158. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  159. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  160. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  161. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  162. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  163. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  164. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  165. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  166. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  170. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  171. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  172. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  173. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  174. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  175. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  176. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  177. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  178. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  179. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  180. package/src/config/bundled-tool-registry.ts +2 -14
  181. package/src/config/feature-flag-registry.json +24 -0
  182. package/src/config/loader.ts +65 -0
  183. package/src/config/raw-config-utils.ts +58 -0
  184. package/src/config/schema-utils.ts +28 -7
  185. package/src/config/schema.ts +20 -0
  186. package/src/config/schemas/elevenlabs.ts +18 -0
  187. package/src/config/schemas/memory-lifecycle.ts +4 -2
  188. package/src/config/schemas/memory-simplified.ts +101 -0
  189. package/src/config/schemas/memory-storage.ts +1 -1
  190. package/src/config/schemas/memory.ts +4 -0
  191. package/src/config/schemas/services.ts +8 -6
  192. package/src/config/skills.ts +50 -4
  193. package/src/contacts/contact-store.ts +13 -6
  194. package/src/contacts/contacts-write.ts +0 -1
  195. package/src/context/window-manager.ts +13 -2
  196. package/src/daemon/conversation-agent-loop-handlers.ts +54 -8
  197. package/src/daemon/conversation-agent-loop.ts +127 -20
  198. package/src/daemon/conversation-attachments.ts +18 -36
  199. package/src/daemon/conversation-error.ts +2 -1
  200. package/src/daemon/conversation-history.ts +18 -4
  201. package/src/daemon/conversation-lifecycle.ts +50 -16
  202. package/src/daemon/conversation-messaging.ts +70 -26
  203. package/src/daemon/conversation-process.ts +58 -34
  204. package/src/daemon/conversation-runtime-assembly.ts +22 -38
  205. package/src/daemon/conversation-slash.ts +121 -256
  206. package/src/daemon/conversation-surfaces.ts +170 -24
  207. package/src/daemon/conversation-tool-setup.ts +0 -6
  208. package/src/daemon/conversation-workspace.ts +21 -1
  209. package/src/daemon/conversation.ts +69 -30
  210. package/src/daemon/first-greeting.ts +35 -0
  211. package/src/daemon/handlers/config-embeddings.ts +156 -0
  212. package/src/daemon/handlers/config-model.ts +62 -26
  213. package/src/daemon/handlers/conversations.ts +0 -23
  214. package/src/daemon/handlers/identity.ts +12 -1
  215. package/src/daemon/handlers/recording.ts +26 -21
  216. package/src/daemon/host-cu-proxy.ts +2 -2
  217. package/src/daemon/lifecycle.ts +115 -65
  218. package/src/daemon/message-protocol.ts +3 -0
  219. package/src/daemon/message-types/conversations.ts +18 -0
  220. package/src/daemon/message-types/messages.ts +1 -0
  221. package/src/daemon/message-types/shared.ts +2 -0
  222. package/src/daemon/message-types/surfaces.ts +2 -0
  223. package/src/daemon/message-types/upgrades.ts +23 -0
  224. package/src/daemon/server.ts +83 -12
  225. package/src/daemon/shutdown-handlers.ts +8 -5
  226. package/src/daemon/startup-error.ts +9 -0
  227. package/src/daemon/tool-side-effects.ts +11 -28
  228. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  229. package/src/followups/followup-store.ts +47 -1
  230. package/src/instrument.ts +0 -4
  231. package/src/media/app-icon-generator.ts +2 -2
  232. package/src/memory/app-git-service.ts +28 -16
  233. package/src/memory/app-store.ts +230 -41
  234. package/src/memory/archive-store.ts +400 -0
  235. package/src/memory/attachments-store.ts +558 -130
  236. package/src/memory/brief-formatting.ts +33 -0
  237. package/src/memory/brief-open-loops.ts +266 -0
  238. package/src/memory/brief-time.ts +161 -0
  239. package/src/memory/brief.ts +75 -0
  240. package/src/memory/conversation-attention-store.ts +70 -0
  241. package/src/memory/conversation-crud.ts +591 -8
  242. package/src/memory/conversation-directories.ts +125 -0
  243. package/src/memory/conversation-disk-view.ts +390 -0
  244. package/src/memory/conversation-key-store.ts +17 -5
  245. package/src/memory/conversation-queries.ts +5 -1
  246. package/src/memory/conversation-title-service.ts +21 -49
  247. package/src/memory/db-init.ts +40 -0
  248. package/src/memory/embedding-backend.ts +42 -53
  249. package/src/memory/embedding-gemini.test.ts +4 -4
  250. package/src/memory/embedding-local.ts +1 -3
  251. package/src/memory/embedding-ollama.ts +1 -3
  252. package/src/memory/embedding-openai.ts +1 -3
  253. package/src/memory/indexer.ts +114 -21
  254. package/src/memory/items-extractor.ts +42 -13
  255. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  256. package/src/memory/job-handlers/embedding.test.ts +2 -4
  257. package/src/memory/job-handlers/embedding.ts +83 -0
  258. package/src/memory/job-utils.ts +1 -1
  259. package/src/memory/jobs-store.ts +6 -0
  260. package/src/memory/jobs-worker.ts +12 -0
  261. package/src/memory/llm-request-log-store.ts +100 -1
  262. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  263. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  264. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  265. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  266. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  267. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  268. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  269. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  270. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  271. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  272. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  273. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  274. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  275. package/src/memory/migrations/185-memory-brief-state.ts +52 -0
  276. package/src/memory/migrations/186-memory-archive.ts +109 -0
  277. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +19 -0
  278. package/src/memory/migrations/index.ts +10 -0
  279. package/src/memory/migrations/registry.ts +13 -0
  280. package/src/memory/qdrant-client.ts +23 -4
  281. package/src/memory/reducer-store.ts +271 -0
  282. package/src/memory/reducer-types.ts +99 -0
  283. package/src/memory/reducer.ts +453 -0
  284. package/src/memory/retriever.test.ts +601 -2
  285. package/src/memory/retriever.ts +85 -9
  286. package/src/memory/schema/conversations.ts +9 -0
  287. package/src/memory/schema/index.ts +2 -0
  288. package/src/memory/schema/infrastructure.ts +13 -7
  289. package/src/memory/schema/memory-archive.ts +121 -0
  290. package/src/memory/schema/memory-brief.ts +55 -0
  291. package/src/memory/schema/oauth.ts +6 -0
  292. package/src/memory/search/semantic.ts +17 -4
  293. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  294. package/src/notifications/copy-composer.ts +26 -0
  295. package/src/notifications/decision-engine.ts +14 -1
  296. package/src/notifications/emit-signal.ts +1 -1
  297. package/src/notifications/signal.ts +36 -0
  298. package/src/oauth/byo-connection.test.ts +1 -45
  299. package/src/oauth/byo-connection.ts +2 -8
  300. package/src/oauth/connect-orchestrator.ts +15 -11
  301. package/src/oauth/connection-resolver.test.ts +191 -0
  302. package/src/oauth/connection-resolver.ts +66 -38
  303. package/src/oauth/connection.ts +0 -1
  304. package/src/oauth/oauth-store.ts +99 -47
  305. package/src/oauth/platform-connection.test.ts +0 -1
  306. package/src/oauth/platform-connection.ts +11 -3
  307. package/src/oauth/seed-providers.ts +78 -3
  308. package/src/oauth/token-persistence.ts +16 -10
  309. package/src/permissions/checker.ts +160 -14
  310. package/src/permissions/defaults.ts +14 -0
  311. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  312. package/src/providers/anthropic/client.ts +8 -1
  313. package/src/providers/failover.ts +4 -1
  314. package/src/providers/gemini/client.ts +50 -0
  315. package/src/providers/model-catalog.ts +92 -0
  316. package/src/providers/model-intents.ts +29 -20
  317. package/src/providers/openai/client.ts +49 -0
  318. package/src/providers/types.ts +2 -0
  319. package/src/runtime/access-request-helper.ts +16 -7
  320. package/src/runtime/auth/credential-service.ts +3 -1
  321. package/src/runtime/auth/route-policy.ts +14 -1
  322. package/src/runtime/btw-sidechain.ts +101 -0
  323. package/src/runtime/channel-reply-delivery.ts +17 -1
  324. package/src/runtime/http-router.ts +3 -1
  325. package/src/runtime/http-server.ts +196 -141
  326. package/src/runtime/http-types.ts +1 -0
  327. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  328. package/src/runtime/routes/access-request-decision.ts +41 -0
  329. package/src/runtime/routes/app-management-routes.ts +6 -3
  330. package/src/runtime/routes/app-routes.ts +7 -3
  331. package/src/runtime/routes/approval-routes.ts +1 -0
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  333. package/src/runtime/routes/attachment-routes.ts +45 -15
  334. package/src/runtime/routes/btw-routes.ts +21 -61
  335. package/src/runtime/routes/conversation-management-routes.ts +74 -0
  336. package/src/runtime/routes/conversation-query-routes.ts +187 -10
  337. package/src/runtime/routes/conversation-routes.ts +269 -28
  338. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  339. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  340. package/src/runtime/routes/identity-routes.ts +2 -35
  341. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  342. package/src/runtime/routes/llm-context-normalization.ts +1212 -0
  343. package/src/runtime/routes/log-export-routes.ts +3 -0
  344. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  345. package/src/runtime/routes/memory-item-routes.ts +94 -5
  346. package/src/runtime/routes/migration-routes.ts +4 -1
  347. package/src/runtime/routes/oauth-apps.ts +291 -0
  348. package/src/runtime/routes/secret-routes.ts +30 -1
  349. package/src/runtime/routes/settings-routes.ts +14 -0
  350. package/src/runtime/routes/surface-action-routes.ts +68 -1
  351. package/src/runtime/routes/trace-event-routes.ts +4 -1
  352. package/src/schedule/schedule-store.ts +30 -21
  353. package/src/security/secure-keys.ts +21 -0
  354. package/src/signals/bash.ts +1 -1
  355. package/src/skills/inline-command-expansions.ts +204 -0
  356. package/src/skills/inline-command-render.ts +127 -0
  357. package/src/skills/inline-command-runner.ts +242 -0
  358. package/src/skills/transitive-version-hash.ts +88 -0
  359. package/src/swarm/backend-claude-code.ts +3 -6
  360. package/src/tasks/task-store.ts +43 -1
  361. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  362. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  363. package/src/tools/AGENTS.md +6 -10
  364. package/src/tools/apps/executors.ts +17 -232
  365. package/src/tools/claude-code/claude-code.ts +2 -3
  366. package/src/tools/credentials/vault.ts +7 -12
  367. package/src/tools/host-filesystem/read.ts +13 -10
  368. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  369. package/src/tools/permission-checker.ts +8 -1
  370. package/src/tools/schedule/list.ts +2 -7
  371. package/src/tools/schema-transforms.ts +5 -0
  372. package/src/tools/shared/filesystem/format-diff.ts +2 -7
  373. package/src/tools/skills/execute.ts +1 -1
  374. package/src/tools/skills/load.ts +140 -6
  375. package/src/tools/tool-manifest.ts +0 -6
  376. package/src/tools/ui-surface/definitions.ts +2 -2
  377. package/src/util/device-id.ts +28 -5
  378. package/src/util/platform.ts +24 -0
  379. package/src/util/pricing.ts +1 -0
  380. package/src/util/retry.ts +1 -3
  381. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  382. package/src/workspace/migrations/006-services-config.ts +5 -0
  383. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  384. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  385. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  386. package/src/workspace/migrations/{002-backfill-installation-id.ts → 011-backfill-installation-id.ts} +24 -13
  387. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  388. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  389. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  390. package/src/workspace/migrations/registry.ts +11 -1
  391. package/src/workspace/top-level-renderer.ts +12 -0
  392. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  393. package/src/__tests__/asset-search-tool.test.ts +0 -536
  394. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  395. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  396. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  397. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  398. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  399. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  400. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  401. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  402. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  403. package/src/daemon/media-visibility-policy.ts +0 -59
  404. package/src/tools/assets/materialize.ts +0 -248
  405. package/src/tools/assets/search.ts +0 -400
@@ -0,0 +1,598 @@
1
+ /**
2
+ * Tests for inline command expansion rendering during skill_load.
3
+ *
4
+ * Validates that:
5
+ * - Root skills with `!\`command\`` tokens get those tokens expanded exactly
6
+ * once at skill_load time, wrapped in <inline_skill_command> XML tags.
7
+ * - When the feature flag is off, skill_load returns an error instead of
8
+ * leaving unresolved live tokens in the prompt.
9
+ * - When the skill source is "extra", skill_load rejects with a specific error.
10
+ * - Render failures produce stable inline stubs rather than raw stderr.
11
+ */
12
+
13
+ import {
14
+ existsSync,
15
+ mkdirSync,
16
+ mkdtempSync,
17
+ rmSync,
18
+ writeFileSync,
19
+ } from "node:fs";
20
+ import { tmpdir } from "node:os";
21
+ import { join } from "node:path";
22
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
23
+
24
+ // ── Test directory ────────────────────────────────────────────────────────────
25
+
26
+ const TEST_DIR = mkdtempSync(
27
+ join(tmpdir(), "vellum-skill-load-inline-cmd-test-"),
28
+ );
29
+
30
+ // ── Mocks (must be declared before any imports from the project) ─────────────
31
+
32
+ const platformOverrides: Record<string, (...args: unknown[]) => unknown> = {
33
+ getRootDir: () => TEST_DIR,
34
+ getDataDir: () => join(TEST_DIR, "data"),
35
+ ensureDataDir: () => {},
36
+ getPidPath: () => join(TEST_DIR, "vellum.pid"),
37
+ getDbPath: () => join(TEST_DIR, "data", "assistant.db"),
38
+ getLogPath: () => join(TEST_DIR, "logs", "vellum.log"),
39
+ getWorkspaceDir: () => join(TEST_DIR, "workspace"),
40
+ getWorkspaceSkillsDir: () => join(TEST_DIR, "skills"),
41
+ getWorkspaceConfigPath: () => join(TEST_DIR, "workspace", "config.json"),
42
+ getWorkspaceHooksDir: () => join(TEST_DIR, "workspace", "hooks"),
43
+ getWorkspacePromptPath: (f: unknown) =>
44
+ join(TEST_DIR, "workspace", String(f)),
45
+ getInterfacesDir: () => join(TEST_DIR, "interfaces"),
46
+ getHooksDir: () => join(TEST_DIR, "hooks"),
47
+ getSandboxRootDir: () => join(TEST_DIR, "sandbox"),
48
+ getSandboxWorkingDir: () => join(TEST_DIR, "sandbox", "work"),
49
+ getHistoryPath: () => join(TEST_DIR, "history"),
50
+ getSessionTokenPath: () => join(TEST_DIR, "session-token"),
51
+ readSessionToken: () => null,
52
+ getClipboardCommand: () => null,
53
+ readLockfile: () => null,
54
+ normalizeAssistantId: (id: unknown) => String(id),
55
+ writeLockfile: () => {},
56
+ getEmbeddingModelsDir: () => join(TEST_DIR, "embedding-models"),
57
+ getTCPPort: () => 8765,
58
+ isTCPEnabled: () => false,
59
+ getTCPHost: () => "127.0.0.1",
60
+ isIOSPairingEnabled: () => false,
61
+ getPlatformTokenPath: () => join(TEST_DIR, "platform-token"),
62
+ readPlatformToken: () => null,
63
+ isMacOS: () => process.platform === "darwin",
64
+ isLinux: () => process.platform === "linux",
65
+ isWindows: () => process.platform === "win32",
66
+ getPlatformName: () => process.platform,
67
+ getWorkspaceDirDisplay: () => "~/.vellum/workspace",
68
+ getConversationsDir: () => join(TEST_DIR, "conversations"),
69
+ };
70
+ mock.module("../util/platform.js", () => platformOverrides);
71
+
72
+ mock.module("../util/logger.js", () => ({
73
+ getLogger: () =>
74
+ new Proxy({} as Record<string, unknown>, {
75
+ get: () => () => {},
76
+ }),
77
+ truncateForLog: (s: unknown) => String(s),
78
+ }));
79
+
80
+ // Track inline command runner calls
81
+ interface RunInlineCommandCall {
82
+ command: string;
83
+ workingDir: string;
84
+ }
85
+ const runInlineCommandCalls: RunInlineCommandCall[] = [];
86
+
87
+ /** Return type matching InlineCommandResult from the runner module. */
88
+ interface MockInlineCommandResult {
89
+ output: string;
90
+ ok: boolean;
91
+ failureReason?:
92
+ | "timeout"
93
+ | "non_zero_exit"
94
+ | "binary_output"
95
+ | "spawn_failure";
96
+ }
97
+
98
+ type MockRunFn = (
99
+ command: string,
100
+ workingDir: string,
101
+ ) => Promise<MockInlineCommandResult>;
102
+
103
+ // Default mock: commands succeed with their command string echoed
104
+ let mockRunInlineCommand = mock<MockRunFn>(
105
+ (command: string, workingDir: string) => {
106
+ runInlineCommandCalls.push({ command, workingDir });
107
+ return Promise.resolve({
108
+ output: `result of: ${command}`,
109
+ ok: true,
110
+ });
111
+ },
112
+ );
113
+
114
+ mock.module("../skills/inline-command-runner.js", () => ({
115
+ runInlineCommand: (command: string, workingDir: string, _options?: unknown) =>
116
+ mockRunInlineCommand(command, workingDir),
117
+ }));
118
+
119
+ // Mock autoInstallFromCatalog
120
+ const mockAutoInstall = mock((_skillId: string) => Promise.resolve(false));
121
+ mock.module("../skills/catalog-install.js", () => ({
122
+ autoInstallFromCatalog: (skillId: string) => mockAutoInstall(skillId),
123
+ resolveCatalog: (_skillId?: string) => Promise.resolve([]),
124
+ }));
125
+
126
+ interface TestConfig {
127
+ permissions: { mode: "strict" | "workspace" };
128
+ skills: { load: { extraDirs: string[] } };
129
+ sandbox: { enabled: boolean };
130
+ assistantFeatureFlagValues?: Record<string, boolean>;
131
+ [key: string]: unknown;
132
+ }
133
+
134
+ const testConfig: TestConfig = {
135
+ permissions: { mode: "workspace" },
136
+ skills: { load: { extraDirs: [] } },
137
+ sandbox: { enabled: true },
138
+ assistantFeatureFlagValues: {
139
+ "feature_flags.inline-skill-commands.enabled": true,
140
+ },
141
+ };
142
+
143
+ mock.module("../config/loader.js", () => ({
144
+ getConfig: () => testConfig,
145
+ loadConfig: () => testConfig,
146
+ invalidateConfigCache: () => {},
147
+ saveConfig: () => {},
148
+ loadRawConfig: () => ({}),
149
+ saveRawConfig: () => {},
150
+ getNestedValue: () => undefined,
151
+ setNestedValue: () => {},
152
+ }));
153
+
154
+ // ── Imports (after mocks) ─────────────────────────────────────────────────
155
+
156
+ await import("../tools/skills/load.js");
157
+ const { getTool } = await import("../tools/registry.js");
158
+
159
+ // ── Helpers ───────────────────────────────────────────────────────────────
160
+
161
+ function writeSkill(
162
+ skillId: string,
163
+ name: string,
164
+ description: string,
165
+ body: string,
166
+ ): void {
167
+ const skillDir = join(TEST_DIR, "skills", skillId);
168
+ mkdirSync(skillDir, { recursive: true });
169
+ writeFileSync(
170
+ join(skillDir, "SKILL.md"),
171
+ `---\nname: "${name}"\ndescription: "${description}"\n---\n\n${body}\n`,
172
+ );
173
+ }
174
+
175
+ async function executeSkillLoad(
176
+ input: Record<string, unknown>,
177
+ workingDir = "/tmp",
178
+ ): Promise<{ content: string; isError: boolean }> {
179
+ const tool = getTool("skill_load");
180
+ if (!tool) throw new Error("skill_load tool was not registered");
181
+
182
+ const result = await tool.execute(input, {
183
+ workingDir,
184
+ conversationId: "conversation-1",
185
+ trustClass: "guardian",
186
+ });
187
+ return { content: result.content, isError: result.isError };
188
+ }
189
+
190
+ // ── Tests ─────────────────────────────────────────────────────────────────
191
+
192
+ describe("skill_load inline command expansion", () => {
193
+ beforeEach(() => {
194
+ mkdirSync(join(TEST_DIR, "skills"), { recursive: true });
195
+ runInlineCommandCalls.length = 0;
196
+ mockAutoInstall.mockReset();
197
+ mockAutoInstall.mockImplementation(() => Promise.resolve(false));
198
+
199
+ // Reset to default: commands succeed
200
+ mockRunInlineCommand = mock<MockRunFn>(
201
+ (command: string, workingDir: string) => {
202
+ runInlineCommandCalls.push({ command, workingDir });
203
+ return Promise.resolve({
204
+ output: `result of: ${command}`,
205
+ ok: true,
206
+ });
207
+ },
208
+ );
209
+ mock.module("../skills/inline-command-runner.js", () => ({
210
+ runInlineCommand: (
211
+ command: string,
212
+ workingDir: string,
213
+ _options?: unknown,
214
+ ) => mockRunInlineCommand(command, workingDir),
215
+ }));
216
+
217
+ // Enable the feature flag
218
+ testConfig.assistantFeatureFlagValues = {
219
+ "feature_flags.inline-skill-commands.enabled": true,
220
+ };
221
+ testConfig.skills = { load: { extraDirs: [] } };
222
+ });
223
+
224
+ afterEach(() => {
225
+ if (existsSync(TEST_DIR)) {
226
+ rmSync(TEST_DIR, { recursive: true, force: true });
227
+ }
228
+ });
229
+
230
+ // ── Basic expansion ──────────────────────────────────────────────────
231
+
232
+ describe("basic expansion", () => {
233
+ test("expands a single inline command token in a root skill", async () => {
234
+ writeSkill(
235
+ "dynamic-skill",
236
+ "Dynamic Skill",
237
+ "A skill with inline commands",
238
+ 'Current date: !`echo "2024-01-01"`',
239
+ );
240
+
241
+ const result = await executeSkillLoad({ skill: "dynamic-skill" });
242
+ expect(result.isError).toBe(false);
243
+ expect(result.content).toContain(
244
+ '<inline_skill_command index="0">result of: echo "2024-01-01"</inline_skill_command>',
245
+ );
246
+ // The original token should be replaced
247
+ expect(result.content).not.toContain("!`echo");
248
+ });
249
+
250
+ test("expands multiple inline command tokens in encounter order", async () => {
251
+ writeSkill(
252
+ "multi-cmd-skill",
253
+ "Multi Command Skill",
254
+ "A skill with multiple inline commands",
255
+ "First: !`cmd-one`\nSecond: !`cmd-two`\nThird: !`cmd-three`",
256
+ );
257
+
258
+ const result = await executeSkillLoad({ skill: "multi-cmd-skill" });
259
+ expect(result.isError).toBe(false);
260
+ expect(result.content).toContain(
261
+ '<inline_skill_command index="0">result of: cmd-one</inline_skill_command>',
262
+ );
263
+ expect(result.content).toContain(
264
+ '<inline_skill_command index="1">result of: cmd-two</inline_skill_command>',
265
+ );
266
+ expect(result.content).toContain(
267
+ '<inline_skill_command index="2">result of: cmd-three</inline_skill_command>',
268
+ );
269
+ });
270
+
271
+ test("tokens are expanded exactly once (not re-expanded)", async () => {
272
+ writeSkill(
273
+ "once-skill",
274
+ "Once Skill",
275
+ "Expand only once",
276
+ "Data: !`echo hello`",
277
+ );
278
+
279
+ await executeSkillLoad({ skill: "once-skill" });
280
+ // The runner should be called exactly once for this one token
281
+ expect(runInlineCommandCalls).toHaveLength(1);
282
+ expect(runInlineCommandCalls[0].command).toBe("echo hello");
283
+ });
284
+
285
+ test("passes the conversation working directory to the runner", async () => {
286
+ writeSkill("cwd-skill", "CWD Skill", "Check working dir", "Info: !`pwd`");
287
+
288
+ const workingDir = "/my/project/root";
289
+ await executeSkillLoad({ skill: "cwd-skill" }, workingDir);
290
+ expect(runInlineCommandCalls).toHaveLength(1);
291
+ expect(runInlineCommandCalls[0].workingDir).toBe(workingDir);
292
+ });
293
+ });
294
+
295
+ // ── Plain skills (no inline commands) ────────────────────────────────
296
+
297
+ describe("plain skills", () => {
298
+ test("plain skill without inline commands loads normally", async () => {
299
+ writeSkill(
300
+ "plain-skill",
301
+ "Plain Skill",
302
+ "No inline commands",
303
+ "Just regular content.",
304
+ );
305
+
306
+ const result = await executeSkillLoad({ skill: "plain-skill" });
307
+ expect(result.isError).toBe(false);
308
+ expect(result.content).toContain("Just regular content.");
309
+ expect(result.content).not.toContain("inline_skill_command");
310
+ // Runner should not be called
311
+ expect(runInlineCommandCalls).toHaveLength(0);
312
+ });
313
+ });
314
+
315
+ // ── Feature flag off ─────────────────────────────────────────────────
316
+
317
+ describe("feature flag disabled", () => {
318
+ test("returns error when flag is off and skill has inline commands", async () => {
319
+ testConfig.assistantFeatureFlagValues = {
320
+ "feature_flags.inline-skill-commands.enabled": false,
321
+ };
322
+
323
+ writeSkill(
324
+ "flagged-off-skill",
325
+ "Flagged Off Skill",
326
+ "Has inline commands",
327
+ "Data: !`echo hello`",
328
+ );
329
+
330
+ const result = await executeSkillLoad({ skill: "flagged-off-skill" });
331
+ expect(result.isError).toBe(true);
332
+ expect(result.content).toContain(
333
+ "inline-skill-commands feature flag is disabled",
334
+ );
335
+ // Runner should not be called when flag is off
336
+ expect(runInlineCommandCalls).toHaveLength(0);
337
+ });
338
+
339
+ test("plain skill still loads when flag is off", async () => {
340
+ testConfig.assistantFeatureFlagValues = {
341
+ "feature_flags.inline-skill-commands.enabled": false,
342
+ };
343
+
344
+ writeSkill(
345
+ "plain-flag-off",
346
+ "Plain Flag Off",
347
+ "No inline commands",
348
+ "Regular content.",
349
+ );
350
+
351
+ const result = await executeSkillLoad({ skill: "plain-flag-off" });
352
+ expect(result.isError).toBe(false);
353
+ expect(result.content).toContain("Regular content.");
354
+ });
355
+ });
356
+
357
+ // ── Extra source rejection ───────────────────────────────────────────
358
+ //
359
+ // The SkillLoadTool checks `skill.source === "extra"` and rejects inline
360
+ // command expansion for third-party skill sources. Since `loadSkillBySelector`
361
+ // doesn't propagate `extraDirs` from config, extra-source skills can't be
362
+ // easily loaded through the tool in a unit test without deep mocking.
363
+ //
364
+ // We verify the rejection logic exists by importing the tool class directly
365
+ // and confirming the code path rejects extra sources when inline commands
366
+ // are present. The actual source-level guard is tested by checking that
367
+ // workspace-source skills *do* expand (above) while the code explicitly
368
+ // gates on INLINE_COMMAND_ELIGIBLE_SOURCES which excludes "extra".
369
+
370
+ describe("extra source rejection (code-level verification)", () => {
371
+ test("INLINE_COMMAND_ELIGIBLE_SOURCES does not include 'extra'", async () => {
372
+ // Read the load.ts source to verify the eligible sources set
373
+ const { readFileSync } = await import("node:fs");
374
+ const { join: pjoin } = await import("node:path");
375
+ const loadSrc = readFileSync(
376
+ pjoin(
377
+ import.meta.dirname ?? __dirname,
378
+ "..",
379
+ "tools",
380
+ "skills",
381
+ "load.ts",
382
+ ),
383
+ "utf-8",
384
+ );
385
+ // The eligible sources set must include bundled, managed, workspace but NOT extra
386
+ expect(loadSrc).toContain('"bundled"');
387
+ expect(loadSrc).toContain('"managed"');
388
+ expect(loadSrc).toContain('"workspace"');
389
+ expect(loadSrc).toContain('skill.source === "extra"');
390
+ });
391
+ });
392
+
393
+ // ── Render failures ──────────────────────────────────────────────────
394
+
395
+ describe("render failures", () => {
396
+ test("timeout renders stable stub", async () => {
397
+ mockRunInlineCommand = mock<MockRunFn>(
398
+ (command: string, workingDir: string) => {
399
+ runInlineCommandCalls.push({ command, workingDir });
400
+ return Promise.resolve({
401
+ output: "Inline command timed out after 10000ms.",
402
+ ok: false,
403
+ failureReason: "timeout",
404
+ });
405
+ },
406
+ );
407
+ mock.module("../skills/inline-command-runner.js", () => ({
408
+ runInlineCommand: (
409
+ command: string,
410
+ workingDir: string,
411
+ _options?: unknown,
412
+ ) => mockRunInlineCommand(command, workingDir),
413
+ }));
414
+
415
+ writeSkill(
416
+ "timeout-skill",
417
+ "Timeout Skill",
418
+ "Command times out",
419
+ "Data: !`sleep 999`",
420
+ );
421
+
422
+ const result = await executeSkillLoad({ skill: "timeout-skill" });
423
+ expect(result.isError).toBe(false);
424
+ expect(result.content).toContain(
425
+ '<inline_skill_command index="0">[inline command unavailable: command timed out]</inline_skill_command>',
426
+ );
427
+ // Original token should be replaced
428
+ expect(result.content).not.toContain("!`sleep");
429
+ });
430
+
431
+ test("non-zero exit renders stable stub", async () => {
432
+ mockRunInlineCommand = mock<MockRunFn>(
433
+ (command: string, workingDir: string) => {
434
+ runInlineCommandCalls.push({ command, workingDir });
435
+ return Promise.resolve({
436
+ output: "Inline command failed (exit code 1).",
437
+ ok: false,
438
+ failureReason: "non_zero_exit",
439
+ });
440
+ },
441
+ );
442
+ mock.module("../skills/inline-command-runner.js", () => ({
443
+ runInlineCommand: (
444
+ command: string,
445
+ workingDir: string,
446
+ _options?: unknown,
447
+ ) => mockRunInlineCommand(command, workingDir),
448
+ }));
449
+
450
+ writeSkill("fail-skill", "Fail Skill", "Command fails", "Data: !`false`");
451
+
452
+ const result = await executeSkillLoad({ skill: "fail-skill" });
453
+ expect(result.isError).toBe(false);
454
+ expect(result.content).toContain(
455
+ '<inline_skill_command index="0">[inline command unavailable: command failed]</inline_skill_command>',
456
+ );
457
+ });
458
+
459
+ test("spawn failure renders stable stub", async () => {
460
+ mockRunInlineCommand = mock<MockRunFn>(
461
+ (command: string, workingDir: string) => {
462
+ runInlineCommandCalls.push({ command, workingDir });
463
+ return Promise.resolve({
464
+ output: "Inline command could not be started.",
465
+ ok: false,
466
+ failureReason: "spawn_failure",
467
+ });
468
+ },
469
+ );
470
+ mock.module("../skills/inline-command-runner.js", () => ({
471
+ runInlineCommand: (
472
+ command: string,
473
+ workingDir: string,
474
+ _options?: unknown,
475
+ ) => mockRunInlineCommand(command, workingDir),
476
+ }));
477
+
478
+ writeSkill(
479
+ "spawn-fail-skill",
480
+ "Spawn Fail Skill",
481
+ "Command spawn fails",
482
+ "Data: !`nonexistent-binary`",
483
+ );
484
+
485
+ const result = await executeSkillLoad({ skill: "spawn-fail-skill" });
486
+ expect(result.isError).toBe(false);
487
+ expect(result.content).toContain(
488
+ '<inline_skill_command index="0">[inline command unavailable: command could not be started]</inline_skill_command>',
489
+ );
490
+ });
491
+
492
+ test("binary output renders stable stub", async () => {
493
+ mockRunInlineCommand = mock<MockRunFn>(
494
+ (command: string, workingDir: string) => {
495
+ runInlineCommandCalls.push({ command, workingDir });
496
+ return Promise.resolve({
497
+ output: "Inline command produced binary output.",
498
+ ok: false,
499
+ failureReason: "binary_output",
500
+ });
501
+ },
502
+ );
503
+ mock.module("../skills/inline-command-runner.js", () => ({
504
+ runInlineCommand: (
505
+ command: string,
506
+ workingDir: string,
507
+ _options?: unknown,
508
+ ) => mockRunInlineCommand(command, workingDir),
509
+ }));
510
+
511
+ writeSkill(
512
+ "binary-skill",
513
+ "Binary Skill",
514
+ "Command produces binary",
515
+ "Data: !`cat /dev/urandom`",
516
+ );
517
+
518
+ const result = await executeSkillLoad({ skill: "binary-skill" });
519
+ expect(result.isError).toBe(false);
520
+ expect(result.content).toContain(
521
+ '<inline_skill_command index="0">[inline command unavailable: command produced binary output]</inline_skill_command>',
522
+ );
523
+ });
524
+
525
+ test("mixed success and failure renders both correctly", async () => {
526
+ let callIndex = 0;
527
+ mockRunInlineCommand = mock<MockRunFn>(
528
+ (command: string, workingDir: string) => {
529
+ runInlineCommandCalls.push({ command, workingDir });
530
+ const idx = callIndex++;
531
+ if (idx === 1) {
532
+ // Second command fails
533
+ return Promise.resolve({
534
+ output: "Inline command failed (exit code 1).",
535
+ ok: false,
536
+ failureReason: "non_zero_exit",
537
+ });
538
+ }
539
+ return Promise.resolve({
540
+ output: `result of: ${command}`,
541
+ ok: true,
542
+ });
543
+ },
544
+ );
545
+ mock.module("../skills/inline-command-runner.js", () => ({
546
+ runInlineCommand: (
547
+ command: string,
548
+ workingDir: string,
549
+ _options?: unknown,
550
+ ) => mockRunInlineCommand(command, workingDir),
551
+ }));
552
+
553
+ writeSkill(
554
+ "mixed-skill",
555
+ "Mixed Skill",
556
+ "Some commands fail",
557
+ "A: !`echo ok` B: !`bad-cmd` C: !`echo fine`",
558
+ );
559
+
560
+ const result = await executeSkillLoad({ skill: "mixed-skill" });
561
+ expect(result.isError).toBe(false);
562
+ // First and third succeed
563
+ expect(result.content).toContain(
564
+ '<inline_skill_command index="0">result of: echo ok</inline_skill_command>',
565
+ );
566
+ expect(result.content).toContain(
567
+ '<inline_skill_command index="2">result of: echo fine</inline_skill_command>',
568
+ );
569
+ // Second fails with stub
570
+ expect(result.content).toContain(
571
+ '<inline_skill_command index="1">[inline command unavailable: command failed]</inline_skill_command>',
572
+ );
573
+ });
574
+ });
575
+
576
+ // ── XML wrapper format ───────────────────────────────────────────────
577
+
578
+ describe("XML wrapper format", () => {
579
+ test("output is wrapped in <inline_skill_command> with index attribute", async () => {
580
+ writeSkill(
581
+ "xml-skill",
582
+ "XML Skill",
583
+ "Check XML wrapping",
584
+ "Info: !`echo data`",
585
+ );
586
+
587
+ const result = await executeSkillLoad({ skill: "xml-skill" });
588
+ expect(result.isError).toBe(false);
589
+ // Verify exact XML tag format
590
+ const match = result.content.match(
591
+ /<inline_skill_command index="(\d+)">(.*?)<\/inline_skill_command>/,
592
+ );
593
+ expect(match).not.toBeNull();
594
+ expect(match![1]).toBe("0");
595
+ expect(match![2]).toBe("result of: echo data");
596
+ });
597
+ });
598
+ });