@vellumai/assistant 0.5.6 → 0.5.7

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 (305) hide show
  1. package/.env.example +16 -2
  2. package/ARCHITECTURE.md +6 -75
  3. package/Dockerfile +1 -1
  4. package/README.md +0 -2
  5. package/bun.lock +0 -414
  6. package/docs/architecture/keychain-broker.md +45 -240
  7. package/docs/architecture/security.md +0 -17
  8. package/docs/credential-execution-service.md +2 -2
  9. package/node_modules/@vellumai/ces-contracts/package.json +1 -0
  10. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +119 -0
  11. package/node_modules/@vellumai/credential-storage/package.json +1 -0
  12. package/node_modules/@vellumai/egress-proxy/package.json +1 -0
  13. package/package.json +2 -3
  14. package/src/__tests__/actor-token-service.test.ts +0 -114
  15. package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
  16. package/src/__tests__/browser-skill-endstate.test.ts +6 -5
  17. package/src/__tests__/btw-routes.test.ts +0 -39
  18. package/src/__tests__/call-domain.test.ts +0 -128
  19. package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +0 -5
  21. package/src/__tests__/channel-readiness-service.test.ts +1 -60
  22. package/src/__tests__/checker.test.ts +4 -2
  23. package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
  24. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  25. package/src/__tests__/config-schema.test.ts +1 -1
  26. package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
  27. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  28. package/src/__tests__/conversation-skill-tools.test.ts +0 -54
  29. package/src/__tests__/conversation-title-service.test.ts +87 -0
  30. package/src/__tests__/credential-execution-feature-gates.test.ts +28 -14
  31. package/src/__tests__/credential-execution-managed-contract.test.ts +33 -18
  32. package/src/__tests__/credential-security-e2e.test.ts +0 -66
  33. package/src/__tests__/credential-security-invariants.test.ts +4 -45
  34. package/src/__tests__/credentials-cli.test.ts +78 -0
  35. package/src/__tests__/db-migration-rollback.test.ts +2015 -1
  36. package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
  37. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
  38. package/src/__tests__/guardian-routing-state.test.ts +0 -5
  39. package/src/__tests__/host-shell-tool.test.ts +6 -7
  40. package/src/__tests__/http-user-message-parity.test.ts +3 -103
  41. package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
  42. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
  43. package/src/__tests__/intent-routing.test.ts +0 -13
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
  45. package/src/__tests__/keychain-broker-client.test.ts +161 -22
  46. package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
  47. package/src/__tests__/migration-export-http.test.ts +2 -2
  48. package/src/__tests__/migration-import-commit-http.test.ts +2 -2
  49. package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
  50. package/src/__tests__/migration-validate-http.test.ts +2 -2
  51. package/src/__tests__/non-member-access-request.test.ts +0 -5
  52. package/src/__tests__/notification-decision-fallback.test.ts +4 -0
  53. package/src/__tests__/notification-decision-identity.test.ts +4 -0
  54. package/src/__tests__/permission-types.test.ts +1 -0
  55. package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
  56. package/src/__tests__/qdrant-manager.test.ts +28 -2
  57. package/src/__tests__/registry.test.ts +0 -6
  58. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
  59. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
  60. package/src/__tests__/secure-keys.test.ts +83 -263
  61. package/src/__tests__/shell-identity.test.ts +96 -6
  62. package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
  63. package/src/__tests__/skill-feature-flags.test.ts +46 -45
  64. package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
  65. package/src/__tests__/skill-load-inline-command.test.ts +8 -12
  66. package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
  67. package/src/__tests__/skill-load-tool.test.ts +0 -2
  68. package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
  69. package/src/__tests__/skills.test.ts +0 -2
  70. package/src/__tests__/slack-inbound-verification.test.ts +0 -4
  71. package/src/__tests__/suggestion-routes.test.ts +1 -32
  72. package/src/__tests__/system-prompt.test.ts +0 -1
  73. package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
  74. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
  75. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
  76. package/src/__tests__/update-bulletin.test.ts +0 -2
  77. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
  78. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -6
  79. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
  80. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +218 -0
  81. package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
  82. package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
  83. package/src/calls/audio-store.test.ts +97 -0
  84. package/src/calls/audio-store.ts +205 -0
  85. package/src/calls/call-controller.ts +85 -7
  86. package/src/calls/call-domain.ts +3 -0
  87. package/src/calls/call-store.ts +10 -3
  88. package/src/calls/fish-audio-client.ts +117 -0
  89. package/src/calls/relay-server.ts +27 -0
  90. package/src/calls/twilio-routes.ts +2 -1
  91. package/src/calls/types.ts +1 -0
  92. package/src/calls/voice-ingress-preflight.ts +0 -42
  93. package/src/calls/voice-quality.ts +26 -5
  94. package/src/calls/voice-session-bridge.ts +6 -12
  95. package/src/cli/commands/config.ts +1 -4
  96. package/src/cli/commands/credentials.ts +34 -4
  97. package/src/cli/commands/oauth/index.ts +7 -0
  98. package/src/cli/commands/oauth/platform.ts +179 -0
  99. package/src/cli/commands/platform.ts +3 -3
  100. package/src/config/assistant-feature-flags.ts +186 -5
  101. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  102. package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
  103. package/src/config/bundled-skills/settings/TOOLS.json +2 -2
  104. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
  105. package/src/config/bundled-tool-registry.ts +1 -11
  106. package/src/config/env-registry.ts +1 -1
  107. package/src/config/env.ts +8 -14
  108. package/src/config/feature-flag-registry.json +48 -8
  109. package/src/config/loader.ts +98 -31
  110. package/src/config/schema.ts +4 -13
  111. package/src/config/schemas/calls.ts +13 -0
  112. package/src/config/schemas/fish-audio.ts +39 -0
  113. package/src/config/schemas/security.ts +0 -4
  114. package/src/config/types.ts +0 -1
  115. package/src/contacts/contact-store.ts +39 -0
  116. package/src/contacts/types.ts +2 -0
  117. package/src/credential-execution/approval-bridge.ts +1 -0
  118. package/src/credential-execution/executable-discovery.ts +28 -4
  119. package/src/credential-execution/feature-gates.ts +16 -0
  120. package/src/credential-execution/process-manager.ts +38 -0
  121. package/src/daemon/assistant-attachments.ts +9 -0
  122. package/src/daemon/config-watcher.ts +5 -0
  123. package/src/daemon/conversation-tool-setup.ts +0 -105
  124. package/src/daemon/conversation.ts +10 -1
  125. package/src/daemon/handlers/config-vercel.ts +92 -0
  126. package/src/daemon/handlers/skills.ts +2 -15
  127. package/src/daemon/install-symlink.ts +195 -0
  128. package/src/daemon/lifecycle.ts +227 -51
  129. package/src/daemon/message-types/conversations.ts +3 -4
  130. package/src/daemon/message-types/diagnostics.ts +3 -22
  131. package/src/daemon/message-types/messages.ts +0 -2
  132. package/src/daemon/message-types/upgrades.ts +8 -0
  133. package/src/daemon/server.ts +30 -92
  134. package/src/events/domain-events.ts +2 -1
  135. package/src/inbound/platform-callback-registration.ts +3 -3
  136. package/src/instrument.ts +8 -5
  137. package/src/memory/conversation-title-service.ts +50 -1
  138. package/src/memory/db-init.ts +12 -0
  139. package/src/memory/items-extractor.ts +15 -1
  140. package/src/memory/job-handlers/conversation-starters.ts +4 -1
  141. package/src/memory/jobs-store.ts +30 -5
  142. package/src/memory/jobs-worker.ts +31 -7
  143. package/src/memory/migrations/001-job-deferrals.ts +19 -0
  144. package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
  145. package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
  146. package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
  147. package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
  148. package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
  149. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
  150. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
  151. package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
  152. package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
  153. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
  154. package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
  155. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
  156. package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
  157. package/src/memory/migrations/116-messages-fts.ts +106 -1
  158. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
  159. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
  160. package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
  161. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
  162. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
  163. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
  164. package/src/memory/migrations/141-rename-verification-table.ts +54 -0
  165. package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
  166. package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
  167. package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
  168. package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
  169. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
  170. package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
  171. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
  172. package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
  173. package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
  174. package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
  175. package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
  176. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
  177. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
  178. package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
  179. package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
  180. package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
  181. package/src/memory/migrations/index.ts +4 -0
  182. package/src/memory/migrations/registry.ts +90 -0
  183. package/src/memory/migrations/validate-migration-state.ts +137 -11
  184. package/src/memory/qdrant-circuit-breaker.ts +9 -0
  185. package/src/memory/qdrant-manager.ts +64 -7
  186. package/src/memory/schema/calls.ts +1 -0
  187. package/src/memory/schema/contacts.ts +1 -0
  188. package/src/notifications/decision-engine.ts +4 -1
  189. package/src/oauth/connection-resolver.ts +6 -4
  190. package/src/permissions/checker.ts +0 -38
  191. package/src/permissions/shell-identity.ts +76 -22
  192. package/src/permissions/types.ts +4 -2
  193. package/src/platform/client.ts +35 -7
  194. package/src/prompts/persona-resolver.ts +138 -0
  195. package/src/prompts/system-prompt.ts +36 -4
  196. package/src/prompts/templates/users/default.md +1 -0
  197. package/src/providers/registry.ts +27 -40
  198. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  199. package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
  200. package/src/runtime/auth/external-assistant-id.ts +13 -59
  201. package/src/runtime/auth/route-policy.ts +15 -1
  202. package/src/runtime/auth/token-service.ts +43 -138
  203. package/src/runtime/channel-readiness-service.ts +1 -16
  204. package/src/runtime/http-server.ts +27 -2
  205. package/src/runtime/middleware/error-handler.ts +1 -9
  206. package/src/runtime/routes/audio-routes.ts +40 -0
  207. package/src/runtime/routes/btw-routes.ts +0 -17
  208. package/src/runtime/routes/conversation-query-routes.ts +63 -1
  209. package/src/runtime/routes/conversation-routes.ts +4 -44
  210. package/src/runtime/routes/diagnostics-routes.ts +1 -477
  211. package/src/runtime/routes/identity-routes.ts +18 -29
  212. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
  213. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
  214. package/src/runtime/routes/integrations/vercel.ts +89 -0
  215. package/src/runtime/routes/log-export-routes.ts +5 -0
  216. package/src/runtime/routes/memory-item-routes.ts +24 -6
  217. package/src/runtime/routes/migration-rollback-routes.ts +209 -0
  218. package/src/runtime/routes/migration-routes.ts +17 -1
  219. package/src/runtime/routes/notification-routes.ts +58 -0
  220. package/src/runtime/routes/schedule-routes.ts +65 -0
  221. package/src/runtime/routes/settings-routes.ts +41 -1
  222. package/src/runtime/routes/tts-routes.ts +86 -0
  223. package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
  224. package/src/runtime/routes/workspace-commit-routes.ts +62 -0
  225. package/src/runtime/routes/workspace-routes.test.ts +22 -1
  226. package/src/runtime/routes/workspace-routes.ts +1 -1
  227. package/src/runtime/routes/workspace-utils.ts +86 -2
  228. package/src/security/ces-credential-client.ts +59 -22
  229. package/src/security/ces-rpc-credential-backend.ts +85 -0
  230. package/src/security/credential-backend.ts +12 -88
  231. package/src/security/keychain-broker-client.ts +10 -2
  232. package/src/security/secure-keys.ts +94 -113
  233. package/src/skills/catalog-install.ts +13 -7
  234. package/src/telemetry/usage-telemetry-reporter.ts +4 -2
  235. package/src/tools/calls/call-start.ts +1 -0
  236. package/src/tools/executor.ts +0 -4
  237. package/src/tools/network/script-proxy/session-manager.ts +19 -4
  238. package/src/tools/network/web-fetch.ts +3 -1
  239. package/src/tools/skills/execute.ts +1 -1
  240. package/src/tools/types.ts +0 -8
  241. package/src/util/errors.ts +0 -12
  242. package/src/util/platform.ts +3 -50
  243. package/src/workspace/git-service.ts +5 -2
  244. package/src/workspace/migrations/001-avatar-rename.ts +15 -0
  245. package/src/workspace/migrations/003-seed-device-id.ts +17 -1
  246. package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
  247. package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
  248. package/src/workspace/migrations/006-services-config.ts +49 -0
  249. package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
  250. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
  251. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
  252. package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
  253. package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
  254. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
  255. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
  256. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
  257. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
  258. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
  259. package/src/workspace/migrations/017-seed-persona-dirs.ts +95 -0
  260. package/src/workspace/migrations/migrate-to-workspace-volume.ts +23 -1
  261. package/src/workspace/migrations/registry.ts +8 -0
  262. package/src/workspace/migrations/runner.ts +106 -2
  263. package/src/workspace/migrations/types.ts +4 -0
  264. package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
  265. package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
  266. package/src/__tests__/diagnostics-export.test.ts +0 -288
  267. package/src/__tests__/local-gateway-health.test.ts +0 -209
  268. package/src/__tests__/secret-ingress-handler.test.ts +0 -120
  269. package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
  270. package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
  271. package/src/__tests__/swarm-orchestrator.test.ts +0 -463
  272. package/src/__tests__/swarm-plan-validator.test.ts +0 -384
  273. package/src/__tests__/swarm-recursion.test.ts +0 -197
  274. package/src/__tests__/swarm-router-planner.test.ts +0 -234
  275. package/src/__tests__/swarm-tool.test.ts +0 -185
  276. package/src/__tests__/swarm-worker-backend.test.ts +0 -144
  277. package/src/__tests__/swarm-worker-runner.test.ts +0 -288
  278. package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
  279. package/src/commands/cc-command-registry.ts +0 -248
  280. package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
  281. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
  282. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
  283. package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
  284. package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
  285. package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
  286. package/src/config/schemas/swarm.ts +0 -82
  287. package/src/logfire.ts +0 -135
  288. package/src/runtime/local-gateway-health.ts +0 -275
  289. package/src/security/secret-ingress.ts +0 -68
  290. package/src/swarm/backend-claude-code.ts +0 -225
  291. package/src/swarm/checkpoint.ts +0 -137
  292. package/src/swarm/graph-utils.ts +0 -53
  293. package/src/swarm/index.ts +0 -55
  294. package/src/swarm/limits.ts +0 -66
  295. package/src/swarm/orchestrator.ts +0 -424
  296. package/src/swarm/plan-validator.ts +0 -117
  297. package/src/swarm/router-planner.ts +0 -162
  298. package/src/swarm/router-prompts.ts +0 -39
  299. package/src/swarm/synthesizer.ts +0 -81
  300. package/src/swarm/types.ts +0 -72
  301. package/src/swarm/worker-backend.ts +0 -131
  302. package/src/swarm/worker-prompts.ts +0 -80
  303. package/src/swarm/worker-runner.ts +0 -170
  304. package/src/tools/claude-code/claude-code.ts +0 -610
  305. package/src/tools/swarm/delegate.ts +0 -205
