@vellumai/assistant 0.10.9-staging.1 → 0.10.9

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 (100) hide show
  1. package/AGENTS.md +4 -0
  2. package/knip.json +1 -4
  3. package/openapi.yaml +27 -17
  4. package/package.json +1 -1
  5. package/src/__tests__/always-loaded-tools-guard.test.ts +4 -6
  6. package/src/__tests__/app-routes-csp.test.ts +13 -4
  7. package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +4 -8
  8. package/src/__tests__/disk-pressure-tools.test.ts +4 -4
  9. package/src/__tests__/file-list-tool.test.ts +22 -12
  10. package/src/__tests__/list-all-apps.test.ts +204 -0
  11. package/src/__tests__/messaging-send-tool.test.ts +67 -0
  12. package/src/__tests__/mtime-cache.test.ts +42 -0
  13. package/src/__tests__/oauth-provider-profiles.test.ts +3 -2
  14. package/src/__tests__/outlook-messaging-provider.test.ts +71 -0
  15. package/src/__tests__/plugin-app-serve-routes.test.ts +163 -0
  16. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
  17. package/src/__tests__/scaffold-managed-skill-tool.test.ts +47 -0
  18. package/src/__tests__/subagent-notify-parent.test.ts +4 -3
  19. package/src/__tests__/subagent-tool-filtering.test.ts +21 -21
  20. package/src/__tests__/subagent-tool-gate-mode.test.ts +6 -6
  21. package/src/apps/app-store.ts +302 -2
  22. package/src/cli/AGENTS.md +17 -0
  23. package/src/cli/commands/__tests__/gateway.test.ts +55 -0
  24. package/src/cli/commands/apps.help.ts +17 -11
  25. package/src/cli/commands/apps.ts +5 -15
  26. package/src/cli/commands/gateway.help.ts +30 -0
  27. package/src/cli/commands/gateway.ts +40 -1
  28. package/src/cli/commands/platform/__tests__/status.test.ts +0 -90
  29. package/src/cli/commands/platform/index.help.ts +3 -2
  30. package/src/cli/commands/platform/index.ts +0 -9
  31. package/src/cli/commands/routes.help.ts +13 -7
  32. package/src/cli/commands/routes.ts +5 -3
  33. package/src/cli/commands/status.help.ts +24 -0
  34. package/src/cli/commands/status.ts +29 -0
  35. package/src/cli/lib/__tests__/global-json-option.test.ts +97 -0
  36. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +1 -0
  37. package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +31 -0
  38. package/src/cli/lib/bundled-marketplace.json +187 -173
  39. package/src/cli/lib/global-json-option.ts +41 -0
  40. package/src/cli/lib/plugin-fingerprint.ts +18 -7
  41. package/src/cli/program.ts +5 -0
  42. package/src/config/bundled-skills/messaging/TOOLS.json +2 -2
  43. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +27 -19
  44. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
  45. package/src/config/feature-flag-registry.json +1 -9
  46. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +3 -6
  47. package/src/daemon/__tests__/conversation-tool-setup-plugin-scope.test.ts +3 -6
  48. package/src/daemon/__tests__/conversation-tool-setup.test.ts +8 -5
  49. package/src/daemon/conversation-tool-setup.ts +5 -90
  50. package/src/daemon/conversation.ts +2 -2
  51. package/src/messaging/provider-types.ts +9 -0
  52. package/src/messaging/providers/outlook/adapter.ts +17 -2
  53. package/src/messaging/providers/outlook/client.ts +11 -2
  54. package/src/messaging/providers/outlook/types.ts +9 -0
  55. package/src/monitoring/__tests__/plugin-source-watch.test.ts +37 -0
  56. package/src/monitoring/plugin-source-watch.ts +53 -0
  57. package/src/oauth/seed-providers.ts +6 -0
  58. package/src/persistence/embeddings/__tests__/messages-lexical-index.test.ts +3 -0
  59. package/src/persistence/embeddings/messages-lexical-index.ts +11 -0
  60. package/src/plugin-api/conversation-turn.ts +234 -0
  61. package/src/plugin-api/index.ts +12 -0
  62. package/src/plugins/__tests__/source-fingerprint.test.ts +41 -0
  63. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +48 -0
  64. package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +46 -5
  65. package/src/plugins/defaults/memory/graph/tool-handlers.ts +2 -2
  66. package/src/plugins/defaults/memory/graph-topology/__tests__/build-memory-graph.test.ts +81 -2
  67. package/src/plugins/defaults/memory/graph-topology/build-memory-graph.ts +80 -33
  68. package/src/plugins/defaults/memory/memory-retrospective-job.ts +40 -1
  69. package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +28 -2
  70. package/src/plugins/mtime-cache.ts +26 -13
  71. package/src/plugins/plugin-tree-walk.ts +30 -0
  72. package/src/plugins/source-fingerprint.ts +9 -2
  73. package/src/plugins/surface-import.ts +28 -0
  74. package/src/providers/speech-to-text/__tests__/vellum-managed-realtime.test.ts +1 -1
  75. package/src/providers/speech-to-text/__tests__/vellum-speech-relay-connection.test.ts +4 -1
  76. package/src/providers/speech-to-text/vellum-speech-relay-connection.ts +5 -0
  77. package/src/runtime/routes/__tests__/gateway-status-routes.test.ts +84 -0
  78. package/src/runtime/routes/__tests__/plugins-routes.test.ts +47 -3
  79. package/src/runtime/routes/__tests__/user-routes-cli.test.ts +154 -0
  80. package/src/runtime/routes/app-management-routes.ts +96 -32
  81. package/src/runtime/routes/app-routes.ts +33 -17
  82. package/src/runtime/routes/gateway-status-routes.ts +69 -0
  83. package/src/runtime/routes/index.ts +2 -0
  84. package/src/runtime/routes/platform-routes.ts +4 -14
  85. package/src/runtime/routes/plugins-routes.ts +58 -1
  86. package/src/runtime/routes/settings-routes.ts +8 -6
  87. package/src/runtime/routes/user-route-dispatcher.ts +6 -86
  88. package/src/runtime/routes/user-route-resolution.ts +141 -0
  89. package/src/runtime/routes/user-routes-cli.ts +56 -35
  90. package/src/telemetry/AGENTS.md +15 -0
  91. package/src/telemetry/__tests__/telemetry-event-fixtures.ts +258 -0
  92. package/src/telemetry/telemetry-wire-source.json +1 -1
  93. package/src/telemetry/telemetry-wire-validation.test.ts +157 -0
  94. package/src/telemetry/telemetry-wire-validation.ts +125 -0
  95. package/src/telemetry/telemetry-wire.generated.ts +60 -9
  96. package/src/telemetry/types.test.ts +38 -0
  97. package/src/telemetry/types.ts +183 -91
  98. package/src/telemetry/usage-telemetry-reporter.ts +5 -0
  99. package/src/telemetry/watchdog-direct-emit.ts +6 -0
  100. package/src/tools/skills/scaffold-managed.ts +43 -11
