@vellumai/assistant 0.5.14 → 0.5.16

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 (175) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/docs/architecture/integrations.md +15 -14
  3. package/knip.json +3 -1
  4. package/openapi.yaml +11 -43
  5. package/package.json +1 -1
  6. package/src/__tests__/assistant-feature-flags-integration.test.ts +3 -375
  7. package/src/__tests__/ces-rpc-credential-backend.test.ts +4 -1
  8. package/src/__tests__/checker.test.ts +59 -0
  9. package/src/__tests__/cli-command-risk-guard.test.ts +98 -10
  10. package/src/__tests__/cli-memory.test.ts +372 -0
  11. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +12 -2
  12. package/src/__tests__/config-schema.test.ts +0 -2
  13. package/src/__tests__/config-watcher-feature-flags.test.ts +211 -0
  14. package/src/__tests__/conversation-runtime-assembly.test.ts +7 -4
  15. package/src/__tests__/conversation-slash-commands.test.ts +2 -6
  16. package/src/__tests__/conversation-usage.test.ts +1 -0
  17. package/src/__tests__/credential-security-e2e.test.ts +4 -1
  18. package/src/__tests__/docker-signing-key-bootstrap.test.ts +7 -73
  19. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -7
  20. package/src/__tests__/guardian-routing-invariants.test.ts +151 -0
  21. package/src/__tests__/heartbeat-service.test.ts +1 -3
  22. package/src/__tests__/intent-routing.test.ts +6 -18
  23. package/src/__tests__/log-export-workspace.test.ts +2 -28
  24. package/src/__tests__/managed-skill-lifecycle.test.ts +7 -37
  25. package/src/__tests__/managed-store.test.ts +2 -10
  26. package/src/__tests__/messaging-send-tool.test.ts +6 -6
  27. package/src/__tests__/migration-cross-version-compatibility.test.ts +1 -29
  28. package/src/__tests__/migration-export-http.test.ts +3 -34
  29. package/src/__tests__/migration-import-commit-http.test.ts +1 -29
  30. package/src/__tests__/migration-import-preflight-http.test.ts +3 -34
  31. package/src/__tests__/no-domain-routing-in-prompt-guard.test.ts +2 -1
  32. package/src/__tests__/oauth-apps-routes.test.ts +120 -10
  33. package/src/__tests__/oauth-connect-orchestrator.test.ts +709 -0
  34. package/src/__tests__/oauth-provider-serializer.test.ts +2 -1
  35. package/src/__tests__/oauth-provider-visibility.test.ts +149 -0
  36. package/src/__tests__/oauth-providers-routes.test.ts +5 -2
  37. package/src/__tests__/oauth-store.test.ts +0 -5
  38. package/src/__tests__/outlook-messaging-provider.test.ts +576 -0
  39. package/src/__tests__/path-policy.test.ts +2 -17
  40. package/src/__tests__/permission-types.test.ts +0 -1
  41. package/src/__tests__/platform-callback-registration.test.ts +3 -7
  42. package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
  43. package/src/__tests__/provider-error-scenarios.test.ts +0 -2
  44. package/src/__tests__/qdrant-manager.test.ts +68 -21
  45. package/src/__tests__/require-fresh-approval.test.ts +0 -1
  46. package/src/__tests__/sandbox-diagnostics.test.ts +20 -29
  47. package/src/__tests__/scaffold-managed-skill-tool.test.ts +2 -10
  48. package/src/__tests__/secret-allowlist.test.ts +20 -35
  49. package/src/__tests__/shell-credential-ref.test.ts +0 -5
  50. package/src/__tests__/skill-load-feature-flag.test.ts +2 -43
  51. package/src/__tests__/skill-load-inline-command.test.ts +3 -65
  52. package/src/__tests__/skill-load-inline-includes.test.ts +3 -65
  53. package/src/__tests__/skill-load-tool.test.ts +3 -67
  54. package/src/__tests__/skill-memory.test.ts +362 -119
  55. package/src/__tests__/skills.test.ts +22 -49
  56. package/src/__tests__/slack-channel-config.test.ts +2 -21
  57. package/src/__tests__/starter-bundle.test.ts +2 -8
  58. package/src/__tests__/stt-hints.test.ts +7 -2
  59. package/src/__tests__/system-prompt.test.ts +25 -45
  60. package/src/__tests__/task-compiler.test.ts +0 -21
  61. package/src/__tests__/task-management-tools.test.ts +0 -21
  62. package/src/__tests__/task-memory-cleanup.test.ts +0 -21
  63. package/src/__tests__/task-runner.test.ts +0 -21
  64. package/src/__tests__/task-scheduler.test.ts +0 -21
  65. package/src/__tests__/terminal-tools.test.ts +1 -17
  66. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +0 -79
  67. package/src/__tests__/tool-approval-handler.test.ts +1 -20
  68. package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -11
  69. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -25
  70. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  71. package/src/__tests__/tool-executor.test.ts +0 -1
  72. package/src/__tests__/tool-grant-request-escalation.test.ts +1 -20
  73. package/src/__tests__/tool-preview-lifecycle.test.ts +0 -20
  74. package/src/__tests__/trust-store.test.ts +9 -41
  75. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -30
  76. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -21
  77. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -22
  78. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -22
  79. package/src/__tests__/trusted-contact-verification.test.ts +0 -22
  80. package/src/__tests__/turn-boundary-resolution.test.ts +0 -28
  81. package/src/__tests__/twilio-provider.test.ts +0 -16
  82. package/src/__tests__/twilio-routes-twiml.test.ts +7 -12
  83. package/src/__tests__/twilio-routes.test.ts +0 -24
  84. package/src/__tests__/update-bulletin.test.ts +17 -89
  85. package/src/__tests__/usage-cache-backfill-migration.test.ts +0 -20
  86. package/src/__tests__/usage-routes.test.ts +0 -21
  87. package/src/__tests__/user-reference.test.ts +1 -5
  88. package/src/__tests__/vbundle-pax-and-symlink.test.ts +4 -34
  89. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +2 -53
  90. package/src/__tests__/voice-invite-redemption.test.ts +0 -21
  91. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -24
  92. package/src/__tests__/voice-session-bridge.test.ts +0 -21
  93. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -23
  94. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +2 -2
  95. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +2 -23
  96. package/src/__tests__/workspace-migration-down-functions.test.ts +0 -6
  97. package/src/acp/client-handler.ts +1 -2
  98. package/src/cli/__tests__/notifications.test.ts +0 -22
  99. package/src/cli/cli-memory.ts +176 -0
  100. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  101. package/src/cli/commands/oauth/connect.ts +15 -0
  102. package/src/cli/commands/oauth/providers.ts +49 -42
  103. package/src/cli/commands/platform/__tests__/connect.test.ts +2 -48
  104. package/src/cli/commands/platform/__tests__/disconnect.test.ts +2 -48
  105. package/src/cli/commands/platform/__tests__/status.test.ts +0 -50
  106. package/src/config/bundled-skills/computer-use/TOOLS.json +7 -7
  107. package/src/config/bundled-skills/messaging/SKILL.md +17 -2
  108. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  109. package/src/config/feature-flag-registry.json +16 -0
  110. package/src/config/loader.ts +4 -0
  111. package/src/config/schemas/security.ts +0 -6
  112. package/src/config/schemas/services.ts +8 -0
  113. package/src/context/window-manager.ts +28 -9
  114. package/src/credential-execution/approval-bridge.ts +0 -1
  115. package/src/daemon/config-watcher.ts +51 -0
  116. package/src/daemon/conversation-agent-loop.ts +3 -2
  117. package/src/daemon/conversation-process.ts +1 -0
  118. package/src/daemon/conversation-usage.ts +1 -0
  119. package/src/daemon/handlers/skills.ts +9 -1
  120. package/src/daemon/lifecycle.ts +13 -4
  121. package/src/daemon/message-types/conversations.ts +1 -0
  122. package/src/daemon/providers-setup.ts +2 -0
  123. package/src/daemon/server.ts +26 -22
  124. package/src/events/domain-events.ts +1 -2
  125. package/src/memory/db-init.ts +9 -0
  126. package/src/memory/job-handlers/batch-extraction.ts +16 -4
  127. package/src/memory/job-handlers/embedding.test.ts +3 -27
  128. package/src/memory/job-handlers/journal-carry-forward.test.ts +1 -29
  129. package/src/memory/llm-usage-store.ts +35 -2
  130. package/src/memory/migrations/201-oauth-providers-feature-flag.ts +11 -0
  131. package/src/memory/migrations/202-drop-callback-transport-column.ts +13 -0
  132. package/src/memory/migrations/index.ts +2 -0
  133. package/src/memory/qdrant-manager.ts +26 -5
  134. package/src/memory/query-expansion.ts +1 -1
  135. package/src/memory/retriever.test.ts +22 -20
  136. package/src/memory/retriever.ts +10 -2
  137. package/src/memory/schema/oauth.ts +1 -1
  138. package/src/memory/search/mmr.ts +8 -5
  139. package/src/memory/slack-thread-store.ts +17 -0
  140. package/src/messaging/providers/outlook/adapter.ts +193 -0
  141. package/src/messaging/providers/outlook/client.ts +311 -0
  142. package/src/messaging/providers/outlook/types.ts +83 -0
  143. package/src/notifications/adapters/slack.ts +1 -1
  144. package/src/oauth/__tests__/identity-verifier.test.ts +1 -1
  145. package/src/oauth/connect-orchestrator.ts +10 -3
  146. package/src/oauth/oauth-store.ts +10 -11
  147. package/src/oauth/provider-serializer.ts +3 -0
  148. package/src/oauth/provider-visibility.ts +16 -0
  149. package/src/oauth/seed-providers.ts +49 -17
  150. package/src/permissions/checker.ts +39 -7
  151. package/src/permissions/types.ts +2 -4
  152. package/src/prompts/journal-context.ts +9 -11
  153. package/src/prompts/system-prompt.ts +3 -64
  154. package/src/prompts/templates/UPDATES.md +6 -0
  155. package/src/runtime/auth/__tests__/credential-service.test.ts +1 -27
  156. package/src/runtime/auth/__tests__/token-service.test.ts +1 -25
  157. package/src/runtime/auth/route-policy.ts +0 -4
  158. package/src/runtime/guardian-reply-router.ts +6 -2
  159. package/src/runtime/routes/conversation-query-routes.ts +2 -58
  160. package/src/runtime/routes/inbound-stages/background-dispatch.ts +43 -2
  161. package/src/runtime/routes/memory-item-routes.test.ts +0 -17
  162. package/src/runtime/routes/memory-item-routes.ts +103 -12
  163. package/src/runtime/routes/oauth-apps.ts +18 -1
  164. package/src/runtime/routes/oauth-providers.ts +13 -1
  165. package/src/runtime/routes/settings-routes.ts +1 -0
  166. package/src/runtime/routes/usage-routes.ts +19 -2
  167. package/src/runtime/routes/work-items-routes.test.ts +0 -21
  168. package/src/runtime/routes/workspace-routes.test.ts +3 -27
  169. package/src/security/secret-allowlist.ts +4 -4
  170. package/src/skills/skill-memory.ts +62 -23
  171. package/src/tools/memory/handlers.test.ts +1 -29
  172. package/src/tools/permission-checker.ts +0 -18
  173. package/src/tools/skills/skill-script-runner.ts +1 -1
  174. package/src/util/device-id.ts +3 -65
  175. package/src/workspace/git-service.ts +27 -6