@@ -1,288 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
-
3
- import type { SwarmTaskNode } from "../swarm/types.js";
4
- import type {
5
- SwarmWorkerBackend,
6
- SwarmWorkerBackendInput,
7
- } from "../swarm/worker-backend.js";
8
- import {
9
- buildWorkerPrompt,
10
- parseWorkerOutput,
11
- } from "../swarm/worker-prompts.js";
12
- import type { WorkerStatusKind } from "../swarm/worker-runner.js";
13
- import { runWorkerTask } from "../swarm/worker-runner.js";
14
-
15
- function makeTask(overrides?: Partial<SwarmTaskNode>): SwarmTaskNode {
16
- return {
17
- id: "test-task",
18
- role: "coder",
19
- objective: "Write a function",
20
- dependencies: [],
21
- ...overrides,
22
- };
23
- }
24
-
25
- function makeBackend(
26
- overrides?: Partial<SwarmWorkerBackend>,
27
- ): SwarmWorkerBackend {
28
- return {
29
- name: "test-backend",
30
- isAvailable: () => true,
31
- runTask: async () => ({
32
- success: true,
33
- output:
34
- '```json\n{"summary":"Done","artifacts":[],"issues":[],"nextSteps":[]}\n```',
35
- durationMs: 100,
36
- }),
37
- ...overrides,
38
- };
39
- }
40
-
41
- describe("runWorkerTask", () => {
42
- test("returns completed result on success", async () => {
43
- const result = await runWorkerTask({
44
- task: makeTask(),
45
- backend: makeBackend(),
46
- workingDir: "/tmp",
47
- timeoutMs: 5000,
48
- });
49
- expect(result.status).toBe("completed");
50
- expect(result.taskId).toBe("test-task");
51
- expect(result.summary).toBe("Done");
52
- expect(result.durationMs).toBe(100);
53
- });
54
-
55
- test("returns failed result when backend is unavailable", async () => {
56
- const result = await runWorkerTask({
57
- task: makeTask(),
58
- backend: makeBackend({ isAvailable: () => false }),
59
- workingDir: "/tmp",
60
- timeoutMs: 5000,
61
- });
62
- expect(result.status).toBe("failed");
63
- expect(result.issues[0]).toContain("unavailable");
64
- });
65
-
66
- test("returns failed result when backend returns failure", async () => {
67
- const result = await runWorkerTask({
68
- task: makeTask(),
69
- backend: makeBackend({
70
- runTask: async () => ({
71
- success: false,
72
- output: "Something went wrong",
73
- failureReason: "timeout",
74
- durationMs: 900,
75
- }),
76
- }),
77
- workingDir: "/tmp",
78
- timeoutMs: 5000,
79
- });
80
- expect(result.status).toBe("failed");
81
- expect(result.issues).toContain("timeout");
82
- expect(result.durationMs).toBe(900);
83
- });
84
-
85
- test("returns failed result when backend throws", async () => {
86
- const result = await runWorkerTask({
87
- task: makeTask(),
88
- backend: makeBackend({
89
- runTask: async () => {
90
- throw new Error("Boom");
91
- },
92
- }),
93
- workingDir: "/tmp",
94
- timeoutMs: 5000,
95
- });
96
- expect(result.status).toBe("failed");
97
- expect(result.summary).toContain("Boom");
98
- });
99
-
100
- test("emits status callbacks in order", async () => {
101
- const statuses: WorkerStatusKind[] = [];
102
- await runWorkerTask({
103
- task: makeTask(),
104
- backend: makeBackend(),
105
- workingDir: "/tmp",
106
- timeoutMs: 5000,
107
- onStatus: (_taskId, status) => statuses.push(status),
108
- });
109
- expect(statuses).toEqual(["queued", "running", "completed"]);
110
- });
111
-
112
- test("emits queued then failed when backend unavailable", async () => {
113
- const statuses: WorkerStatusKind[] = [];
114
- await runWorkerTask({
115
- task: makeTask(),
116
- backend: makeBackend({ isAvailable: () => false }),
117
- workingDir: "/tmp",
118
- timeoutMs: 5000,
119
- onStatus: (_taskId, status) => statuses.push(status),
120
- });
121
- expect(statuses).toEqual(["queued", "failed"]);
122
- });
123
-
124
- test("continues execution when onStatus callback throws", async () => {
125
- const result = await runWorkerTask({
126
- task: makeTask(),
127
- backend: makeBackend(),
128
- workingDir: "/tmp",
129
- timeoutMs: 5000,
130
- onStatus: () => {
131
- throw new Error("status callback failed");
132
- },
133
- });
134
- expect(result.status).toBe("completed");
135
- expect(result.summary).toBe("Done");
136
- });
137
-
138
- test("maps role to correct profile in prompt", async () => {
139
- let capturedInput: SwarmWorkerBackendInput | null = null;
140
- await runWorkerTask({
141
- task: makeTask({ role: "researcher" }),
142
- backend: makeBackend({
143
- runTask: async (input) => {
144
- capturedInput = input;
145
- return { success: true, output: "ok", durationMs: 50 };
146
- },
147
- }),
148
- workingDir: "/tmp",
149
- timeoutMs: 5000,
150
- });
151
- expect(capturedInput!.profile).toBe("researcher");
152
- });
153
-
154
- test("includes dependency outputs in prompt", async () => {
155
- let capturedInput: SwarmWorkerBackendInput | null = null;
156
- await runWorkerTask({
157
- task: makeTask(),
158
- dependencyOutputs: [{ taskId: "dep-1", summary: "Research complete" }],
159
- backend: makeBackend({
160
- runTask: async (input) => {
161
- capturedInput = input;
162
- return { success: true, output: "ok", durationMs: 50 };
163
- },
164
- }),
165
- workingDir: "/tmp",
166
- timeoutMs: 5000,
167
- });
168
- expect(capturedInput!.prompt).toContain("dep-1");
169
- expect(capturedInput!.prompt).toContain("Research complete");
170
- });
171
- });
172
-
173
- describe("parseWorkerOutput", () => {
174
- test("parses valid fenced JSON", () => {
175
- const raw =
176
- 'Some preamble\n```json\n{"summary":"Done","artifacts":["file.ts"],"issues":[],"nextSteps":["test"]}\n```\nSome epilogue';
177
- const result = parseWorkerOutput(raw);
178
- expect(result.summary).toBe("Done");
179
- expect(result.artifacts).toEqual(["file.ts"]);
180
- expect(result.nextSteps).toEqual(["test"]);
181
- });
182
-
183
- test("falls back to raw summary on invalid JSON", () => {
184
- const raw = "```json\n{invalid json}\n```";
185
- const result = parseWorkerOutput(raw);
186
- expect(result.summary).toBe(raw.slice(0, 500));
187
- expect(result.artifacts).toEqual([]);
188
- });
189
-
190
- test("falls back to raw summary when no JSON block", () => {
191
- const raw = "Just a plain text output without any JSON.";
192
- const result = parseWorkerOutput(raw);
193
- expect(result.summary).toBe(raw);
194
- expect(result.artifacts).toEqual([]);
195
- });
196
-
197
- test("truncates long raw output to 500 chars", () => {
198
- const raw = "x".repeat(1000);
199
- const result = parseWorkerOutput(raw);
200
- expect(result.summary.length).toBe(500);
201
- });
202
-
203
- test("uses the final fenced JSON block when multiple are present", () => {
204
- const raw = [
205
- "```json",
206
- '{"summary":"intermediate","artifacts":["a.ts"],"issues":[],"nextSteps":[]}',
207
- "```",
208
- "",
209
- "```json",
210
- '{"summary":"final","artifacts":["b.ts"],"issues":["warn"],"nextSteps":["ship"]}',
211
- "```",
212
- ].join("\n");
213
- const result = parseWorkerOutput(raw);
214
- expect(result.summary).toBe("final");
215
- expect(result.artifacts).toEqual(["b.ts"]);
216
- expect(result.issues).toEqual(["warn"]);
217
- expect(result.nextSteps).toEqual(["ship"]);
218
- });
219
-
220
- test("skips trailing non-contract JSON and picks the last valid block", () => {
221
- const raw = [
222
- "```json",
223
- '{"summary":"real result","artifacts":["out.ts"],"issues":[],"nextSteps":["deploy"]}',
224
- "```",
225
- "",
226
- "Here is an example config:",
227
- "```json",
228
- '{"port":3000,"debug":true}',
229
- "```",
230
- ].join("\n");
231
- const result = parseWorkerOutput(raw);
232
- expect(result.summary).toBe("real result");
233
- expect(result.artifacts).toEqual(["out.ts"]);
234
- expect(result.nextSteps).toEqual(["deploy"]);
235
- });
236
-
237
- test("skips trailing malformed JSON and picks an earlier valid block", () => {
238
- const raw = [
239
- "```json",
240
- '{"summary":"good","artifacts":[],"issues":[],"nextSteps":[]}',
241
- "```",
242
- "",
243
- "```json",
244
- "{this is not valid json}",
245
- "```",
246
- ].join("\n");
247
- const result = parseWorkerOutput(raw);
248
- expect(result.summary).toBe("good");
249
- });
250
- });
251
-
252
- describe("buildWorkerPrompt", () => {
253
- test("includes role and objective", () => {
254
- const prompt = buildWorkerPrompt({
255
- role: "coder",
256
- objective: "Build feature X",
257
- });
258
- expect(prompt).toContain("coder");
259
- expect(prompt).toContain("Build feature X");
260
- });
261
-
262
- test("includes upstream context when provided", () => {
263
- const prompt = buildWorkerPrompt({
264
- role: "coder",
265
- objective: "Build it",
266
- upstreamContext: "This is a React project",
267
- });
268
- expect(prompt).toContain("This is a React project");
269
- });
270
-
271
- test("includes dependency outputs when provided", () => {
272
- const prompt = buildWorkerPrompt({
273
- role: "coder",
274
- objective: "Build it",
275
- dependencyOutputs: [
276
- { taskId: "research", summary: "Found the API docs" },
277
- ],
278
- });
279
- expect(prompt).toContain("research");
280
- expect(prompt).toContain("Found the API docs");
281
- });
282
-
283
- test("includes output contract instructions", () => {
284
- const prompt = buildWorkerPrompt({ role: "coder", objective: "Test" });
285
- expect(prompt).toContain("```json");
286
- expect(prompt).toContain("summary");
287
- });
288
- });
@@ -1,396 +0,0 @@
1
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
5
-
6
- import {
7
- discoverCCCommands,
8
- getCCCommand,
9
- invalidateCCCommandCache,
10
- loadCCCommandTemplate,
11
- } from "../cc-command-registry.js";
12
-
13
- let tmpDir: string;
14
-
15
- beforeEach(() => {
16
- tmpDir = mkdtempSync(join(tmpdir(), "cc-cmd-test-"));
17
- invalidateCCCommandCache();
18
- });
19
-
20
- afterEach(() => {
21
- rmSync(tmpDir, { recursive: true, force: true });
22
- invalidateCCCommandCache();
23
- });
24
-
25
- /** Helper to create a .claude/commands/ directory with markdown files. */
26
- function createCommandsDir(base: string, files: Record<string, string>): void {
27
- const commandsDir = join(base, ".claude", "commands");
28
- mkdirSync(commandsDir, { recursive: true });
29
- for (const [name, content] of Object.entries(files)) {
30
- writeFileSync(join(commandsDir, name), content, "utf-8");
31
- }
32
- }
33
-
34
- describe("discoverCCCommands", () => {
35
- test("discovers commands in .claude/commands/", () => {
36
- createCommandsDir(tmpDir, {
37
- "hello.md": "# Hello World\nThis is the hello command.",
38
- "deploy.md": "Deploy the application to production.",
39
- });
40
-
41
- const registry = discoverCCCommands(tmpDir);
42
- expect(registry.entries.size).toBe(2);
43
-
44
- const hello = registry.entries.get("hello");
45
- expect(hello).toBeDefined();
46
- expect(hello!.name).toBe("hello");
47
- expect(hello!.summary).toBe("Hello World");
48
- expect(hello!.source).toBe(tmpDir);
49
-
50
- const deploy = registry.entries.get("deploy");
51
- expect(deploy).toBeDefined();
52
- expect(deploy!.name).toBe("deploy");
53
- expect(deploy!.summary).toBe("Deploy the application to production.");
54
- });
55
-
56
- test("child directory commands override parent on name collisions", () => {
57
- // Create parent commands
58
- createCommandsDir(tmpDir, {
59
- "shared.md": "Parent version of shared command.",
60
- });
61
-
62
- // Create child directory with overriding command
63
- const childDir = join(tmpDir, "project");
64
- mkdirSync(childDir, { recursive: true });
65
- createCommandsDir(childDir, {
66
- "shared.md": "Child version of shared command.",
67
- });
68
-
69
- const registry = discoverCCCommands(childDir);
70
- const shared = registry.entries.get("shared");
71
- expect(shared).toBeDefined();
72
- expect(shared!.summary).toBe("Child version of shared command.");
73
- expect(shared!.source).toBe(childDir);
74
- });
75
-
76
- test("invalid filenames are skipped", () => {
77
- createCommandsDir(tmpDir, {
78
- "valid-name.md": "A valid command.",
79
- ".hidden.md": "Hidden file should be skipped.",
80
- "-starts-with-dash.md": "Invalid start character.",
81
- });
82
-
83
- const registry = discoverCCCommands(tmpDir);
84
- expect(registry.entries.size).toBe(1);
85
- expect(registry.entries.has("valid-name")).toBe(true);
86
- expect(registry.entries.has(".hidden")).toBe(false);
87
- expect(registry.entries.has("-starts-with-dash")).toBe(false);
88
- });
89
-
90
- test("non-.md files are ignored", () => {
91
- createCommandsDir(tmpDir, {
92
- "readme.txt": "Not a markdown file.",
93
- "command.md": "A real command.",
94
- "notes.json": "{}",
95
- });
96
-
97
- const registry = discoverCCCommands(tmpDir);
98
- expect(registry.entries.size).toBe(1);
99
- expect(registry.entries.has("command")).toBe(true);
100
- });
101
-
102
- test("empty directory returns empty registry", () => {
103
- const commandsDir = join(tmpDir, ".claude", "commands");
104
- mkdirSync(commandsDir, { recursive: true });
105
-
106
- const registry = discoverCCCommands(tmpDir);
107
- expect(registry.entries.size).toBe(0);
108
- });
109
-
110
- test("no .claude/commands/ directory returns empty registry", () => {
111
- const registry = discoverCCCommands(tmpDir);
112
- expect(registry.entries.size).toBe(0);
113
- });
114
-
115
- test("commands from multiple ancestor levels are merged", () => {
116
- // Parent has a unique command
117
- createCommandsDir(tmpDir, {
118
- "parent-only.md": "Only in parent.",
119
- });
120
-
121
- // Child has a different command
122
- const childDir = join(tmpDir, "child");
123
- mkdirSync(childDir, { recursive: true });
124
- createCommandsDir(childDir, {
125
- "child-only.md": "Only in child.",
126
- });
127
-
128
- const registry = discoverCCCommands(childDir);
129
- expect(registry.entries.size).toBe(2);
130
- expect(registry.entries.has("parent-only")).toBe(true);
131
- expect(registry.entries.has("child-only")).toBe(true);
132
- });
133
- });
134
-
135
- describe("caching", () => {
136
- test("cache returns same instance within TTL", () => {
137
- createCommandsDir(tmpDir, {
138
- "test.md": "Test command.",
139
- });
140
-
141
- const first = discoverCCCommands(tmpDir);
142
- const second = discoverCCCommands(tmpDir);
143
- expect(first).toBe(second); // same object reference
144
- });
145
-
146
- test("invalidateCCCommandCache forces re-discovery", () => {
147
- createCommandsDir(tmpDir, {
148
- "test.md": "Test command.",
149
- });
150
-
151
- const first = discoverCCCommands(tmpDir);
152
-
153
- invalidateCCCommandCache();
154
-
155
- const second = discoverCCCommands(tmpDir);
156
- expect(first).not.toBe(second); // different object reference
157
- expect(second.entries.size).toBe(1);
158
- });
159
-
160
- test("expired TTL forces re-discovery", () => {
161
- createCommandsDir(tmpDir, {
162
- "test.md": "Test command.",
163
- });
164
-
165
- // Use a very short TTL
166
- const first = discoverCCCommands(tmpDir, 0);
167
- const second = discoverCCCommands(tmpDir, 0);
168
- expect(first).not.toBe(second); // different object reference due to expired TTL
169
- });
170
- });
171
-
172
- describe("getCCCommand", () => {
173
- test("looks up command by name (case-insensitive)", () => {
174
- createCommandsDir(tmpDir, {
175
- "MyCommand.md": "My command description.",
176
- });
177
-
178
- const entry = getCCCommand(tmpDir, "mycommand");
179
- expect(entry).toBeDefined();
180
- expect(entry!.name).toBe("MyCommand");
181
-
182
- const entryUpper = getCCCommand(tmpDir, "MYCOMMAND");
183
- expect(entryUpper).toBeDefined();
184
- expect(entryUpper!.name).toBe("MyCommand");
185
- });
186
-
187
- test("returns undefined for non-existent command", () => {
188
- createCommandsDir(tmpDir, {
189
- "exists.md": "I exist.",
190
- });
191
-
192
- const entry = getCCCommand(tmpDir, "nonexistent");
193
- expect(entry).toBeUndefined();
194
- });
195
- });
196
-
197
- describe("loadCCCommandTemplate", () => {
198
- test("reads full file content at execution time", () => {
199
- const fullContent =
200
- "---\ntitle: Test\n---\n\n# Test Command\n\nThis is the full template body.\n\n## Arguments\n- arg1: required\n- arg2: optional\n";
201
- createCommandsDir(tmpDir, {
202
- "test.md": fullContent,
203
- });
204
-
205
- const registry = discoverCCCommands(tmpDir);
206
- const entry = registry.entries.get("test")!;
207
- expect(entry).toBeDefined();
208
-
209
- const template = loadCCCommandTemplate(entry);
210
- expect(template).toBe(fullContent);
211
- });
212
- });
213
-
214
- describe("summary extraction", () => {
215
- test("skips YAML frontmatter", () => {
216
- createCommandsDir(tmpDir, {
217
- "with-frontmatter.md":
218
- "---\ntitle: My Command\nauthor: test\n---\n\nActual summary line.",
219
- });
220
-
221
- const registry = discoverCCCommands(tmpDir);
222
- const entry = registry.entries.get("with-frontmatter");
223
- expect(entry).toBeDefined();
224
- expect(entry!.summary).toBe("Actual summary line.");
225
- });
226
-
227
- test("strips heading markers", () => {
228
- createCommandsDir(tmpDir, {
229
- "heading.md": "## This is a heading",
230
- });
231
-
232
- const registry = discoverCCCommands(tmpDir);
233
- const entry = registry.entries.get("heading");
234
- expect(entry).toBeDefined();
235
- expect(entry!.summary).toBe("This is a heading");
236
- });
237
-
238
- test("strips multiple heading levels", () => {
239
- createCommandsDir(tmpDir, {
240
- "h1.md": "# H1 Heading",
241
- "h3.md": "### H3 Heading",
242
- });
243
-
244
- const registry = discoverCCCommands(tmpDir);
245
- expect(registry.entries.get("h1")!.summary).toBe("H1 Heading");
246
- expect(registry.entries.get("h3")!.summary).toBe("H3 Heading");
247
- });
248
-
249
- test("skips empty lines before summary", () => {
250
- createCommandsDir(tmpDir, {
251
- "empty-lines.md": "\n\n\nFirst real line.",
252
- });
253
-
254
- const registry = discoverCCCommands(tmpDir);
255
- expect(registry.entries.get("empty-lines")!.summary).toBe(
256
- "First real line.",
257
- );
258
- });
259
-
260
- test("truncates summary to 100 characters", () => {
261
- const longLine = "A".repeat(150);
262
- createCommandsDir(tmpDir, {
263
- "long.md": longLine,
264
- });
265
-
266
- const registry = discoverCCCommands(tmpDir);
267
- const entry = registry.entries.get("long");
268
- expect(entry).toBeDefined();
269
- expect(entry!.summary.length).toBe(100);
270
- expect(entry!.summary).toBe("A".repeat(100));
271
- });
272
-
273
- test("handles file with only frontmatter and no content", () => {
274
- createCommandsDir(tmpDir, {
275
- "empty-body.md": "---\ntitle: Empty\n---\n",
276
- });
277
-
278
- const registry = discoverCCCommands(tmpDir);
279
- const entry = registry.entries.get("empty-body");
280
- expect(entry).toBeDefined();
281
- expect(entry!.summary).toBe("");
282
- });
283
-
284
- test("returns empty summary when frontmatter is truncated by partial read", () => {
285
- // Simulate frontmatter that exceeds SUMMARY_READ_BYTES (1024).
286
- // The closing --- delimiter will be cut off, causing FRONTMATTER_REGEX to
287
- // fail. extractSummary should return '' instead of '---'.
288
- const largeFrontmatter =
289
- "---\n" + "key: " + "x".repeat(1100) + "\n---\n\nActual summary.";
290
- createCommandsDir(tmpDir, {
291
- "big-frontmatter.md": largeFrontmatter,
292
- });
293
-
294
- const registry = discoverCCCommands(tmpDir);
295
- const entry = registry.entries.get("big-frontmatter");
296
- expect(entry).toBeDefined();
297
- expect(entry!.summary).toBe("");
298
- });
299
-
300
- test("returns empty summary when frontmatter is truncated (CRLF)", () => {
301
- const largeFrontmatter =
302
- "---\r\n" + "key: " + "x".repeat(1100) + "\r\n---\r\n\r\nActual summary.";
303
- createCommandsDir(tmpDir, {
304
- "big-frontmatter-crlf.md": largeFrontmatter,
305
- });
306
-
307
- const registry = discoverCCCommands(tmpDir);
308
- const entry = registry.entries.get("big-frontmatter-crlf");
309
- expect(entry).toBeDefined();
310
- expect(entry!.summary).toBe("");
311
- });
312
-
313
- test("returns empty summary when frontmatter is truncated with multibyte UTF-8 characters", () => {
314
- // When frontmatter contains multibyte UTF-8 characters (e.g., CJK text),
315
- // the JavaScript string length (UTF-16 code units) is smaller than the
316
- // byte length. The truncation guard must compare byte length, not
317
- // string length, against SUMMARY_READ_BYTES (1024).
318
- //
319
- // Each CJK character is 3 bytes in UTF-8 but 1 code unit in UTF-16.
320
- // We need the total byte count to reach 1024 while string length stays
321
- // well below 1024 to exercise the bug.
322
- const cjkChars = "\u4e00".repeat(340); // 340 chars * 3 bytes = 1020 bytes
323
- // '---\n' is 4 bytes, so total = 4 + 1020 = 1024 bytes, but string
324
- // length = 4 + 340 = 344 chars — well under 1024.
325
- const truncatedContent = "---\n" + cjkChars;
326
- createCommandsDir(tmpDir, {
327
- "multibyte-frontmatter.md": truncatedContent,
328
- });
329
-
330
- const registry = discoverCCCommands(tmpDir);
331
- const entry = registry.entries.get("multibyte-frontmatter");
332
- expect(entry).toBeDefined();
333
- // Should return '' because the frontmatter opening delimiter is present
334
- // but the closing delimiter is missing and the byte length reached the
335
- // read limit — indicating truncation.
336
- expect(entry!.summary).toBe("");
337
- });
338
-
339
- test("returns summary for small file starting with thematic break ---", () => {
340
- // A small markdown file that starts with "---" as a thematic break (not
341
- // frontmatter) should still have its first content line extracted as a
342
- // summary, rather than being treated as truncated frontmatter.
343
- createCommandsDir(tmpDir, {
344
- "thematic-break.md":
345
- "---\nThis is a valid summary after a thematic break.",
346
- });
347
-
348
- const registry = discoverCCCommands(tmpDir);
349
- const entry = registry.entries.get("thematic-break");
350
- expect(entry).toBeDefined();
351
- expect(entry!.summary).toBe(
352
- "This is a valid summary after a thematic break.",
353
- );
354
- });
355
-
356
- test("handles frontmatter with Windows-style line endings", () => {
357
- createCommandsDir(tmpDir, {
358
- "crlf.md": "---\r\ntitle: Test\r\n---\r\n\r\nSummary with CRLF.",
359
- });
360
-
361
- const registry = discoverCCCommands(tmpDir);
362
- const entry = registry.entries.get("crlf");
363
- expect(entry).toBeDefined();
364
- expect(entry!.summary).toBe("Summary with CRLF.");
365
- });
366
- });
367
-
368
- describe("command name validation", () => {
369
- test("accepts valid names with dots, dashes, underscores", () => {
370
- createCommandsDir(tmpDir, {
371
- "my-command.md": "Dashed name.",
372
- "my_command.md": "Underscored name.",
373
- "my.command.md": "Dotted name.",
374
- "Command123.md": "Alphanumeric.",
375
- "a.md": "Single char.",
376
- });
377
-
378
- const registry = discoverCCCommands(tmpDir);
379
- expect(registry.entries.has("my-command")).toBe(true);
380
- expect(registry.entries.has("my_command")).toBe(true);
381
- expect(registry.entries.has("my.command")).toBe(true);
382
- expect(registry.entries.has("command123")).toBe(true);
383
- expect(registry.entries.has("a")).toBe(true);
384
- });
385
-
386
- test("rejects names starting with special characters", () => {
387
- createCommandsDir(tmpDir, {
388
- "_start.md": "Starts with underscore.",
389
- ".start.md": "Starts with dot.",
390
- "-start.md": "Starts with dash.",
391
- });
392
-
393
- const registry = discoverCCCommands(tmpDir);
394
- expect(registry.entries.size).toBe(0);
395
- });
396
- });