@@ -23,7 +23,7 @@ describe("oauth provider profiles (DB-seeded)", () => {
23
23
  );
24
24
  });
25
25
 
26
- test("google provider row contains bearer injection templates for 3 Google API hosts", () => {
26
+ test("google provider row contains bearer injection templates for 4 Google API hosts", () => {
27
27
  const provider = getProvider("google");
28
28
 
29
29
  expect(provider).toBeDefined();
@@ -36,7 +36,7 @@ describe("oauth provider profiles (DB-seeded)", () => {
36
36
  valuePrefix: string;
37
37
  }>;
38
38
 
39
- expect(templates).toHaveLength(3);
39
+ expect(templates).toHaveLength(4);
40
40
 
41
41
  const byHost = new Map(templates.map((t) => [t.hostPattern, t]));
42
42
 
@@ -44,6 +44,7 @@ describe("oauth provider profiles (DB-seeded)", () => {
44
44
  "gmail.googleapis.com",
45
45
  "www.googleapis.com",
46
46
  "people.googleapis.com",
47
+ "docs.googleapis.com",
47
48
  ]) {
48
49
  const tpl = byHost.get(host);
49
50
  expect(tpl).toBeDefined();
@@ -473,6 +473,7 @@ describe("Outlook messaging provider", () => {
473
473
  conn,
474
474
  "original-msg-id",
475
475
  "Reply text",
476
+ undefined,
476
477
  );
477
478
  expect(mockSendMessage).not.toHaveBeenCalled();
478
479
  expect(result).toMatchObject({
@@ -481,6 +482,76 @@ describe("Outlook messaging provider", () => {
481
482
  });
482
483
  });
483
484
 
485
+ test("attaches files to a new message as Graph file attachments", async () => {
486
+ const conn = createMockConnection();
487
+ await outlookMessagingProvider.sendMessage(
488
+ conn,
489
+ "recipient@example.com",
490
+ "See attached",
491
+ {
492
+ subject: "Docs",
493
+ attachments: [
494
+ {
495
+ filename: "report.pdf",
496
+ mimeType: "application/pdf",
497
+ data: Buffer.from("pdf-bytes"),
498
+ },
499
+ ],
500
+ },
501
+ );
502
+
503
+ expect(mockSendMessage).toHaveBeenCalledWith(conn, {
504
+ message: {
505
+ subject: "Docs",
506
+ body: { contentType: "text", content: "See attached" },
507
+ toRecipients: [
508
+ { emailAddress: { address: "recipient@example.com" } },
509
+ ],
510
+ attachments: [
511
+ {
512
+ "@odata.type": "#microsoft.graph.fileAttachment",
513
+ name: "report.pdf",
514
+ contentType: "application/pdf",
515
+ contentBytes: Buffer.from("pdf-bytes").toString("base64"),
516
+ },
517
+ ],
518
+ },
519
+ });
520
+ });
521
+
522
+ test("forwards attachments to replyToMessage when replying", async () => {
523
+ const conn = createMockConnection();
524
+ await outlookMessagingProvider.sendMessage(
525
+ conn,
526
+ "conv-id",
527
+ "Reply body",
528
+ {
529
+ inReplyTo: "original-msg-id",
530
+ attachments: [
531
+ {
532
+ filename: "report.pdf",
533
+ mimeType: "application/pdf",
534
+ data: Buffer.from("pdf-bytes"),
535
+ },
536
+ ],
537
+ },
538
+ );
539
+
540
+ expect(mockReplyToMessage).toHaveBeenCalledWith(
541
+ conn,
542
+ "original-msg-id",
543
+ "Reply body",
544
+ [
545
+ {
546
+ "@odata.type": "#microsoft.graph.fileAttachment",
547
+ name: "report.pdf",
548
+ contentType: "application/pdf",
549
+ contentBytes: Buffer.from("pdf-bytes").toString("base64"),
550
+ },
551
+ ],
552
+ );
553
+ });
554
+
484
555
  test("throws when connection is undefined", async () => {
485
556
  await expect(
486
557
  outlookMessagingProvider.sendMessage(
@@ -0,0 +1,163 @@
1
+ import { mkdirSync, 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 { ROUTES as APP_MGMT_ROUTES } from "../runtime/routes/app-management-routes.js";
7
+ import { ROUTES as APP_ROUTES } from "../runtime/routes/app-routes.js";
8
+ import { BadRequestError, NotFoundError } from "../runtime/routes/errors.js";
9
+ import type {
10
+ ResponseHeaderArgs,
11
+ RouteDefinition,
12
+ RouteHandlerArgs,
13
+ } from "../runtime/routes/types.js";
14
+ import { getWorkspacePluginsDir } from "../util/platform.js";
15
+
16
+ let workspaceDir: string;
17
+
18
+ function findRoute(routes: RouteDefinition[], operationId: string) {
19
+ const route = routes.find((r) => r.operationId === operationId);
20
+ if (!route) {
21
+ throw new Error(`Route not found: ${operationId}`);
22
+ }
23
+ return route;
24
+ }
25
+
26
+ /** Install a plugin with a package.json and one bundled app directory. */
27
+ function installPluginApp(
28
+ plugin: string,
29
+ app: string,
30
+ files: Record<string, string>,
31
+ ): void {
32
+ const appDir = join(getWorkspacePluginsDir(), plugin, "apps", app);
33
+ mkdirSync(appDir, { recursive: true });
34
+ writeFileSync(
35
+ join(getWorkspacePluginsDir(), plugin, "package.json"),
36
+ JSON.stringify({ name: plugin, version: "1.0.0" }),
37
+ );
38
+ for (const [rel, content] of Object.entries(files)) {
39
+ const full = join(appDir, rel);
40
+ mkdirSync(join(full, ".."), { recursive: true });
41
+ writeFileSync(full, content);
42
+ }
43
+ }
44
+
45
+ beforeEach(() => {
46
+ workspaceDir = join(
47
+ tmpdir(),
48
+ `vellum-plugin-serve-${Date.now()}-${Math.random().toString(36).slice(2)}`,
49
+ );
50
+ process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
51
+ });
52
+
53
+ afterEach(() => {
54
+ rmSync(workspaceDir, { recursive: true, force: true });
55
+ });
56
+
57
+ describe("plugin app serve routes", () => {
58
+ test("pages_serve renders a single-file plugin app's index.html", () => {
59
+ installPluginApp("acme", "dash", {
60
+ "index.html": "<div id='root'>Plugin Home</div>",
61
+ });
62
+ const handler = findRoute(APP_ROUTES, "pages_serve").handler;
63
+ const html = handler({
64
+ pathParams: { appId: "plugins~acme~dash" },
65
+ } as RouteHandlerArgs) as string;
66
+
67
+ expect(html).toContain("Plugin Home");
68
+ expect(html).toContain("<!DOCTYPE html>");
69
+ });
70
+
71
+ test("pages_serve 404s for an unknown / non-plugin id", () => {
72
+ const handler = findRoute(APP_ROUTES, "pages_serve").handler;
73
+ expect(() =>
74
+ handler({
75
+ pathParams: { appId: "plugins~acme~missing" },
76
+ } as RouteHandlerArgs),
77
+ ).toThrow(NotFoundError);
78
+ });
79
+
80
+ test("servePageHeaders CSP: single-file allows inline scripts, multifile does not", () => {
81
+ installPluginApp("acme", "single", {
82
+ "index.html": "<div>x</div>",
83
+ });
84
+ installPluginApp("acme", "multi", {
85
+ "dist/index.html": "<script src='main.js'></script>",
86
+ });
87
+ const route = findRoute(APP_ROUTES, "pages_serve");
88
+ const headersFn = route.responseHeaders as (
89
+ args: ResponseHeaderArgs,
90
+ ) => Record<string, string>;
91
+
92
+ const single = headersFn({ pathParams: { appId: "plugins~acme~single" } });
93
+ const multi = headersFn({ pathParams: { appId: "plugins~acme~multi" } });
94
+
95
+ expect(single["Content-Security-Policy"]).toContain(
96
+ "script-src 'self' 'unsafe-inline'",
97
+ );
98
+ expect(multi["Content-Security-Policy"]).toContain("script-src 'self';");
99
+ expect(multi["Content-Security-Policy"]).not.toContain(
100
+ "script-src 'self' 'unsafe-inline'",
101
+ );
102
+ });
103
+
104
+ test("apps_asset serves a bundled asset from a plugin app dir", () => {
105
+ installPluginApp("acme", "dash", {
106
+ "index.html": "<div>x</div>",
107
+ "logo.svg": "<svg>logo</svg>",
108
+ });
109
+ const handler = findRoute(APP_ROUTES, "apps_asset").handler;
110
+ const bytes = handler({
111
+ pathParams: { appId: "plugins~acme~dash", path: "logo.svg" },
112
+ } as RouteHandlerArgs) as Uint8Array;
113
+
114
+ expect(Buffer.from(bytes).toString("utf-8")).toBe("<svg>logo</svg>");
115
+ });
116
+
117
+ test("apps_asset rejects traversal out of a plugin app dir", () => {
118
+ installPluginApp("acme", "dash", { "index.html": "<div>x</div>" });
119
+ const handler = findRoute(APP_ROUTES, "apps_asset").handler;
120
+ expect(() =>
121
+ handler({
122
+ pathParams: { appId: "plugins~acme~dash", path: "../../package.json" },
123
+ } as RouteHandlerArgs),
124
+ ).toThrow(BadRequestError);
125
+ });
126
+
127
+ test("apps_dist_file serves a compiled asset from a plugin app dist/", () => {
128
+ installPluginApp("acme", "multi", {
129
+ "dist/index.html": "<html></html>",
130
+ "dist/main.js": "console.log(1)",
131
+ });
132
+ const handler = findRoute(APP_ROUTES, "apps_dist_file").handler;
133
+ const bytes = handler({
134
+ pathParams: { appId: "plugins~acme~multi", filename: "main.js" },
135
+ } as RouteHandlerArgs) as Uint8Array;
136
+
137
+ expect(Buffer.from(bytes).toString("utf-8")).toBe("console.log(1)");
138
+ });
139
+ });
140
+
141
+ describe("plugin apps are read-only over the management surface", () => {
142
+ test("apps_delete rejects a plugin app id", () => {
143
+ installPluginApp("acme", "dash", { "index.html": "<div>x</div>" });
144
+ const handler = findRoute(APP_MGMT_ROUTES, "apps_delete").handler;
145
+ expect(() =>
146
+ handler({
147
+ pathParams: { id: "plugins~acme~dash" },
148
+ headers: {},
149
+ } as RouteHandlerArgs),
150
+ ).toThrow(BadRequestError);
151
+ });
152
+
153
+ test("apps_data_mutate rejects a plugin app id", () => {
154
+ installPluginApp("acme", "dash", { "index.html": "<div>x</div>" });
155
+ const handler = findRoute(APP_MGMT_ROUTES, "apps_data_mutate").handler;
156
+ expect(() =>
157
+ handler({
158
+ pathParams: { id: "plugins~acme~dash" },
159
+ body: { method: "create", data: {} },
160
+ } as RouteHandlerArgs),
161
+ ).toThrow(BadRequestError);
162
+ });
163
+ });
@@ -191,6 +191,7 @@ const BASELINE: Record<string, readonly string[]> = {
191
191
  "../../../runtime/capabilities.js",
192
192
  "../../../runtime/services/auto-analysis-guard.js",
193
193
  "../../../runtime/sync/resource-sync-events.js",
194
+ "../../../telemetry/watchdog-events-store.js",
194
195
  "../../../tools/registry.js",
195
196
  "../../../tools/types.js",
196
197
  "../../../types.js",
@@ -13,6 +13,21 @@ import type { SkillSource } from "../config/skills.js";
13
13
  const TEST_DIR = process.env.VELLUM_WORKSPACE_DIR!;
14
14
  const mockRefreshSkillCapabilityMemories = mock(() => {});
15
15
 
16
+ const watchdogEvents: Array<{
17
+ checkName: string;
18
+ value?: number | null;
19
+ detail?: Record<string, unknown> | null;
20
+ }> = [];
21
+ mock.module("../telemetry/watchdog-events-store.js", () => ({
22
+ recordWatchdogEvent: (record: {
23
+ checkName: string;
24
+ value?: number | null;
25
+ detail?: Record<string, unknown> | null;
26
+ }) => {
27
+ watchdogEvents.push(record);
28
+ },
29
+ }));
30
+
16
31
  mock.module("../daemon/skill-memory-refresh.js", () => ({
17
32
  refreshSkillCapabilityMemories: mockRefreshSkillCapabilityMemories,
18
33
  }));
@@ -88,6 +103,7 @@ beforeEach(() => {
88
103
  mockRefreshSkillCapabilityMemories.mockClear();
89
104
  skillCardJobUpserts = [];
90
105
  skillCardUpsertThrows = false;
106
+ watchdogEvents.length = 0;
91
107
  });
92
108
 
93
109
  afterEach(() => {
@@ -146,6 +162,16 @@ describe("scaffold_managed_skill tool", () => {
146
162
  expect(skill).toBeDefined();
147
163
  expect(skill!.name).toBe("Test Skill");
148
164
  expect(mockRefreshSkillCapabilityMemories).toHaveBeenCalledTimes(1);
165
+
166
+ // A genuine create emits the central authoring counter, attributed to
167
+ // the user for a non-retrospective origin.
168
+ expect(watchdogEvents).toEqual([
169
+ {
170
+ checkName: "skill_authored",
171
+ value: 1,
172
+ detail: { authored_by: "user" },
173
+ },
174
+ ]);
149
175
  });
150
176
 
151
177
  test("accepts legacy add_to_index input without returning index metadata", async () => {
@@ -202,6 +228,12 @@ describe("scaffold_managed_skill tool", () => {
202
228
  makeContext(),
203
229
  );
204
230
  expect(result3.isError).toBe(false);
231
+
232
+ // Only the original create counts — the overwrite refined an existing
233
+ // skill and must not emit a second authoring event.
234
+ expect(
235
+ watchdogEvents.filter((e) => e.checkName === "skill_authored"),
236
+ ).toHaveLength(1);
205
237
  });
206
238
 
207
239
  test("rejects missing required fields", async () => {
@@ -691,6 +723,15 @@ describe("scaffold_managed_skill tool", () => {
691
723
 
692
724
  expect(result.isError).toBe(false);
693
725
  expect(installMetaFor("retro-skill")?.author).toBe("assistant");
726
+ // The central authoring counter attributes the create to the
727
+ // retrospective.
728
+ expect(watchdogEvents).toEqual([
729
+ {
730
+ checkName: "skill_authored",
731
+ value: 1,
732
+ detail: { authored_by: "retrospective" },
733
+ },
734
+ ]);
694
735
  });
695
736
 
696
737
  test('tags author "user" for a normal (non-retrospective) scaffold', async () => {
@@ -851,6 +892,12 @@ describe("scaffold_managed_skill tool", () => {
851
892
  ),
852
893
  ),
853
894
  ).toBe(true);
895
+
896
+ // Only the V1 create counts toward authoring — the V2 refinement of an
897
+ // existing skill emits no second event.
898
+ expect(
899
+ watchdogEvents.filter((e) => e.checkName === "skill_authored"),
900
+ ).toHaveLength(1);
854
901
  });
855
902
 
856
903
  // ── Conversation lineage (retrospective-authored skills) ───────────────────
@@ -79,6 +79,7 @@ mock.module("../runtime/assistant-event-hub.js", () => ({
79
79
  broadcastMessage: () => {},
80
80
  }));
81
81
 
82
+ import type { Conversation } from "../daemon/conversation.js";
82
83
  import { isToolActiveForContext } from "../daemon/conversation-tool-setup.js";
83
84
  import type { SubagentRecord } from "../persistence/subagent-store.js";
84
85
  import { notifyParentFromChild } from "../subagent/notify.js";
@@ -171,7 +172,7 @@ describe("notify_parent tool definition", () => {
171
172
  skillProjectionState: new Map(),
172
173
  skillProjectionCache: new Map(),
173
174
  toolsDisabledDepth: 0,
174
- } as unknown as import("../daemon/conversation-tool-setup.js").SkillProjectionContext;
175
+ } as unknown as Conversation;
175
176
  expect(isToolActiveForContext("notify_parent", ctx)).toBe(false);
176
177
  });
177
178
 
@@ -181,7 +182,7 @@ describe("notify_parent tool definition", () => {
181
182
  skillProjectionState: new Map(),
182
183
  skillProjectionCache: new Map(),
183
184
  toolsDisabledDepth: 0,
184
- } as unknown as import("../daemon/conversation-tool-setup.js").SkillProjectionContext;
185
+ } as unknown as Conversation;
185
186
  expect(isToolActiveForContext("notify_parent", ctx)).toBe(false);
186
187
  });
187
188
 
@@ -192,7 +193,7 @@ describe("notify_parent tool definition", () => {
192
193
  skillProjectionState: new Map(),
193
194
  skillProjectionCache: new Map(),
194
195
  toolsDisabledDepth: 0,
195
- } as unknown as import("../daemon/conversation-tool-setup.js").SkillProjectionContext;
196
+ } as unknown as Conversation;
196
197
  expect(isToolActiveForContext("notify_parent", ctx)).toBe(true);
197
198
  });
198
199
  });
@@ -1,8 +1,8 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
 
3
+ import type { Conversation } from "../daemon/conversation.js";
3
4
  import {
4
5
  isToolActiveForContext,
5
- type SkillProjectionContext,
6
6
  SUBAGENT_ONLY_TOOL_NAMES,
7
7
  } from "../daemon/conversation-tool-setup.js";
8
8
 
@@ -18,48 +18,48 @@ describe("subagent-only tool filtering", () => {
18
18
  });
19
19
 
20
20
  test("hides subagent-only tools from main conversations (isSubagent=false)", () => {
21
- const ctx: SkillProjectionContext = {
21
+ const ctx = {
22
22
  skillProjectionState: new Map(),
23
23
  skillProjectionCache: {},
24
24
  toolsDisabledDepth: 0,
25
25
  hasNoClient: false,
26
26
  isSubagent: false,
27
- };
27
+ } as unknown as Conversation;
28
28
 
29
29
  expect(isToolActiveForContext(TEST_TOOL_NAME, ctx)).toBe(false);
30
30
  });
31
31
 
32
32
  test("hides subagent-only tools when isSubagent is undefined", () => {
33
- const ctx: SkillProjectionContext = {
33
+ const ctx = {
34
34
  skillProjectionState: new Map(),
35
35
  skillProjectionCache: {},
36
36
  toolsDisabledDepth: 0,
37
37
  hasNoClient: false,
38
- };
38
+ } as unknown as Conversation;
39
39
 
40
40
  expect(isToolActiveForContext(TEST_TOOL_NAME, ctx)).toBe(false);
41
41
  });
42
42
 
43
43
  test("shows subagent-only tools to subagent conversations (isSubagent=true)", () => {
44
- const ctx: SkillProjectionContext = {
44
+ const ctx = {
45
45
  skillProjectionState: new Map(),
46
46
  skillProjectionCache: {},
47
47
  toolsDisabledDepth: 0,
48
48
  hasNoClient: true,
49
49
  isSubagent: true,
50
- };
50
+ } as unknown as Conversation;
51
51
 
52
52
  expect(isToolActiveForContext(TEST_TOOL_NAME, ctx)).toBe(true);
53
53
  });
54
54
 
55
55
  test("does not affect regular tools when isSubagent is false", () => {
56
- const ctx: SkillProjectionContext = {
56
+ const ctx = {
57
57
  skillProjectionState: new Map(),
58
58
  skillProjectionCache: {},
59
59
  toolsDisabledDepth: 0,
60
60
  hasNoClient: false,
61
61
  isSubagent: false,
62
- };
62
+ } as unknown as Conversation;
63
63
 
64
64
  // A regular tool not in SUBAGENT_ONLY_TOOL_NAMES should still be active
65
65
  expect(isToolActiveForContext("bash", ctx)).toBe(true);
@@ -68,14 +68,14 @@ describe("subagent-only tool filtering", () => {
68
68
  test("respects subagentAllowedTools — tools outside the allowlist are inactive", () => {
69
69
  // Mirrors `createResolveToolsCallback`'s post-filter so callers see the
70
70
  // same final tool set the LLM receives.
71
- const ctx: SkillProjectionContext = {
71
+ const ctx = {
72
72
  skillProjectionState: new Map(),
73
73
  skillProjectionCache: {},
74
74
  toolsDisabledDepth: 0,
75
75
  hasNoClient: false,
76
76
  isSubagent: true,
77
77
  subagentAllowedTools: new Set(["bash"]),
78
- };
78
+ } as unknown as Conversation;
79
79
 
80
80
  expect(isToolActiveForContext("bash", ctx)).toBe(true);
81
81
  expect(isToolActiveForContext("ask_question", ctx)).toBe(false);
@@ -86,28 +86,28 @@ describe("subagent-only tool filtering", () => {
86
86
  // `subagentToolGateMode: "execution"` keeps the full tool surface on the
87
87
  // wire for provider prompt-cache parity; the executor callback rejects
88
88
  // non-allowlisted calls instead.
89
- const ctx: SkillProjectionContext = {
89
+ const ctx = {
90
90
  skillProjectionState: new Map(),
91
91
  skillProjectionCache: {},
92
92
  toolsDisabledDepth: 0,
93
93
  hasNoClient: false,
94
94
  subagentAllowedTools: new Set(["remember"]),
95
95
  subagentToolGateMode: "execution",
96
- };
96
+ } as unknown as Conversation;
97
97
 
98
98
  expect(isToolActiveForContext("remember", ctx)).toBe(true);
99
99
  expect(isToolActiveForContext("bash", ctx)).toBe(true);
100
100
  });
101
101
 
102
102
  test("explicit wire gate mode filters exactly like the absent default", () => {
103
- const ctx: SkillProjectionContext = {
103
+ const ctx = {
104
104
  skillProjectionState: new Map(),
105
105
  skillProjectionCache: {},
106
106
  toolsDisabledDepth: 0,
107
107
  hasNoClient: false,
108
108
  subagentAllowedTools: new Set(["remember"]),
109
109
  subagentToolGateMode: "wire",
110
- };
110
+ } as unknown as Conversation;
111
111
 
112
112
  expect(isToolActiveForContext("remember", ctx)).toBe(true);
113
113
  expect(isToolActiveForContext("bash", ctx)).toBe(false);
@@ -116,14 +116,14 @@ describe("subagent-only tool filtering", () => {
116
116
  test("execution gate mode still applies non-allowlist filters (toolsDisabledDepth)", () => {
117
117
  // The execution gate only bypasses the subagent allowlist's wire filter;
118
118
  // every other visibility rule still applies.
119
- const ctx: SkillProjectionContext = {
119
+ const ctx = {
120
120
  skillProjectionState: new Map(),
121
121
  skillProjectionCache: {},
122
122
  toolsDisabledDepth: 1,
123
123
  hasNoClient: false,
124
124
  subagentAllowedTools: new Set(["remember"]),
125
125
  subagentToolGateMode: "execution",
126
- };
126
+ } as unknown as Conversation;
127
127
 
128
128
  expect(isToolActiveForContext("remember", ctx)).toBe(false);
129
129
  expect(isToolActiveForContext("bash", ctx)).toBe(false);
@@ -132,12 +132,12 @@ describe("subagent-only tool filtering", () => {
132
132
  test("returns false for every tool when toolsDisabledDepth > 0", () => {
133
133
  // `createResolveToolsCallback` returns an empty tool list when tools are
134
134
  // disabled; mirror it here so this helper reports the same final tool set.
135
- const ctx: SkillProjectionContext = {
135
+ const ctx = {
136
136
  skillProjectionState: new Map(),
137
137
  skillProjectionCache: {},
138
138
  toolsDisabledDepth: 1,
139
139
  hasNoClient: false,
140
- };
140
+ } as unknown as Conversation;
141
141
 
142
142
  expect(isToolActiveForContext("bash", ctx)).toBe(false);
143
143
  expect(isToolActiveForContext("ask_question", ctx)).toBe(false);
@@ -146,13 +146,13 @@ describe("subagent-only tool filtering", () => {
146
146
  test("under disk-pressure cleanup mode, only cleanup-safe tools are active", () => {
147
147
  // `createResolveToolsCallback` restricts the turn to cleanup-safe tools
148
148
  // (`file_remove`, `bash`, etc.); ensure the helper agrees.
149
- const ctx: SkillProjectionContext = {
149
+ const ctx = {
150
150
  skillProjectionState: new Map(),
151
151
  skillProjectionCache: {},
152
152
  toolsDisabledDepth: 0,
153
153
  hasNoClient: false,
154
154
  diskPressureCleanupModeActive: true,
155
- };
155
+ } as unknown as Conversation;
156
156
 
157
157
  // `bash` is in DISK_PRESSURE_CLEANUP_TOOL_NAMES; `ask_question` is not.
158
158
  expect(isToolActiveForContext("bash", ctx)).toBe(true);
@@ -72,10 +72,10 @@ mock.module("../daemon/conversation-skill-tools.js", () => ({
72
72
  // Imports after mocks are in place
73
73
  // ---------------------------------------------------------------------------
74
74
 
75
+ import type { Conversation } from "../daemon/conversation.js";
75
76
  import {
76
77
  createResolveToolsCallback,
77
78
  createToolExecutor,
78
- type SkillProjectionContext,
79
79
  type ToolSetupContext,
80
80
  } from "../daemon/conversation-tool-setup.js";
81
81
  import {
@@ -95,14 +95,14 @@ function makeToolDef(name: string): ToolDefinition {
95
95
  }
96
96
 
97
97
  function makeProjectionCtx(
98
- overrides: Partial<SkillProjectionContext> = {},
99
- ): SkillProjectionContext {
98
+ overrides: Partial<Conversation> = {},
99
+ ): Conversation {
100
100
  return {
101
101
  skillProjectionState: new Map(),
102
102
  skillProjectionCache: {} as SkillProjectionCache,
103
103
  toolsDisabledDepth: 0,
104
104
  ...overrides,
105
- };
105
+ } as unknown as Conversation;
106
106
  }
107
107
 
108
108
  function makeSetupCtx(
@@ -237,8 +237,8 @@ describe("createResolveToolsCallback — toolContextPin", () => {
237
237
 
238
238
  /** Execution-gate ctx shaped like a clientless fork-retrospective wake. */
239
239
  function clientlessExecutionCtx(
240
- overrides: Partial<SkillProjectionContext> = {},
241
- ): SkillProjectionContext {
240
+ overrides: Partial<Conversation> = {},
241
+ ): Conversation {
242
242
  return makeProjectionCtx({
243
243
  hasNoClient: true,
244
244
  subagentAllowedTools: new Set(["remember"]),