vellum 0.2.1 → 0.2.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 (349) hide show
  1. package/README.md +15 -2
  2. package/bun.lock +5 -2
  3. package/package.json +4 -2
  4. package/scripts/capture-x-graphql.ts +562 -0
  5. package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
  6. package/scripts/test.sh +5 -0
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +133 -34
  8. package/src/__tests__/account-registry.test.ts +2 -1
  9. package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
  10. package/src/__tests__/asset-materialize-tool.test.ts +16 -15
  11. package/src/__tests__/asset-search-tool.test.ts +23 -22
  12. package/src/__tests__/attachments-store.test.ts +56 -127
  13. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
  14. package/src/__tests__/browser-skill-endstate.test.ts +4 -3
  15. package/src/__tests__/call-bridge.test.ts +385 -0
  16. package/src/__tests__/call-constants.test.ts +40 -0
  17. package/src/__tests__/call-orchestrator.test.ts +130 -4
  18. package/src/__tests__/call-recovery.test.ts +518 -0
  19. package/src/__tests__/call-routes-http.test.ts +459 -0
  20. package/src/__tests__/call-state-machine.test.ts +143 -0
  21. package/src/__tests__/call-store.test.ts +216 -1
  22. package/src/__tests__/cli-discover.test.ts +1 -1
  23. package/src/__tests__/commit-message-enrichment-service.test.ts +148 -7
  24. package/src/__tests__/compaction.benchmark.test.ts +176 -0
  25. package/src/__tests__/computer-use-tools.test.ts +250 -0
  26. package/src/__tests__/config-schema.test.ts +299 -3
  27. package/src/__tests__/conflict-store.test.ts +2 -1
  28. package/src/__tests__/contacts-tools.test.ts +331 -0
  29. package/src/__tests__/conversation-store.test.ts +30 -32
  30. package/src/__tests__/credential-security-invariants.test.ts +4 -0
  31. package/src/__tests__/date-context.test.ts +373 -0
  32. package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
  33. package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
  34. package/src/__tests__/followup-tools.test.ts +303 -0
  35. package/src/__tests__/handlers-twitter-config.test.ts +718 -0
  36. package/src/__tests__/intent-routing.test.ts +64 -57
  37. package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
  38. package/src/__tests__/ipc-snapshot.test.ts +62 -28
  39. package/src/__tests__/llm-usage-store.test.ts +3 -8
  40. package/src/__tests__/media-generate-image.test.ts +1 -1
  41. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  42. package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
  43. package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
  44. package/src/__tests__/playbook-tools.test.ts +342 -0
  45. package/src/__tests__/profile-compiler.test.ts +2 -1
  46. package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
  47. package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
  48. package/src/__tests__/recurrence-engine.test.ts +69 -0
  49. package/src/__tests__/recurrence-types.test.ts +71 -0
  50. package/src/__tests__/registry.test.ts +5 -3
  51. package/src/__tests__/relay-server.test.ts +633 -0
  52. package/src/__tests__/reminder-store.test.ts +6 -3
  53. package/src/__tests__/reminder.test.ts +43 -77
  54. package/src/__tests__/run-orchestrator-assistant-events.test.ts +8 -4
  55. package/src/__tests__/run-orchestrator.test.ts +4 -4
  56. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -6
  57. package/src/__tests__/runtime-runs-http.test.ts +4 -4
  58. package/src/__tests__/runtime-runs.test.ts +4 -4
  59. package/src/__tests__/schedule-store.test.ts +482 -0
  60. package/src/__tests__/schedule-tools.test.ts +700 -0
  61. package/src/__tests__/scheduler-recurrence.test.ts +329 -0
  62. package/src/__tests__/server-history-render.test.ts +14 -13
  63. package/src/__tests__/session-error.test.ts +28 -0
  64. package/src/__tests__/session-init.benchmark.test.ts +462 -0
  65. package/src/__tests__/session-queue.test.ts +71 -48
  66. package/src/__tests__/session-runtime-assembly.test.ts +161 -0
  67. package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
  68. package/src/__tests__/signup-e2e.test.ts +2 -1
  69. package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
  70. package/src/__tests__/skill-script-runner.test.ts +159 -0
  71. package/src/__tests__/speaker-identification.test.ts +52 -0
  72. package/src/__tests__/subagent-manager-notify.test.ts +42 -10
  73. package/src/__tests__/subagent-tools.test.ts +141 -41
  74. package/src/__tests__/task-compiler.test.ts +2 -1
  75. package/src/__tests__/task-runner.test.ts +2 -1
  76. package/src/__tests__/task-scheduler.test.ts +2 -1
  77. package/src/__tests__/task-tools.test.ts +49 -56
  78. package/src/__tests__/tool-audit-listener.test.ts +1 -0
  79. package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
  80. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
  81. package/src/__tests__/tool-executor.test.ts +13 -17
  82. package/src/__tests__/turn-commit.test.ts +218 -3
  83. package/src/__tests__/twilio-provider.test.ts +143 -0
  84. package/src/__tests__/twilio-routes.test.ts +789 -0
  85. package/src/__tests__/twitter-auth-handler.test.ts +581 -0
  86. package/src/__tests__/view-image-tool.test.ts +217 -0
  87. package/src/__tests__/workspace-git-service.test.ts +186 -0
  88. package/src/__tests__/workspace-heartbeat-service.test.ts +13 -3
  89. package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
  90. package/src/bundler/app-bundler.ts +12 -8
  91. package/src/calls/call-bridge.ts +95 -0
  92. package/src/calls/call-constants.ts +43 -5
  93. package/src/calls/call-domain.ts +276 -0
  94. package/src/calls/call-orchestrator.ts +43 -17
  95. package/src/calls/call-recovery.ts +207 -0
  96. package/src/calls/call-state-machine.ts +68 -0
  97. package/src/calls/call-store.ts +192 -5
  98. package/src/calls/relay-server.ts +41 -4
  99. package/src/calls/speaker-identification.ts +213 -0
  100. package/src/calls/twilio-provider.ts +10 -6
  101. package/src/calls/twilio-routes.ts +90 -76
  102. package/src/calls/types.ts +1 -1
  103. package/src/cli/config-commands.ts +334 -0
  104. package/src/cli/core-commands.ts +776 -0
  105. package/src/cli/doordash.ts +251 -1
  106. package/src/cli/ipc-client.ts +82 -0
  107. package/src/cli/map.ts +246 -0
  108. package/src/cli/twitter.ts +575 -0
  109. package/src/cli.ts +7 -5
  110. package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
  111. package/src/commands/cc-command-registry.ts +209 -0
  112. package/src/config/bundled-skills/contacts/SKILL.md +39 -0
  113. package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
  114. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
  115. package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
  116. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
  117. package/src/config/bundled-skills/document/SKILL.md +18 -0
  118. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  119. package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
  120. package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
  121. package/src/config/bundled-skills/doordash/SKILL.md +82 -23
  122. package/src/config/bundled-skills/followups/SKILL.md +32 -0
  123. package/src/config/bundled-skills/followups/TOOLS.json +100 -0
  124. package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
  125. package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
  126. package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
  127. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -23
  128. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
  129. package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
  130. package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
  131. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
  132. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
  133. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
  134. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
  135. package/src/config/bundled-skills/reminder/SKILL.md +20 -0
  136. package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
  137. package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
  138. package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
  139. package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +74 -0
  141. package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
  142. package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
  143. package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
  144. package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
  145. package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
  146. package/src/config/bundled-skills/subagent/SKILL.md +25 -0
  147. package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
  148. package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
  149. package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
  150. package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
  151. package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
  152. package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
  153. package/src/config/bundled-skills/tasks/SKILL.md +28 -0
  154. package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
  155. package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
  156. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
  157. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
  158. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
  159. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
  160. package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
  161. package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
  162. package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
  163. package/src/config/bundled-skills/twitter/SKILL.md +134 -0
  164. package/src/config/bundled-skills/watcher/SKILL.md +27 -0
  165. package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
  166. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
  167. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
  168. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
  169. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
  170. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
  171. package/src/config/defaults.ts +33 -0
  172. package/src/config/loader.ts +4 -1
  173. package/src/config/schema.ts +161 -1
  174. package/src/config/system-prompt.ts +61 -16
  175. package/src/config/templates/IDENTITY.md +7 -0
  176. package/src/config/types.ts +4 -0
  177. package/src/contacts/contact-store.ts +4 -4
  178. package/src/daemon/assistant-attachments.ts +10 -0
  179. package/src/daemon/classifier.ts +3 -1
  180. package/src/daemon/computer-use-session.ts +3 -1
  181. package/src/daemon/date-context.ts +136 -0
  182. package/src/daemon/handlers/apps.ts +16 -1
  183. package/src/daemon/handlers/browser.ts +54 -0
  184. package/src/daemon/handlers/computer-use.ts +7 -1
  185. package/src/daemon/handlers/config.ts +163 -5
  186. package/src/daemon/handlers/diagnostics.ts +5 -1
  187. package/src/daemon/handlers/documents.ts +18 -29
  188. package/src/daemon/handlers/home-base.ts +5 -1
  189. package/src/daemon/handlers/index.ts +40 -277
  190. package/src/daemon/handlers/misc.ts +9 -1
  191. package/src/daemon/handlers/publish.ts +6 -1
  192. package/src/daemon/handlers/sessions.ts +65 -12
  193. package/src/daemon/handlers/shared.ts +36 -1
  194. package/src/daemon/handlers/signing.ts +37 -0
  195. package/src/daemon/handlers/skills.ts +20 -6
  196. package/src/daemon/handlers/subagents.ts +8 -3
  197. package/src/daemon/handlers/twitter-auth.ts +169 -0
  198. package/src/daemon/handlers/work-items.ts +384 -68
  199. package/src/daemon/ipc-contract-inventory.json +28 -4
  200. package/src/daemon/ipc-contract.ts +133 -37
  201. package/src/daemon/ipc-protocol.ts +7 -2
  202. package/src/daemon/lifecycle.ts +21 -0
  203. package/src/daemon/main.ts +10 -4
  204. package/src/daemon/ride-shotgun-handler.ts +74 -10
  205. package/src/daemon/server.ts +143 -26
  206. package/src/daemon/session-agent-loop.ts +887 -0
  207. package/src/daemon/session-attachments.ts +28 -5
  208. package/src/daemon/session-error.ts +24 -3
  209. package/src/daemon/session-lifecycle.ts +147 -0
  210. package/src/daemon/session-media-retry.ts +147 -0
  211. package/src/daemon/session-messaging.ts +145 -0
  212. package/src/daemon/session-notifiers.ts +164 -0
  213. package/src/daemon/session-process.ts +2 -2
  214. package/src/daemon/session-queue-manager.ts +1 -0
  215. package/src/daemon/session-runtime-assembly.ts +52 -0
  216. package/src/daemon/session-skill-tools.ts +124 -5
  217. package/src/daemon/session-slash.ts +3 -0
  218. package/src/daemon/session-surfaces.ts +77 -2
  219. package/src/daemon/session-tool-setup.ts +216 -2
  220. package/src/daemon/session-usage.ts +0 -2
  221. package/src/daemon/session.ts +114 -1404
  222. package/src/daemon/video-thumbnail.ts +60 -0
  223. package/src/doordash/client.ts +121 -27
  224. package/src/doordash/queries.ts +1 -2
  225. package/src/export/formatter.ts +3 -1
  226. package/src/followups/followup-store.ts +4 -2
  227. package/src/followups/types.ts +6 -0
  228. package/src/hooks/templates.ts +1 -1
  229. package/src/index.ts +32 -1153
  230. package/src/memory/attachments-store.ts +28 -83
  231. package/src/memory/channel-delivery-store.ts +7 -21
  232. package/src/memory/clarification-resolver.ts +6 -5
  233. package/src/memory/contradiction-checker.ts +3 -2
  234. package/src/memory/conversation-key-store.ts +10 -29
  235. package/src/memory/conversation-store.ts +2 -1
  236. package/src/memory/db.ts +96 -2
  237. package/src/memory/entity-extractor.ts +6 -3
  238. package/src/memory/items-extractor.ts +5 -4
  239. package/src/memory/jobs-store.ts +3 -2
  240. package/src/memory/llm-usage-store.ts +1 -2
  241. package/src/memory/runs-store.ts +1 -2
  242. package/src/memory/schema.ts +23 -2
  243. package/src/messaging/style-analyzer.ts +3 -2
  244. package/src/messaging/thread-summarizer.ts +8 -12
  245. package/src/messaging/triage-engine.ts +4 -2
  246. package/src/providers/openrouter/client.ts +20 -0
  247. package/src/providers/registry.ts +8 -0
  248. package/src/runtime/http-server.ts +108 -20
  249. package/src/runtime/routes/attachment-routes.ts +2 -3
  250. package/src/runtime/routes/call-routes.ts +140 -0
  251. package/src/runtime/routes/channel-routes.ts +5 -10
  252. package/src/runtime/routes/conversation-routes.ts +5 -5
  253. package/src/runtime/routes/run-routes.ts +2 -2
  254. package/src/runtime/run-orchestrator.ts +9 -3
  255. package/src/schedule/recurrence-engine.ts +138 -0
  256. package/src/schedule/recurrence-types.ts +67 -0
  257. package/src/schedule/schedule-store.ts +102 -57
  258. package/src/schedule/scheduler.ts +9 -6
  259. package/src/security/oauth2.ts +29 -4
  260. package/src/security/secret-allowlist.ts +46 -0
  261. package/src/skills/clawhub.ts +1 -1
  262. package/src/subagent/manager.ts +40 -8
  263. package/src/swarm/backend-claude-code.ts +64 -9
  264. package/src/swarm/worker-prompts.ts +2 -1
  265. package/src/tasks/SPEC.md +34 -28
  266. package/src/tasks/ephemeral-permissions.ts +16 -7
  267. package/src/tasks/task-compiler.ts +5 -4
  268. package/src/tasks/task-runner.ts +10 -5
  269. package/src/tasks/task-scheduler.ts +1 -1
  270. package/src/tasks/tool-sanitizer.ts +36 -0
  271. package/src/tools/assets/search.ts +4 -4
  272. package/src/tools/browser/api-map.ts +220 -0
  273. package/src/tools/browser/auto-navigate.ts +270 -0
  274. package/src/tools/browser/browser-execution.ts +2 -1
  275. package/src/tools/browser/browser-manager.ts +2 -2
  276. package/src/tools/browser/network-recorder.ts +5 -4
  277. package/src/tools/browser/x-auto-navigate.ts +207 -0
  278. package/src/tools/calls/call-end.ts +17 -67
  279. package/src/tools/calls/call-start.ts +24 -85
  280. package/src/tools/calls/call-status.ts +35 -51
  281. package/src/tools/claude-code/claude-code.ts +77 -11
  282. package/src/tools/contacts/contact-merge.ts +46 -78
  283. package/src/tools/contacts/contact-search.ts +35 -79
  284. package/src/tools/contacts/contact-upsert.ts +35 -108
  285. package/src/tools/credentials/vault.ts +20 -4
  286. package/src/tools/document/document-tool.ts +71 -144
  287. package/src/tools/executor.ts +129 -10
  288. package/src/tools/followups/followup_create.ts +46 -88
  289. package/src/tools/followups/followup_list.ts +34 -74
  290. package/src/tools/followups/followup_resolve.ts +31 -66
  291. package/src/tools/host-terminal/cli-discover.ts +2 -1
  292. package/src/tools/host-terminal/host-shell.ts +10 -0
  293. package/src/tools/memory/handlers.ts +5 -4
  294. package/src/tools/network/__tests__/web-search.test.ts +427 -0
  295. package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
  296. package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
  297. package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
  298. package/src/tools/network/web-fetch.ts +18 -6
  299. package/src/tools/playbooks/index.ts +4 -5
  300. package/src/tools/playbooks/playbook-create.ts +3 -47
  301. package/src/tools/playbooks/playbook-delete.ts +1 -25
  302. package/src/tools/playbooks/playbook-list.ts +1 -28
  303. package/src/tools/playbooks/playbook-update.ts +3 -51
  304. package/src/tools/reminder/reminder.ts +5 -78
  305. package/src/tools/schedule/create.ts +69 -74
  306. package/src/tools/schedule/delete.ts +21 -47
  307. package/src/tools/schedule/list.ts +55 -74
  308. package/src/tools/schedule/update.ts +77 -84
  309. package/src/tools/subagent/abort.ts +29 -58
  310. package/src/tools/subagent/message.ts +30 -63
  311. package/src/tools/subagent/read.ts +53 -84
  312. package/src/tools/subagent/spawn.ts +43 -82
  313. package/src/tools/subagent/status.ts +42 -71
  314. package/src/tools/swarm/delegate.ts +2 -1
  315. package/src/tools/tasks/index.ts +8 -8
  316. package/src/tools/tasks/task-delete.ts +60 -88
  317. package/src/tools/tasks/task-list.ts +31 -52
  318. package/src/tools/tasks/task-run.ts +72 -108
  319. package/src/tools/tasks/task-save.ts +33 -65
  320. package/src/tools/tasks/work-item-enqueue.ts +183 -215
  321. package/src/tools/tasks/work-item-list.ts +33 -63
  322. package/src/tools/tasks/work-item-remove.ts +45 -97
  323. package/src/tools/tasks/work-item-update.ts +91 -163
  324. package/src/tools/terminal/backends/native.ts +3 -1
  325. package/src/tools/tool-manifest.ts +0 -62
  326. package/src/tools/types.ts +6 -0
  327. package/src/tools/ui-surface/definitions.ts +3 -1
  328. package/src/tools/watch/screen-watch.ts +3 -1
  329. package/src/tools/watcher/create.ts +52 -98
  330. package/src/tools/watcher/delete.ts +20 -46
  331. package/src/tools/watcher/digest.ts +36 -70
  332. package/src/tools/watcher/list.ts +49 -79
  333. package/src/tools/watcher/update.ts +45 -91
  334. package/src/twitter/client.ts +690 -0
  335. package/src/twitter/session.ts +91 -0
  336. package/src/usage/types.ts +0 -1
  337. package/src/util/truncate.ts +6 -0
  338. package/src/watcher/providers/slack.ts +2 -1
  339. package/src/watcher/watcher-store.ts +3 -2
  340. package/src/work-items/work-item-store.ts +27 -2
  341. package/src/workspace/commit-message-enrichment-service.ts +31 -7
  342. package/src/workspace/git-service.ts +87 -22
  343. package/src/workspace/provider-commit-message-generator.ts +242 -0
  344. package/src/workspace/turn-commit.ts +62 -3
  345. package/src/tools/contacts/index.ts +0 -4
  346. package/src/tools/document/index.ts +0 -5
  347. package/src/tools/followups/index.ts +0 -3
  348. package/src/tools/subagent/index.ts +0 -5
  349. /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