@@ -1,26 +1,8 @@
1
- import { mkdtempSync, rmSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
1
+ import { rmSync } from "node:fs";
4
2
  import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
5
3
 
6
4
  import { eq } from "drizzle-orm";
7
5
 
8
- const testDir = mkdtempSync(join(tmpdir(), "skill-memory-"));
9
-
10
- mock.module("../util/platform.js", () => ({
11
- getDataDir: () => testDir,
12
- isMacOS: () => process.platform === "darwin",
13
- isLinux: () => process.platform === "linux",
14
- isWindows: () => process.platform === "win32",
15
- getPidPath: () => join(testDir, "test.pid"),
16
- getDbPath: () => join(testDir, "test.db"),
17
- getLogPath: () => join(testDir, "test.log"),
18
- ensureDataDir: () => {},
19
- getWorkspaceSkillsDir: () => join(testDir, "skills"),
20
- getWorkspaceConfigPath: () => join(testDir, "config.json"),
21
- readPlatformToken: () => undefined,
22
- }));
23
-
24
6
  mock.module("../util/logger.js", () => ({
25
7
  getLogger: () =>
26
8
  new Proxy({} as Record<string, unknown>, {
@@ -38,16 +20,15 @@ mock.module("../memory/qdrant-client.js", () => ({
38
20
  initQdrantClient: () => {},
39
21
  }));
40
22
 
41
- // Controllable mock for resolveCatalog used by seedCatalogSkillMemories
42
- let mockResolveCatalog: () => Promise<
43
- import("../skills/catalog-install.js").CatalogSkill[]
44
- > = async () => [];
23
+ // Controllable mock for loadSkillCatalog used by seedCatalogSkillMemories
24
+ let mockLoadSkillCatalog: () => import("../config/skills.js").SkillSummary[] =
25
+ () => [];
45
26
 
46
- mock.module("../skills/catalog-install.js", () => ({
47
- resolveCatalog: (..._args: unknown[]) => mockResolveCatalog(),
27
+ mock.module("../config/skills.js", () => ({
28
+ loadSkillCatalog: (..._args: unknown[]) => mockLoadSkillCatalog(),
48
29
  }));
49
30
 
50
- // Controllable mock for isAssistantFeatureFlagEnabled used by seedCatalogSkillMemories
31
+ // Controllable mock for isAssistantFeatureFlagEnabled used by resolveSkillStates
51
32
  let mockIsFeatureFlagEnabled: (key: string) => boolean = () => true;
52
33
 
53
34
  mock.module("../config/assistant-feature-flags.js", () => ({
@@ -78,25 +59,24 @@ mock.module("../config/loader.js", () => ({
78
59
  invalidateConfigCache: () => {},
79
60
  }));
80
61
 
62
+ import type { SkillSummary } from "../config/skills.js";
81
63
  import { getDb, initializeDb, resetDb } from "../memory/db.js";
82
64
  import { memoryItems, memoryJobs } from "../memory/schema.js";
83
- import type { CatalogSkill } from "../skills/catalog-install.js";
84
65
  import {
85
66
  buildCapabilityStatement,
86
67
  deleteSkillCapabilityMemory,
68
+ fromSkillSummary,
87
69
  seedCatalogSkillMemories,
70
+ type SkillCapabilityInput,
88
71
  upsertSkillCapabilityMemory,
89
72
  } from "../skills/skill-memory.js";
73
+ import { ensureDataDir, getDbPath } from "../util/platform.js";
90
74
 
75
+ ensureDataDir();
91
76
  initializeDb();
92
77
 
93
78
  afterAll(() => {
94
79
  resetDb();
95
- try {
96
- rmSync(testDir, { recursive: true });
97
- } catch {
98
- // best effort cleanup
99
- }
100
80
  });
101
81
 
102
82
  function resetTables() {
@@ -107,11 +87,17 @@ function resetTables() {
107
87
  db.run("DELETE FROM memory_jobs");
108
88
  }
109
89
 
110
- function makeSkill(overrides: Partial<CatalogSkill> = {}): CatalogSkill {
90
+ function makeSkillSummary(
91
+ overrides: Partial<SkillSummary> = {},
92
+ ): SkillSummary {
111
93
  return {
112
94
  id: "test-skill",
113
- name: "Test Skill",
95
+ name: "test-skill",
96
+ displayName: "Test Skill",
114
97
  description: "A skill for testing",
98
+ directoryPath: "/skills/test-skill",
99
+ skillFilePath: "/skills/test-skill/SKILL.md",
100
+ source: "managed",
115
101
  ...overrides,
116
102
  };
117
103
  }
@@ -120,33 +106,63 @@ function makeSkill(overrides: Partial<CatalogSkill> = {}): CatalogSkill {
120
106
 
121
107
  describe("buildCapabilityStatement", () => {
122
108
  test("includes display name, id, and description", () => {
123
- const entry = makeSkill({
124
- metadata: { vellum: { "display-name": "My Skill" } },
125
- });
126
- const result = buildCapabilityStatement(entry);
109
+ const input: SkillCapabilityInput = {
110
+ id: "test-skill",
111
+ displayName: "My Skill",
112
+ description: "A skill for testing",
113
+ };
114
+ const result = buildCapabilityStatement(input);
127
115
  expect(result).toContain('"My Skill"');
128
116
  expect(result).toContain("(test-skill)");
129
117
  expect(result).toContain("A skill for testing");
130
118
  });
131
119
 
132
120
  test("includes activation hints when present", () => {
133
- const entry = makeSkill({
134
- metadata: {
135
- vellum: {
136
- "display-name": "My Skill",
137
- "activation-hints": ["user asks to search", "needs web data"],
138
- },
139
- },
140
- });
141
- const result = buildCapabilityStatement(entry);
121
+ const input: SkillCapabilityInput = {
122
+ id: "test-skill",
123
+ displayName: "My Skill",
124
+ description: "A skill for testing",
125
+ activationHints: ["user asks to search", "needs web data"],
126
+ };
127
+ const result = buildCapabilityStatement(input);
142
128
  expect(result).toContain("Use when:");
143
129
  expect(result).toContain("user asks to search");
144
130
  expect(result).toContain("needs web data");
145
131
  });
146
132
 
147
- test("works without metadata (falls back to name)", () => {
148
- const entry = makeSkill({ metadata: undefined });
149
- const result = buildCapabilityStatement(entry);
133
+ test("includes avoidWhen routing cues when present", () => {
134
+ const input: SkillCapabilityInput = {
135
+ id: "test-skill",
136
+ displayName: "My Skill",
137
+ description: "A skill for testing",
138
+ avoidWhen: ["user wants local files only", "offline mode"],
139
+ };
140
+ const result = buildCapabilityStatement(input);
141
+ expect(result).toContain("Avoid when:");
142
+ expect(result).toContain("user wants local files only");
143
+ expect(result).toContain("offline mode");
144
+ });
145
+
146
+ test("includes both activationHints and avoidWhen when present", () => {
147
+ const input: SkillCapabilityInput = {
148
+ id: "test-skill",
149
+ displayName: "My Skill",
150
+ description: "A skill for testing",
151
+ activationHints: ["user asks to search"],
152
+ avoidWhen: ["offline mode"],
153
+ };
154
+ const result = buildCapabilityStatement(input);
155
+ expect(result).toContain("Use when: user asks to search.");
156
+ expect(result).toContain("Avoid when: offline mode.");
157
+ });
158
+
159
+ test("works with just name as displayName", () => {
160
+ const input: SkillCapabilityInput = {
161
+ id: "test-skill",
162
+ displayName: "Test Skill",
163
+ description: "A skill for testing",
164
+ };
165
+ const result = buildCapabilityStatement(input);
150
166
  expect(result).toContain('"Test Skill"');
151
167
  expect(result).toContain("(test-skill)");
152
168
  expect(result).toContain("A skill for testing");
@@ -154,20 +170,70 @@ describe("buildCapabilityStatement", () => {
154
170
 
155
171
  test("truncates long statements to 500 chars", () => {
156
172
  const longDesc = "x".repeat(600);
157
- const entry = makeSkill({ description: longDesc });
158
- const result = buildCapabilityStatement(entry);
173
+ const input: SkillCapabilityInput = {
174
+ id: "test-skill",
175
+ displayName: "Test Skill",
176
+ description: longDesc,
177
+ };
178
+ const result = buildCapabilityStatement(input);
159
179
  expect(result.length).toBe(500);
160
180
  });
161
181
  });
162
182
 
183
+ // ─── fromSkillSummary ────────────────────────────────────────────────────────
184
+
185
+ describe("fromSkillSummary", () => {
186
+ test("maps displayName from SkillSummary", () => {
187
+ const entry = makeSkillSummary({ displayName: "Pretty Name" });
188
+ const input = fromSkillSummary(entry);
189
+ expect(input.displayName).toBe("Pretty Name");
190
+ });
191
+
192
+ test("maps activationHints from SkillSummary", () => {
193
+ const hints = ["user asks to search", "needs web data"];
194
+ const entry = makeSkillSummary({ activationHints: hints });
195
+ const input = fromSkillSummary(entry);
196
+ expect(input.activationHints).toEqual(hints);
197
+ });
198
+
199
+ test("leaves activationHints undefined when not present", () => {
200
+ const entry = makeSkillSummary({ activationHints: undefined });
201
+ const input = fromSkillSummary(entry);
202
+ expect(input.activationHints).toBeUndefined();
203
+ });
204
+
205
+ test("maps avoidWhen from SkillSummary", () => {
206
+ const cues = ["offline mode", "user wants local files only"];
207
+ const entry = makeSkillSummary({ avoidWhen: cues });
208
+ const input = fromSkillSummary(entry);
209
+ expect(input.avoidWhen).toEqual(cues);
210
+ });
211
+
212
+ test("leaves avoidWhen undefined when not present", () => {
213
+ const entry = makeSkillSummary({ avoidWhen: undefined });
214
+ const input = fromSkillSummary(entry);
215
+ expect(input.avoidWhen).toBeUndefined();
216
+ });
217
+
218
+ test("copies id and description directly", () => {
219
+ const entry = makeSkillSummary({
220
+ id: "my-id",
221
+ description: "Does amazing things",
222
+ });
223
+ const input = fromSkillSummary(entry);
224
+ expect(input.id).toBe("my-id");
225
+ expect(input.description).toBe("Does amazing things");
226
+ });
227
+ });
228
+
163
229
  // ─── upsertSkillCapabilityMemory ─────────────────────────────────────────────
164
230
 
165
231
  describe("upsertSkillCapabilityMemory", () => {
166
232
  beforeEach(resetTables);
167
233
 
168
234
  test("inserts with correct kind, subject, confidence, importance", () => {
169
- const entry = makeSkill();
170
- upsertSkillCapabilityMemory("test-skill", entry);
235
+ const input = fromSkillSummary(makeSkillSummary());
236
+ upsertSkillCapabilityMemory("test-skill", input);
171
237
 
172
238
  const db = getDb();
173
239
  const items = db.select().from(memoryItems).all();
@@ -186,8 +252,8 @@ describe("upsertSkillCapabilityMemory", () => {
186
252
  });
187
253
 
188
254
  test("is idempotent (same entry only touches lastSeenAt)", () => {
189
- const entry = makeSkill();
190
- upsertSkillCapabilityMemory("test-skill", entry);
255
+ const input = fromSkillSummary(makeSkillSummary());
256
+ upsertSkillCapabilityMemory("test-skill", input);
191
257
 
192
258
  const db = getDb();
193
259
  const before = db.select().from(memoryItems).all();
@@ -195,7 +261,7 @@ describe("upsertSkillCapabilityMemory", () => {
195
261
  const originalLastSeen = before[0].lastSeenAt;
196
262
 
197
263
  // Upsert again
198
- upsertSkillCapabilityMemory("test-skill", entry);
264
+ upsertSkillCapabilityMemory("test-skill", input);
199
265
 
200
266
  const after = db.select().from(memoryItems).all();
201
267
  expect(after).toHaveLength(1);
@@ -209,8 +275,10 @@ describe("upsertSkillCapabilityMemory", () => {
209
275
  });
210
276
 
211
277
  test("updates statement when description changes", () => {
212
- const entry = makeSkill({ description: "Original description" });
213
- upsertSkillCapabilityMemory("test-skill", entry);
278
+ const input = fromSkillSummary(
279
+ makeSkillSummary({ description: "Original description" }),
280
+ );
281
+ upsertSkillCapabilityMemory("test-skill", input);
214
282
 
215
283
  const db = getDb();
216
284
  const before = db.select().from(memoryItems).all();
@@ -218,8 +286,10 @@ describe("upsertSkillCapabilityMemory", () => {
218
286
  expect(before[0].statement).toContain("Original description");
219
287
 
220
288
  // Change description
221
- const updatedEntry = makeSkill({ description: "Updated description" });
222
- upsertSkillCapabilityMemory("test-skill", updatedEntry);
289
+ const updatedInput = fromSkillSummary(
290
+ makeSkillSummary({ description: "Updated description" }),
291
+ );
292
+ upsertSkillCapabilityMemory("test-skill", updatedInput);
223
293
 
224
294
  const after = db.select().from(memoryItems).all();
225
295
  expect(after).toHaveLength(1);
@@ -232,8 +302,8 @@ describe("upsertSkillCapabilityMemory", () => {
232
302
  });
233
303
 
234
304
  test("reactivates soft-deleted items", () => {
235
- const entry = makeSkill();
236
- upsertSkillCapabilityMemory("test-skill", entry);
305
+ const input = fromSkillSummary(makeSkillSummary());
306
+ upsertSkillCapabilityMemory("test-skill", input);
237
307
 
238
308
  const db = getDb();
239
309
  // Soft-delete the item
@@ -249,7 +319,7 @@ describe("upsertSkillCapabilityMemory", () => {
249
319
  db.run("DELETE FROM memory_jobs");
250
320
 
251
321
  // Upsert again — should reactivate
252
- upsertSkillCapabilityMemory("test-skill", entry);
322
+ upsertSkillCapabilityMemory("test-skill", input);
253
323
 
254
324
  const reactivated = db.select().from(memoryItems).all();
255
325
  expect(reactivated).toHaveLength(1);
@@ -273,7 +343,10 @@ describe("upsertSkillCapabilityMemory", () => {
273
343
  db.run("DROP TABLE IF EXISTS memory_items");
274
344
 
275
345
  expect(() => {
276
- upsertSkillCapabilityMemory("test-skill", makeSkill());
346
+ upsertSkillCapabilityMemory(
347
+ "test-skill",
348
+ fromSkillSummary(makeSkillSummary()),
349
+ );
277
350
  }).not.toThrow();
278
351
 
279
352
  // Restore DB state for subsequent tests.
@@ -281,8 +354,9 @@ describe("upsertSkillCapabilityMemory", () => {
281
354
  // resetting the connection leaves stale migration checkpoints that skip
282
355
  // checkpoint-guarded ALTER TABLE migrations (e.g. source_type column).
283
356
  resetDb();
357
+ const dbPath = getDbPath();
284
358
  for (const ext of ["", "-wal", "-shm"]) {
285
- rmSync(join(testDir, `test.db${ext}`), { force: true });
359
+ rmSync(`${dbPath}${ext}`, { force: true });
286
360
  }
287
361
  initializeDb();
288
362
  });
@@ -294,8 +368,8 @@ describe("deleteSkillCapabilityMemory", () => {
294
368
  beforeEach(resetTables);
295
369
 
296
370
  test("soft-deletes matching item", () => {
297
- const entry = makeSkill();
298
- upsertSkillCapabilityMemory("test-skill", entry);
371
+ const input = fromSkillSummary(makeSkillSummary());
372
+ upsertSkillCapabilityMemory("test-skill", input);
299
373
 
300
374
  const db = getDb();
301
375
  const before = db.select().from(memoryItems).all();
@@ -333,8 +407,9 @@ describe("deleteSkillCapabilityMemory", () => {
333
407
  // Restore DB state for subsequent tests (see upsert "does not throw" test
334
408
  // for rationale on why we delete the DB file).
335
409
  resetDb();
410
+ const dbPath = getDbPath();
336
411
  for (const ext of ["", "-wal", "-shm"]) {
337
- rmSync(join(testDir, `test.db${ext}`), { force: true });
412
+ rmSync(`${dbPath}${ext}`, { force: true });
338
413
  }
339
414
  initializeDb();
340
415
  });
@@ -346,19 +421,31 @@ describe("seedCatalogSkillMemories", () => {
346
421
  beforeEach(() => {
347
422
  resetTables();
348
423
  // Reset mocks to defaults
349
- mockResolveCatalog = async () => [];
424
+ mockLoadSkillCatalog = () => [];
350
425
  mockIsFeatureFlagEnabled = () => true;
351
426
  });
352
427
 
353
- test("upserts capability memories for all catalog entries", async () => {
354
- const skills: CatalogSkill[] = [
355
- makeSkill({ id: "skill-a", name: "Skill A", description: "Does A" }),
356
- makeSkill({ id: "skill-b", name: "Skill B", description: "Does B" }),
357
- makeSkill({ id: "skill-c", name: "Skill C", description: "Does C" }),
428
+ test("upserts capability memories for all enabled skills", () => {
429
+ const skills: SkillSummary[] = [
430
+ makeSkillSummary({
431
+ id: "skill-a",
432
+ displayName: "Skill A",
433
+ description: "Does A",
434
+ }),
435
+ makeSkillSummary({
436
+ id: "skill-b",
437
+ displayName: "Skill B",
438
+ description: "Does B",
439
+ }),
440
+ makeSkillSummary({
441
+ id: "skill-c",
442
+ displayName: "Skill C",
443
+ description: "Does C",
444
+ }),
358
445
  ];
359
- mockResolveCatalog = async () => skills;
446
+ mockLoadSkillCatalog = () => skills;
360
447
 
361
- await seedCatalogSkillMemories();
448
+ seedCatalogSkillMemories();
362
449
 
363
450
  const db = getDb();
364
451
  const items = db
@@ -381,15 +468,128 @@ describe("seedCatalogSkillMemories", () => {
381
468
  }
382
469
  });
383
470
 
384
- test("prunes stale capabilities for skills no longer in catalog", async () => {
471
+ test("includes bundled skills in seeded memories", () => {
472
+ const skills: SkillSummary[] = [
473
+ makeSkillSummary({
474
+ id: "managed-skill",
475
+ displayName: "Managed",
476
+ description: "A managed skill",
477
+ source: "managed",
478
+ }),
479
+ makeSkillSummary({
480
+ id: "bundled-skill",
481
+ displayName: "Bundled",
482
+ description: "A bundled skill",
483
+ source: "bundled",
484
+ bundled: true,
485
+ }),
486
+ ];
487
+ mockLoadSkillCatalog = () => skills;
488
+
489
+ seedCatalogSkillMemories();
490
+
491
+ const db = getDb();
492
+ const items = db
493
+ .select()
494
+ .from(memoryItems)
495
+ .where(eq(memoryItems.kind, "capability"))
496
+ .all();
497
+ expect(items).toHaveLength(2);
498
+
499
+ const subjects = items.map((i) => i.subject).sort();
500
+ expect(subjects).toEqual(["skill:bundled-skill", "skill:managed-skill"]);
501
+
502
+ for (const item of items) {
503
+ expect(item.status).toBe("active");
504
+ }
505
+ });
506
+
507
+ test("excludes bundled skills filtered by allowBundled config", () => {
508
+ const skills: SkillSummary[] = [
509
+ makeSkillSummary({
510
+ id: "allowed-bundled",
511
+ displayName: "Allowed Bundled",
512
+ description: "This bundled skill is allowed",
513
+ source: "bundled",
514
+ bundled: true,
515
+ }),
516
+ makeSkillSummary({
517
+ id: "blocked-bundled",
518
+ displayName: "Blocked Bundled",
519
+ description: "This bundled skill is not in allowBundled",
520
+ source: "bundled",
521
+ bundled: true,
522
+ }),
523
+ makeSkillSummary({
524
+ id: "managed-skill",
525
+ displayName: "Managed",
526
+ description: "A managed skill",
527
+ source: "managed",
528
+ }),
529
+ ];
530
+ mockLoadSkillCatalog = () => skills;
531
+
532
+ // Override config to set allowBundled to only allow one bundled skill
533
+ const configWithAllowBundled = {
534
+ ...TEST_CONFIG,
535
+ skills: {
536
+ ...TEST_CONFIG.skills,
537
+ allowBundled: ["allowed-bundled"],
538
+ },
539
+ };
540
+ mock.module("../config/loader.js", () => ({
541
+ loadConfig: () => configWithAllowBundled,
542
+ getConfig: () => configWithAllowBundled,
543
+ loadRawConfig: () => ({}),
544
+ saveRawConfig: () => {},
545
+ invalidateConfigCache: () => {},
546
+ }));
547
+
548
+ seedCatalogSkillMemories();
549
+
550
+ const db = getDb();
551
+ const items = db
552
+ .select()
553
+ .from(memoryItems)
554
+ .where(eq(memoryItems.kind, "capability"))
555
+ .all();
556
+
557
+ // Only allowed-bundled and managed-skill should be seeded
558
+ expect(items).toHaveLength(2);
559
+ const subjects = items.map((i) => i.subject).sort();
560
+ expect(subjects).toEqual(["skill:allowed-bundled", "skill:managed-skill"]);
561
+
562
+ // Restore default config mock
563
+ mock.module("../config/loader.js", () => ({
564
+ loadConfig: () => TEST_CONFIG,
565
+ getConfig: () => TEST_CONFIG,
566
+ loadRawConfig: () => ({}),
567
+ saveRawConfig: () => {},
568
+ invalidateConfigCache: () => {},
569
+ }));
570
+ });
571
+
572
+ test("prunes stale capabilities for skills no longer enabled", () => {
385
573
  // First seed with three skills
386
- const initialSkills: CatalogSkill[] = [
387
- makeSkill({ id: "skill-a", name: "Skill A", description: "Does A" }),
388
- makeSkill({ id: "skill-b", name: "Skill B", description: "Does B" }),
389
- makeSkill({ id: "skill-c", name: "Skill C", description: "Does C" }),
574
+ const initialSkills: SkillSummary[] = [
575
+ makeSkillSummary({
576
+ id: "skill-a",
577
+ displayName: "Skill A",
578
+ description: "Does A",
579
+ }),
580
+ makeSkillSummary({
581
+ id: "skill-b",
582
+ displayName: "Skill B",
583
+ description: "Does B",
584
+ }),
585
+ makeSkillSummary({
586
+ id: "skill-c",
587
+ displayName: "Skill C",
588
+ description: "Does C",
589
+ }),
390
590
  ];
391
- mockResolveCatalog = async () => initialSkills;
392
- await seedCatalogSkillMemories();
591
+ mockLoadSkillCatalog = () => initialSkills;
592
+ seedCatalogSkillMemories();
393
593
 
394
594
  const db = getDb();
395
595
  const beforeItems = db
@@ -401,10 +601,14 @@ describe("seedCatalogSkillMemories", () => {
401
601
  expect(beforeItems.every((i) => i.status === "active")).toBe(true);
402
602
 
403
603
  // Now seed with only skill-a — skill-b and skill-c should be pruned
404
- mockResolveCatalog = async () => [
405
- makeSkill({ id: "skill-a", name: "Skill A", description: "Does A" }),
604
+ mockLoadSkillCatalog = () => [
605
+ makeSkillSummary({
606
+ id: "skill-a",
607
+ displayName: "Skill A",
608
+ description: "Does A",
609
+ }),
406
610
  ];
407
- await seedCatalogSkillMemories();
611
+ seedCatalogSkillMemories();
408
612
 
409
613
  const afterItems = db
410
614
  .select()
@@ -424,11 +628,11 @@ describe("seedCatalogSkillMemories", () => {
424
628
  expect(deletedSubjects).toEqual(["skill:skill-b", "skill:skill-c"]);
425
629
  });
426
630
 
427
- test("handles empty catalog without errors", async () => {
631
+ test("handles empty catalog without errors", () => {
428
632
  // Pre-populate a skill so we can verify it gets pruned
429
633
  upsertSkillCapabilityMemory(
430
634
  "existing-skill",
431
- makeSkill({ id: "existing-skill" }),
635
+ fromSkillSummary(makeSkillSummary({ id: "existing-skill" })),
432
636
  );
433
637
 
434
638
  const db = getDb();
@@ -437,8 +641,8 @@ describe("seedCatalogSkillMemories", () => {
437
641
  expect(beforeItems[0].status).toBe("active");
438
642
 
439
643
  // Seed with empty catalog
440
- mockResolveCatalog = async () => [];
441
- await seedCatalogSkillMemories();
644
+ mockLoadSkillCatalog = () => [];
645
+ seedCatalogSkillMemories();
442
646
 
443
647
  // The existing skill should be pruned (soft-deleted)
444
648
  const afterItems = db.select().from(memoryItems).all();
@@ -446,35 +650,69 @@ describe("seedCatalogSkillMemories", () => {
446
650
  expect(afterItems[0].status).toBe("deleted");
447
651
  });
448
652
 
449
- test("does not throw when resolveCatalog rejects", async () => {
450
- mockResolveCatalog = async () => {
451
- throw new Error("Network failure");
653
+ test("does not prune non-skill capability memories", () => {
654
+ // Pre-insert a non-skill capability memory directly into the DB
655
+ const db = getDb();
656
+ const now = Date.now();
657
+ db.insert(memoryItems)
658
+ .values({
659
+ id: "cli-doctor-item",
660
+ kind: "capability",
661
+ subject: "cli:doctor",
662
+ statement: "The doctor command diagnoses issues.",
663
+ status: "active",
664
+ confidence: 1.0,
665
+ importance: 0.7,
666
+ fingerprint: "cli-doctor-fp",
667
+ sourceType: "extraction",
668
+ scopeId: "default",
669
+ firstSeenAt: now,
670
+ lastSeenAt: now,
671
+ })
672
+ .run();
673
+
674
+ // Seed with empty catalog — skill pruner runs but should skip cli:* items
675
+ mockLoadSkillCatalog = () => [];
676
+ seedCatalogSkillMemories();
677
+
678
+ const item = db
679
+ .select()
680
+ .from(memoryItems)
681
+ .where(eq(memoryItems.subject, "cli:doctor"))
682
+ .get();
683
+ expect(item).toBeDefined();
684
+ expect(item!.status).toBe("active");
685
+ });
686
+
687
+ test("does not throw when loadSkillCatalog throws", () => {
688
+ mockLoadSkillCatalog = () => {
689
+ throw new Error("Catalog load failure");
452
690
  };
453
691
 
454
692
  // Best-effort: should not propagate the error
455
- await expect(seedCatalogSkillMemories()).resolves.toBeUndefined();
693
+ expect(() => seedCatalogSkillMemories()).not.toThrow();
456
694
  });
457
695
 
458
- test("skips skills whose feature flag is disabled", async () => {
459
- const skills: CatalogSkill[] = [
460
- makeSkill({
696
+ test("skips skills whose feature flag is disabled", () => {
697
+ const skills: SkillSummary[] = [
698
+ makeSkillSummary({
461
699
  id: "unflagged-skill",
462
- name: "Unflagged",
700
+ displayName: "Unflagged",
463
701
  description: "No flag",
464
702
  }),
465
- makeSkill({
703
+ makeSkillSummary({
466
704
  id: "flagged-skill",
467
- name: "Flagged",
705
+ displayName: "Flagged",
468
706
  description: "Has flag",
469
- metadata: { vellum: { "feature-flag": "my_gated_feature" } },
707
+ featureFlag: "my_gated_feature",
470
708
  }),
471
709
  ];
472
- mockResolveCatalog = async () => skills;
710
+ mockLoadSkillCatalog = () => skills;
473
711
 
474
712
  // Disable the feature flag for the flagged skill
475
713
  mockIsFeatureFlagEnabled = (key: string) => key !== "my_gated_feature";
476
714
 
477
- await seedCatalogSkillMemories();
715
+ seedCatalogSkillMemories();
478
716
 
479
717
  const db = getDb();
480
718
  const items = db
@@ -489,24 +727,24 @@ describe("seedCatalogSkillMemories", () => {
489
727
  expect(items[0].status).toBe("active");
490
728
  });
491
729
 
492
- test("prunes pre-existing capability for a skill whose flag becomes disabled", async () => {
730
+ test("prunes pre-existing capability for a skill whose flag becomes disabled", () => {
493
731
  // First seed with both skills, all flags enabled
494
- const skills: CatalogSkill[] = [
495
- makeSkill({
732
+ const skills: SkillSummary[] = [
733
+ makeSkillSummary({
496
734
  id: "unflagged-skill",
497
- name: "Unflagged",
735
+ displayName: "Unflagged",
498
736
  description: "No flag",
499
737
  }),
500
- makeSkill({
738
+ makeSkillSummary({
501
739
  id: "flagged-skill",
502
- name: "Flagged",
740
+ displayName: "Flagged",
503
741
  description: "Has flag",
504
- metadata: { vellum: { "feature-flag": "my_gated_feature" } },
742
+ featureFlag: "my_gated_feature",
505
743
  }),
506
744
  ];
507
- mockResolveCatalog = async () => skills;
745
+ mockLoadSkillCatalog = () => skills;
508
746
  mockIsFeatureFlagEnabled = () => true;
509
- await seedCatalogSkillMemories();
747
+ seedCatalogSkillMemories();
510
748
 
511
749
  const db = getDb();
512
750
  const beforeItems = db
@@ -519,7 +757,7 @@ describe("seedCatalogSkillMemories", () => {
519
757
 
520
758
  // Now disable the flag — the flagged skill should be pruned
521
759
  mockIsFeatureFlagEnabled = (key: string) => key !== "my_gated_feature";
522
- await seedCatalogSkillMemories();
760
+ seedCatalogSkillMemories();
523
761
 
524
762
  const afterItems = db
525
763
  .select()
@@ -538,9 +776,13 @@ describe("seedCatalogSkillMemories", () => {
538
776
  expect(deleted[0].subject).toBe("skill:flagged-skill");
539
777
  });
540
778
 
541
- test("does not throw on DB error during pruning", async () => {
542
- mockResolveCatalog = async () => [
543
- makeSkill({ id: "skill-a", name: "Skill A", description: "Does A" }),
779
+ test("does not throw on DB error during pruning", () => {
780
+ mockLoadSkillCatalog = () => [
781
+ makeSkillSummary({
782
+ id: "skill-a",
783
+ displayName: "Skill A",
784
+ description: "Does A",
785
+ }),
544
786
  ];
545
787
 
546
788
  // Drop memory_items to force a DB error during the prune phase
@@ -548,13 +790,14 @@ describe("seedCatalogSkillMemories", () => {
548
790
  const db = getDb();
549
791
  db.run("DROP TABLE IF EXISTS memory_items");
550
792
 
551
- await expect(seedCatalogSkillMemories()).resolves.toBeUndefined();
793
+ expect(() => seedCatalogSkillMemories()).not.toThrow();
552
794
 
553
795
  // Restore DB state for subsequent tests (see upsert "does not throw" test
554
796
  // for rationale on why we delete the DB file).
555
797
  resetDb();
798
+ const dbPath = getDbPath();
556
799
  for (const ext of ["", "-wal", "-shm"]) {
557
- rmSync(join(testDir, `test.db${ext}`), { force: true });
800
+ rmSync(`${dbPath}${ext}`, { force: true });
558
801
  }
559
802
  initializeDb();
560
803
  });