@@ -0,0 +1,159 @@
1
+ import { describe, test, expect, afterAll } from 'bun:test';
2
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { runSkillToolScript } from '../tools/skills/skill-script-runner.js';
6
+ import type { ToolContext } from '../tools/types.js';
7
+
8
+ const testDir = mkdtempSync(join(tmpdir(), 'skill-runner-test-'));
9
+
10
+ afterAll(() => {
11
+ try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
12
+ });
13
+
14
+ const ctx: ToolContext = {
15
+ workingDir: '/tmp',
16
+ sessionId: 'test-session',
17
+ conversationId: 'test-conversation',
18
+ };
19
+
20
+ function makeSkillDir(name: string): string {
21
+ const dir = join(testDir, name);
22
+ mkdirSync(dir, { recursive: true });
23
+ return dir;
24
+ }
25
+
26
+ // ── Host execution ──────────────────────────────────────────────────
27
+
28
+ describe('runSkillToolScript (host)', () => {
29
+ test('runs a valid skill script and returns result', async () => {
30
+ const skillDir = makeSkillDir('valid-skill');
31
+ writeFileSync(join(skillDir, 'tool.ts'), `
32
+ export async function run(input, context) {
33
+ return { content: 'Hello from skill! Input: ' + JSON.stringify(input), isError: false };
34
+ }
35
+ `);
36
+
37
+ const result = await runSkillToolScript(skillDir, 'tool.ts', { foo: 'bar' }, ctx);
38
+
39
+ expect(result.isError).toBe(false);
40
+ expect(result.content).toContain('Hello from skill!');
41
+ expect(result.content).toContain('"foo":"bar"');
42
+ });
43
+
44
+ test('blocks path traversal escape', async () => {
45
+ const skillDir = makeSkillDir('escape-skill');
46
+
47
+ const result = await runSkillToolScript(skillDir, '../../../etc/passwd', {}, ctx);
48
+
49
+ expect(result.isError).toBe(true);
50
+ expect(result.content).toContain('escapes the skill directory');
51
+ });
52
+
53
+ test('returns error when script does not export run()', async () => {
54
+ const skillDir = makeSkillDir('no-run-skill');
55
+ writeFileSync(join(skillDir, 'bad.ts'), `
56
+ export const name = 'not a runner';
57
+ `);
58
+
59
+ const result = await runSkillToolScript(skillDir, 'bad.ts', {}, ctx);
60
+
61
+ expect(result.isError).toBe(true);
62
+ expect(result.content).toContain('does not export a "run" function');
63
+ });
64
+
65
+ test('returns error when script throws', async () => {
66
+ const skillDir = makeSkillDir('throw-skill');
67
+ writeFileSync(join(skillDir, 'throw.ts'), `
68
+ export async function run() {
69
+ throw new Error('Intentional test error');
70
+ }
71
+ `);
72
+
73
+ const result = await runSkillToolScript(skillDir, 'throw.ts', {}, ctx);
74
+
75
+ expect(result.isError).toBe(true);
76
+ expect(result.content).toContain('Intentional test error');
77
+ });
78
+
79
+ test('returns error when script file does not exist', async () => {
80
+ const skillDir = makeSkillDir('missing-skill');
81
+
82
+ const result = await runSkillToolScript(skillDir, 'nonexistent.ts', {}, ctx);
83
+
84
+ expect(result.isError).toBe(true);
85
+ expect(result.content).toContain('Failed to load skill tool script');
86
+ });
87
+ });
88
+
89
+ // ── Version hash checking ───────────────────────────────────────────
90
+
91
+ describe('runSkillToolScript version hash', () => {
92
+ test('blocks execution when hash mismatches', async () => {
93
+ const skillDir = makeSkillDir('hash-mismatch');
94
+ writeFileSync(join(skillDir, 'tool.ts'), `
95
+ export async function run() {
96
+ return { content: 'should not run', isError: false };
97
+ }
98
+ `);
99
+
100
+ const result = await runSkillToolScript(skillDir, 'tool.ts', {}, ctx, {
101
+ expectedSkillVersionHash: 'expected-hash-123',
102
+ skillDirHashResolver: () => 'different-hash-456',
103
+ });
104
+
105
+ expect(result.isError).toBe(true);
106
+ expect(result.content).toContain('Skill version mismatch');
107
+ expect(result.content).toContain('expected-hash-123');
108
+ expect(result.content).toContain('different-hash-456');
109
+ });
110
+
111
+ test('allows execution when hash matches', async () => {
112
+ const skillDir = makeSkillDir('hash-match');
113
+ writeFileSync(join(skillDir, 'tool.ts'), `
114
+ export async function run() {
115
+ return { content: 'hash matched', isError: false };
116
+ }
117
+ `);
118
+
119
+ const result = await runSkillToolScript(skillDir, 'tool.ts', {}, ctx, {
120
+ expectedSkillVersionHash: 'matching-hash',
121
+ skillDirHashResolver: () => 'matching-hash',
122
+ });
123
+
124
+ expect(result.isError).toBe(false);
125
+ expect(result.content).toBe('hash matched');
126
+ });
127
+
128
+ test('returns error when hash resolver throws', async () => {
129
+ const skillDir = makeSkillDir('hash-error');
130
+ writeFileSync(join(skillDir, 'tool.ts'), `
131
+ export async function run() {
132
+ return { content: 'should not run', isError: false };
133
+ }
134
+ `);
135
+
136
+ const result = await runSkillToolScript(skillDir, 'tool.ts', {}, ctx, {
137
+ expectedSkillVersionHash: 'some-hash',
138
+ skillDirHashResolver: () => { throw new Error('hash computation failed'); },
139
+ });
140
+
141
+ expect(result.isError).toBe(true);
142
+ expect(result.content).toContain('Failed to compute skill version hash');
143
+ expect(result.content).toContain('hash computation failed');
144
+ });
145
+
146
+ test('skips hash check when no expected hash provided', async () => {
147
+ const skillDir = makeSkillDir('no-hash');
148
+ writeFileSync(join(skillDir, 'tool.ts'), `
149
+ export async function run() {
150
+ return { content: 'no hash check', isError: false };
151
+ }
152
+ `);
153
+
154
+ const result = await runSkillToolScript(skillDir, 'tool.ts', {}, ctx);
155
+
156
+ expect(result.isError).toBe(false);
157
+ expect(result.content).toBe('no hash check');
158
+ });
159
+ });
@@ -0,0 +1,52 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { extractPromptSpeakerMetadata, SpeakerIdentityTracker } from '../calls/speaker-identification.js';
3
+
4
+ describe('speaker-identification', () => {
5
+ test('extractPromptSpeakerMetadata: reads top-level and nested fields', () => {
6
+ const metadata = extractPromptSpeakerMetadata({
7
+ speaker_id: 'spk-7',
8
+ speaker_label: 'Conference room mic',
9
+ speaker_confidence: '0.88',
10
+ metadata: {
11
+ participantId: 'participant-22',
12
+ },
13
+ });
14
+
15
+ expect(metadata.speakerId).toBe('spk-7');
16
+ expect(metadata.speakerLabel).toBe('Conference room mic');
17
+ expect(metadata.speakerConfidence).toBe(0.88);
18
+ expect(metadata.participantId).toBe('participant-22');
19
+ });
20
+
21
+ test('SpeakerIdentityTracker: keeps stable identity for provider speaker id', () => {
22
+ const tracker = new SpeakerIdentityTracker();
23
+
24
+ const first = tracker.identifySpeaker({
25
+ speakerId: 'speaker-a',
26
+ speakerName: 'Aaron',
27
+ speakerConfidence: 0.93,
28
+ });
29
+ const second = tracker.identifySpeaker({
30
+ speakerId: 'speaker-a',
31
+ speakerName: 'Aaron',
32
+ speakerConfidence: 0.81,
33
+ });
34
+
35
+ expect(first.speakerId).toBe('speaker-a');
36
+ expect(first.speakerLabel).toBe('Aaron');
37
+ expect(first.source).toBe('provider');
38
+ expect(second.speakerId).toBe('speaker-a');
39
+ expect(second.speakerLabel).toBe('Aaron');
40
+ expect(second.speakerConfidence).toBe(0.81);
41
+ expect(tracker.listProfiles()).toHaveLength(1);
42
+ });
43
+
44
+ test('SpeakerIdentityTracker: falls back to inferred primary speaker without provider ids', () => {
45
+ const tracker = new SpeakerIdentityTracker();
46
+ const speaker = tracker.identifySpeaker({});
47
+
48
+ expect(speaker.speakerId).toBe('primary-speaker');
49
+ expect(speaker.speakerLabel).toBe('Speaker 1');
50
+ expect(speaker.source).toBe('inferred');
51
+ });
52
+ });
@@ -9,13 +9,14 @@ interface FakeManagedSubagent {
9
9
  abort: () => void;
10
10
  dispose: () => void;
11
11
  messages: Array<{ role: string; content: Array<{ type: string; text: string }> }>;
12
- sendToClient: () => void;
12
+ sendToClient: (msg: ServerMessage) => void;
13
13
  loadFromDb?: () => Promise<void>;
14
14
  persistUserMessage?: (msg: string) => string;
15
15
  runAgentLoop?: () => Promise<void>;
16
+ usageStats: { inputTokens: number; outputTokens: number; estimatedCost: number };
16
17
  };
17
18
  state: SubagentState;
18
- parentSendToClient: () => void;
19
+ parentSendToClient: (msg: ServerMessage) => void;
19
20
  }
20
21
 
21
22
  /** Type-safe accessor for SubagentManager's private internals via bracket notation. */
@@ -37,19 +38,21 @@ function injectFakeSubagent(
37
38
  manager: SubagentManager,
38
39
  subagentId: string,
39
40
  state: SubagentState,
41
+ parentSendToClient?: (msg: ServerMessage) => void,
40
42
  ): void {
41
43
  const fakeSession: FakeManagedSubagent['session'] = {
42
44
  abort: () => {},
43
45
  dispose: () => {},
44
46
  messages: [],
45
47
  sendToClient: () => {},
48
+ usageStats: { inputTokens: 100, outputTokens: 50, estimatedCost: 0.005 },
46
49
  };
47
50
 
48
51
  const internals = asInternals(manager);
49
52
  const subagents = internals.subagents;
50
53
  const parentToChildren = internals.parentToChildren;
51
54
 
52
- subagents.set(subagentId, { session: fakeSession, state, parentSendToClient: () => {} });
55
+ subagents.set(subagentId, { session: fakeSession, state, parentSendToClient: parentSendToClient ?? (() => {}) });
53
56
 
54
57
  const parentId = state.config.parentSessionId;
55
58
  if (!parentToChildren.has(parentId)) {
@@ -78,7 +81,7 @@ function makeState(
78
81
  }
79
82
 
80
83
  describe('SubagentManager abort notification', () => {
81
- test('abort notifies parent with abort message', () => {
84
+ test('abort notifies parent with do-not-respawn message', () => {
82
85
  const manager = new SubagentManager();
83
86
  const subagentId = 'sub-1';
84
87
  const state = makeState(subagentId);
@@ -97,18 +100,45 @@ describe('SubagentManager abort notification', () => {
97
100
  expect(result).toBe(true);
98
101
  expect(state.status).toBe('aborted');
99
102
  expect(notifications).toHaveLength(1);
100
- expect(notifications[0].parentSessionId).toBe('parent-sess-1');
101
- expect(notifications[0].message).toContain('[Subagent "Test subagent" was aborted]');
103
+ expect(notifications[0].message).toContain('explicitly aborted');
104
+ expect(notifications[0].message).toContain('Do NOT re-spawn');
105
+ });
106
+
107
+ test('abort notification routes to parent sender, not aborting sender', () => {
108
+ const manager = new SubagentManager();
109
+ const subagentId = 'sub-1';
110
+ const state = makeState(subagentId);
111
+
112
+ // Track which sender onSubagentFinished receives.
113
+ let notificationSender: unknown = null;
114
+ manager.onSubagentFinished = (_pid, _message, sender) => {
115
+ notificationSender = sender;
116
+ };
117
+
118
+ // The parent's stored sender (set at spawn time).
119
+ const parentSender = () => {};
120
+ injectFakeSubagent(manager, subagentId, state, parentSender);
121
+
122
+ // A different sender (simulating abort from a different thread's socket).
123
+ const abortingSender = ((_msg: ServerMessage) => {}) as (msg: ServerMessage) => void;
124
+
125
+ manager.abort(subagentId, abortingSender);
126
+
127
+ // onSubagentFinished should receive the parent's sender, not the aborting one.
128
+ expect(notificationSender).toBe(parentSender);
129
+ expect(notificationSender).not.toBe(abortingSender);
102
130
  });
103
131
 
104
132
  test('abort sends subagent_status_changed to client', () => {
105
133
  const manager = new SubagentManager();
106
134
  const subagentId = 'sub-1';
107
- injectFakeSubagent(manager, subagentId, makeState(subagentId));
108
135
 
109
136
  const clientMessages: ServerMessage[] = [];
110
137
  const sendToClient = (msg: ServerMessage) => clientMessages.push(msg);
111
138
 
139
+ // Pass the sender as parentSendToClient so the stored sender receives the status update.
140
+ injectFakeSubagent(manager, subagentId, makeState(subagentId), sendToClient);
141
+
112
142
  manager.abort(subagentId, sendToClient);
113
143
 
114
144
  const statusMsg = clientMessages.find((m) => m.type === 'subagent_status_changed');
@@ -222,13 +252,14 @@ describe('SubagentManager notifyParent (via runSubagent)', () => {
222
252
  await asInternals(manager).runSubagent(subagentId, 'Do something');
223
253
 
224
254
  expect(state.status).toBe('completed');
255
+ expect(state.usage).toEqual({ inputTokens: 100, outputTokens: 50, estimatedCost: 0.005 });
225
256
  expect(notifications).toHaveLength(1);
226
257
  expect(notifications[0].parentSessionId).toBe('parent-sess-1');
227
258
  expect(notifications[0].message).toContain('[Subagent "Test subagent" completed]');
228
259
  expect(notifications[0].message).toContain('subagent_read');
229
260
  });
230
261
 
231
- test('failed subagent notifies parent with error', async () => {
262
+ test('failed subagent notifies parent with error and asks user before retry', async () => {
232
263
  const manager = new SubagentManager();
233
264
  const subagentId = 'sub-1';
234
265
  const state = makeState(subagentId);
@@ -251,10 +282,11 @@ describe('SubagentManager notifyParent (via runSubagent)', () => {
251
282
 
252
283
  expect(state.status).toBe('failed');
253
284
  expect(state.error).toBe('API rate limit exceeded');
285
+ expect(state.usage).toEqual({ inputTokens: 100, outputTokens: 50, estimatedCost: 0.005 });
254
286
  expect(notifications).toHaveLength(1);
255
- expect(notifications[0].parentSessionId).toBe('parent-sess-1');
256
- expect(notifications[0].message).toContain('[Subagent "Test subagent" failed]');
287
+ expect(notifications[0].message).toContain('failed');
257
288
  expect(notifications[0].message).toContain('API rate limit exceeded');
289
+ expect(notifications[0].message).toContain('Do NOT re-spawn');
258
290
  });
259
291
 
260
292
  test('failed subagent does not notify if already aborted', async () => {
@@ -1,51 +1,49 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
- import { subagentSpawnTool } from '../tools/subagent/spawn.js';
3
- import { subagentStatusTool } from '../tools/subagent/status.js';
4
- import { subagentAbortTool } from '../tools/subagent/abort.js';
5
- import { subagentMessageTool } from '../tools/subagent/message.js';
6
- import { subagentReadTool } from '../tools/subagent/read.js';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { executeSubagentSpawn } from '../tools/subagent/spawn.js';
5
+ import { executeSubagentStatus } from '../tools/subagent/status.js';
6
+ import { executeSubagentAbort } from '../tools/subagent/abort.js';
7
+ import { executeSubagentMessage } from '../tools/subagent/message.js';
8
+ import { executeSubagentRead } from '../tools/subagent/read.js';
9
+ import { SubagentManager } from '../subagent/manager.js';
10
+ import type { SubagentState } from '../subagent/types.js';
7
11
 
8
- describe('Subagent tool shapes', () => {
9
- test('spawn tool has correct shape', () => {
10
- expect(subagentSpawnTool.name).toBe('subagent_spawn');
11
- expect(subagentSpawnTool.category).toBe('orchestration');
12
- expect(typeof subagentSpawnTool.execute).toBe('function');
13
- expect(typeof subagentSpawnTool.getDefinition).toBe('function');
14
- const def = subagentSpawnTool.getDefinition!();
15
- expect((def.input_schema as Record<string, unknown>).required).toEqual(['label', 'objective']);
16
- });
12
+ // Load tool definitions from the bundled skill TOOLS.json
13
+ const toolsJson = JSON.parse(
14
+ readFileSync(join(import.meta.dirname, '../config/bundled-skills/subagent/TOOLS.json'), 'utf-8'),
15
+ );
16
+ const findTool = (name: string) => toolsJson.tools.find((t: { name: string }) => t.name === name);
17
17
 
18
- test('status tool has correct shape', () => {
19
- expect(subagentStatusTool.name).toBe('subagent_status');
20
- expect(subagentStatusTool.category).toBe('orchestration');
21
- expect(typeof subagentStatusTool.execute).toBe('function');
18
+ describe('Subagent tool definitions', () => {
19
+ test('spawn tool has correct definition', () => {
20
+ const def = findTool('subagent_spawn');
21
+ expect(def).toBeDefined();
22
+ expect(def.input_schema.required).toEqual(['label', 'objective']);
22
23
  });
23
24
 
24
- test('abort tool has correct shape', () => {
25
- expect(subagentAbortTool.name).toBe('subagent_abort');
26
- expect(typeof subagentAbortTool.execute).toBe('function');
27
- const def = subagentAbortTool.getDefinition!();
28
- expect((def.input_schema as Record<string, unknown>).required).toEqual(['subagent_id']);
25
+ test('abort tool has correct definition', () => {
26
+ const def = findTool('subagent_abort');
27
+ expect(def).toBeDefined();
28
+ expect(def.input_schema.required).toEqual(['subagent_id']);
29
29
  });
30
30
 
31
- test('message tool has correct shape', () => {
32
- expect(subagentMessageTool.name).toBe('subagent_message');
33
- expect(typeof subagentMessageTool.execute).toBe('function');
34
- const def = subagentMessageTool.getDefinition!();
35
- expect((def.input_schema as Record<string, unknown>).required).toEqual(['subagent_id', 'content']);
31
+ test('message tool has correct definition', () => {
32
+ const def = findTool('subagent_message');
33
+ expect(def).toBeDefined();
34
+ expect(def.input_schema.required).toEqual(['subagent_id', 'content']);
36
35
  });
37
36
 
38
- test('read tool has correct shape', () => {
39
- expect(subagentReadTool.name).toBe('subagent_read');
40
- expect(typeof subagentReadTool.execute).toBe('function');
41
- const def = subagentReadTool.getDefinition!();
42
- expect((def.input_schema as Record<string, unknown>).required).toEqual(['subagent_id']);
37
+ test('read tool has correct definition', () => {
38
+ const def = findTool('subagent_read');
39
+ expect(def).toBeDefined();
40
+ expect(def.input_schema.required).toEqual(['subagent_id']);
43
41
  });
44
42
  });
45
43
 
46
44
  describe('Subagent tool execute validation', () => {
47
45
  test('spawn returns error when no sendToClient', async () => {
48
- const result = await subagentSpawnTool.execute(
46
+ const result = await executeSubagentSpawn(
49
47
  { label: 'test', objective: 'do something' },
50
48
  { workingDir: '/tmp', sessionId: 'sess-1', conversationId: 'conv-1' },
51
49
  );
@@ -54,7 +52,7 @@ describe('Subagent tool execute validation', () => {
54
52
  });
55
53
 
56
54
  test('spawn returns error when missing label', async () => {
57
- const result = await subagentSpawnTool.execute(
55
+ const result = await executeSubagentSpawn(
58
56
  { objective: 'do something' },
59
57
  { workingDir: '/tmp', sessionId: 'sess-1', conversationId: 'conv-1', sendToClient: () => {} },
60
58
  );
@@ -63,7 +61,7 @@ describe('Subagent tool execute validation', () => {
63
61
  });
64
62
 
65
63
  test('status returns empty when no subagents', async () => {
66
- const result = await subagentStatusTool.execute(
64
+ const result = await executeSubagentStatus(
67
65
  {},
68
66
  { workingDir: '/tmp', sessionId: 'nonexistent-session', conversationId: 'conv-1' },
69
67
  );
@@ -72,7 +70,7 @@ describe('Subagent tool execute validation', () => {
72
70
  });
73
71
 
74
72
  test('status returns error for unknown subagent_id', async () => {
75
- const result = await subagentStatusTool.execute(
73
+ const result = await executeSubagentStatus(
76
74
  { subagent_id: 'nonexistent-id' },
77
75
  { workingDir: '/tmp', sessionId: 'sess-1', conversationId: 'conv-1' },
78
76
  );
@@ -81,7 +79,7 @@ describe('Subagent tool execute validation', () => {
81
79
  });
82
80
 
83
81
  test('abort returns error for unknown subagent_id', async () => {
84
- const result = await subagentAbortTool.execute(
82
+ const result = await executeSubagentAbort(
85
83
  { subagent_id: 'nonexistent-id' },
86
84
  { workingDir: '/tmp', sessionId: 'sess-1', conversationId: 'conv-1' },
87
85
  );
@@ -90,7 +88,7 @@ describe('Subagent tool execute validation', () => {
90
88
  });
91
89
 
92
90
  test('abort returns error when missing subagent_id', async () => {
93
- const result = await subagentAbortTool.execute(
91
+ const result = await executeSubagentAbort(
94
92
  {},
95
93
  { workingDir: '/tmp', sessionId: 'sess-1', conversationId: 'conv-1' },
96
94
  );
@@ -99,7 +97,7 @@ describe('Subagent tool execute validation', () => {
99
97
  });
100
98
 
101
99
  test('message returns error for unknown subagent_id', async () => {
102
- const result = await subagentMessageTool.execute(
100
+ const result = await executeSubagentMessage(
103
101
  { subagent_id: 'nonexistent-id', content: 'hello' },
104
102
  { workingDir: '/tmp', sessionId: 'sess-1', conversationId: 'conv-1' },
105
103
  );
@@ -108,7 +106,7 @@ describe('Subagent tool execute validation', () => {
108
106
  });
109
107
 
110
108
  test('message returns error when missing required fields', async () => {
111
- const result = await subagentMessageTool.execute(
109
+ const result = await executeSubagentMessage(
112
110
  { subagent_id: 'some-id' },
113
111
  { workingDir: '/tmp', sessionId: 'sess-1', conversationId: 'conv-1' },
114
112
  );
@@ -116,3 +114,105 @@ describe('Subagent tool execute validation', () => {
116
114
  expect(result.content).toContain('required');
117
115
  });
118
116
  });
117
+
118
+ // ── Ownership validation tests ──────────────────────────────────────
119
+
120
+ /**
121
+ * Inject a fake subagent into the singleton manager so tool executors
122
+ * can find it. Uses the same private-internals trick as the notify tests.
123
+ */
124
+ function injectSubagent(
125
+ manager: SubagentManager,
126
+ subagentId: string,
127
+ parentSessionId: string,
128
+ status: SubagentState['status'] = 'running',
129
+ ): void {
130
+ const internals = manager as unknown as {
131
+ subagents: Map<string, { session: unknown; state: SubagentState; parentSendToClient: () => void }>;
132
+ parentToChildren: Map<string, Set<string>>;
133
+ };
134
+ const state: SubagentState = {
135
+ config: { id: subagentId, parentSessionId, label: 'Test', objective: 'test' },
136
+ status,
137
+ conversationId: `conv-${subagentId}`,
138
+ createdAt: Date.now(),
139
+ usage: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
140
+ };
141
+ const fakeSession = {
142
+ abort: () => {},
143
+ dispose: () => {},
144
+ messages: [],
145
+ sendToClient: () => {},
146
+ usageStats: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
147
+ };
148
+ internals.subagents.set(subagentId, { session: fakeSession, state, parentSendToClient: () => {} });
149
+ if (!internals.parentToChildren.has(parentSessionId)) {
150
+ internals.parentToChildren.set(parentSessionId, new Set());
151
+ }
152
+ internals.parentToChildren.get(parentSessionId)!.add(subagentId);
153
+ }
154
+
155
+ import { getSubagentManager } from '../subagent/index.js';
156
+
157
+ describe('Subagent tool ownership validation', () => {
158
+ const ownerSession = 'owner-sess';
159
+ const otherSession = 'other-sess';
160
+ const subagentId = 'owned-sub-1';
161
+
162
+ // Inject once — all tests share this subagent.
163
+ const manager = getSubagentManager();
164
+ injectSubagent(manager, subagentId, ownerSession);
165
+
166
+ test('status rejects non-owner session', async () => {
167
+ const result = await executeSubagentStatus(
168
+ { subagent_id: subagentId },
169
+ { workingDir: '/tmp', sessionId: otherSession, conversationId: 'conv-1' },
170
+ );
171
+ expect(result.isError).toBe(true);
172
+ expect(result.content).toContain('No subagent found');
173
+ });
174
+
175
+ test('status succeeds for owner session', async () => {
176
+ const result = await executeSubagentStatus(
177
+ { subagent_id: subagentId },
178
+ { workingDir: '/tmp', sessionId: ownerSession, conversationId: 'conv-1' },
179
+ );
180
+ expect(result.isError).toBe(false);
181
+ });
182
+
183
+ test('message rejects non-owner session', async () => {
184
+ const result = await executeSubagentMessage(
185
+ { subagent_id: subagentId, content: 'hello' },
186
+ { workingDir: '/tmp', sessionId: otherSession, conversationId: 'conv-1' },
187
+ );
188
+ expect(result.isError).toBe(true);
189
+ expect(result.content).toContain('Could not send');
190
+ });
191
+
192
+ test('read rejects non-owner session', async () => {
193
+ const result = await executeSubagentRead(
194
+ { subagent_id: subagentId },
195
+ { workingDir: '/tmp', sessionId: otherSession, conversationId: 'conv-1' },
196
+ );
197
+ expect(result.isError).toBe(true);
198
+ expect(result.content).toContain('No subagent found');
199
+ });
200
+
201
+ test('abort rejects non-owner session', async () => {
202
+ const result = await executeSubagentAbort(
203
+ { subagent_id: subagentId },
204
+ { workingDir: '/tmp', sessionId: otherSession, conversationId: 'conv-1' },
205
+ );
206
+ expect(result.isError).toBe(true);
207
+ expect(result.content).toContain('Could not abort');
208
+ });
209
+
210
+ test('abort succeeds for owner session', async () => {
211
+ const result = await executeSubagentAbort(
212
+ { subagent_id: subagentId },
213
+ { workingDir: '/tmp', sessionId: ownerSession, conversationId: 'conv-1' },
214
+ );
215
+ // Abort succeeds (subagent was running)
216
+ expect(result.isError).toBe(false);
217
+ });
218
+ });
@@ -35,7 +35,7 @@ mock.module('./indexer.js', () => ({
35
35
  }));
36
36
 
37
37
  import type { Database } from 'bun:sqlite';
38
- import { initializeDb, getDb } from '../memory/db.js';
38
+ import { initializeDb, getDb, resetDb } from '../memory/db.js';
39
39
  import { renderTemplate } from '../tasks/task-runner.js';
40
40
  import { compileTaskFromConversation, saveCompiledTask } from '../tasks/task-compiler.js';
41
41
  import { getTask } from '../tasks/task-store.js';
@@ -43,6 +43,7 @@ import { getTask } from '../tasks/task-store.js';
43
43
  initializeDb();
44
44
 
45
45
  afterAll(() => {
46
+ resetDb();
46
47
  try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
47
48
  });
48
49
 
@@ -25,7 +25,7 @@ mock.module('../util/logger.js', () => ({
25
25
  }),
26
26
  }));
27
27
 
28
- import { initializeDb, getDb } from '../memory/db.js';
28
+ import { initializeDb, getDb, resetDb } from '../memory/db.js';
29
29
  import { createTask } from '../tasks/task-store.js';
30
30
  import { getTaskRunRules } from '../tasks/ephemeral-permissions.js';
31
31
  import { renderTemplate, runTask } from '../tasks/task-runner.js';
@@ -33,6 +33,7 @@ import { renderTemplate, runTask } from '../tasks/task-runner.js';
33
33
  initializeDb();
34
34
 
35
35
  afterAll(() => {
36
+ resetDb();
36
37
  try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
37
38
  });
38
39
 
@@ -25,7 +25,7 @@ mock.module('../util/logger.js', () => ({
25
25
  }),
26
26
  }));
27
27
 
28
- import { initializeDb, getDb } from '../memory/db.js';
28
+ import { initializeDb, getDb, resetDb } from '../memory/db.js';
29
29
  import { createTask } from '../tasks/task-store.js';
30
30
  import { scheduleTask } from '../tasks/task-scheduler.js';
31
31
  import {
@@ -48,6 +48,7 @@ function forceScheduleDue(scheduleId: string): void {
48
48
  }
49
49
 
50
50
  afterAll(() => {
51
+ resetDb();
51
52
  try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
52
53
  });
53
54