@vellumai/assistant 0.8.11 → 0.8.12-staging.1

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 (219) hide show
  1. package/ARCHITECTURE.md +15 -17
  2. package/README.md +0 -6
  3. package/bun.lock +6 -122
  4. package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
  5. package/node_modules/@vellumai/gateway-client/package.json +3 -1
  6. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
  7. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
  8. package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
  9. package/openapi.yaml +126 -4
  10. package/package.json +1 -3
  11. package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
  12. package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
  13. package/src/__tests__/anthropic-provider.test.ts +129 -0
  14. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
  15. package/src/__tests__/btw-routes.test.ts +7 -34
  16. package/src/__tests__/checker.test.ts +6 -12
  17. package/src/__tests__/config-loader-backfill.test.ts +4 -2
  18. package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
  19. package/src/__tests__/config-watcher.test.ts +2 -2
  20. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  21. package/src/__tests__/conversation-error.test.ts +2 -6
  22. package/src/__tests__/conversation-history-web-search.test.ts +8 -0
  23. package/src/__tests__/conversation-title-service.test.ts +2 -1
  24. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  25. package/src/__tests__/disk-pressure-tools.test.ts +1 -1
  26. package/src/__tests__/exploration-drift-hook.test.ts +692 -0
  27. package/src/__tests__/filing-service.test.ts +8 -3
  28. package/src/__tests__/guardian-action-store.test.ts +0 -167
  29. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
  30. package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
  31. package/src/__tests__/heartbeat-service.test.ts +5 -2
  32. package/src/__tests__/identity-intro-cache.test.ts +12 -5
  33. package/src/__tests__/identity-routes.test.ts +16 -57
  34. package/src/__tests__/injector-chain.test.ts +8 -3
  35. package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
  36. package/src/__tests__/llm-usage-store.test.ts +11 -0
  37. package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
  38. package/src/__tests__/model-intents.test.ts +1 -1
  39. package/src/__tests__/oauth-cli.test.ts +19 -8
  40. package/src/__tests__/openai-provider.test.ts +34 -0
  41. package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
  42. package/src/__tests__/recurrence-engine.test.ts +45 -0
  43. package/src/__tests__/schedule-routes.test.ts +34 -0
  44. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
  45. package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
  46. package/src/__tests__/skill-tool-factory.test.ts +49 -0
  47. package/src/__tests__/subagent-role-registry.test.ts +24 -1
  48. package/src/__tests__/subagent-tools.test.ts +1 -0
  49. package/src/__tests__/system-prompt.test.ts +109 -11
  50. package/src/__tests__/tool-error-hook.test.ts +1 -0
  51. package/src/__tests__/tool-result-spool.test.ts +337 -0
  52. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  53. package/src/__tests__/validate-input.test.ts +95 -1
  54. package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
  55. package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
  56. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
  57. package/src/agent/loop.ts +81 -24
  58. package/src/api/events/usage-progress.ts +28 -0
  59. package/src/api/index.ts +6 -0
  60. package/src/background-wake/wake-intent-hooks.test.ts +2 -0
  61. package/src/bundler/app-bundler.ts +25 -42
  62. package/src/calls/call-controller.ts +1 -1
  63. package/src/cli/commands/plugins.ts +248 -15
  64. package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
  65. package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
  66. package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
  67. package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
  68. package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
  69. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +301 -0
  70. package/src/cli/lib/inspect-plugin.ts +252 -0
  71. package/src/cli/lib/install-from-github.ts +214 -21
  72. package/src/cli/lib/list-installed-plugins.ts +17 -6
  73. package/src/cli/lib/plugin-artifact.ts +103 -0
  74. package/src/cli/lib/plugin-details.ts +18 -1
  75. package/src/cli/lib/plugin-fingerprint.ts +197 -0
  76. package/src/cli/lib/upgrade-plugin.ts +219 -0
  77. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  78. package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
  79. package/src/config/call-site-defaults.ts +13 -2
  80. package/src/config/feature-flag-registry.json +8 -16
  81. package/src/config/loader.ts +52 -59
  82. package/src/config/schema.ts +0 -2
  83. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  84. package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
  85. package/src/config/schemas/llm.ts +10 -0
  86. package/src/config/schemas/memory-v2.ts +13 -0
  87. package/src/config/schemas/memory-v3.ts +92 -0
  88. package/src/context/post-turn-tool-result-truncation.ts +32 -18
  89. package/src/context/tool-result-spool.ts +104 -0
  90. package/src/credential-execution/feature-gates.ts +0 -1
  91. package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
  92. package/src/daemon/conversation-error.ts +6 -15
  93. package/src/daemon/conversation.ts +9 -0
  94. package/src/daemon/disk-pressure-policy.ts +0 -1
  95. package/src/daemon/lifecycle.ts +1 -20
  96. package/src/daemon/message-types/conversations.ts +2 -15
  97. package/src/daemon/trust-context.ts +1 -1
  98. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  99. package/src/home/__tests__/home-content-refresh.test.ts +114 -0
  100. package/src/home/__tests__/suggested-prompts.test.ts +86 -5
  101. package/src/home/home-content-refresh.ts +43 -31
  102. package/src/home/home-greeting-cache.ts +8 -1
  103. package/src/home/home-greeting.ts +13 -9
  104. package/src/home/suggested-prompts.ts +77 -24
  105. package/src/ipc/routes/trust-rules.test.ts +66 -72
  106. package/src/media/image-credentials.ts +2 -2
  107. package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
  108. package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
  109. package/src/memory/conversation-attention-store.ts +1 -0
  110. package/src/memory/conversation-bootstrap.ts +18 -9
  111. package/src/memory/conversation-crud.ts +12 -2
  112. package/src/memory/conversation-title-service.ts +53 -9
  113. package/src/memory/delivery-channels.ts +0 -69
  114. package/src/memory/graph/extraction-job.ts +0 -15
  115. package/src/memory/guardian-action-store.ts +1 -376
  116. package/src/memory/llm-usage-store.ts +5 -1
  117. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
  118. package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
  119. package/src/memory/v2/__tests__/injection.test.ts +70 -0
  120. package/src/memory/v2/__tests__/static-context.test.ts +12 -0
  121. package/src/memory/v2/consolidation-job.ts +93 -9
  122. package/src/memory/v2/injection.ts +53 -0
  123. package/src/memory/v2/prompts/consolidation.ts +1 -0
  124. package/src/memory/v2/static-context.ts +13 -1
  125. package/src/memory/v2/sweep-job.ts +1 -1
  126. package/src/memory/v2/types.ts +5 -0
  127. package/src/plugin-api/types.ts +7 -0
  128. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
  129. package/src/plugins/defaults/exploration-drift/package.json +15 -0
  130. package/src/plugins/defaults/index.ts +25 -0
  131. package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
  132. package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
  133. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
  134. package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
  135. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  136. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
  137. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
  138. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
  139. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
  140. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
  141. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
  142. package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
  143. package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
  144. package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
  145. package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
  146. package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
  147. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
  148. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
  149. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
  150. package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
  151. package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
  152. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
  153. package/src/prompts/cache-boundary.ts +17 -0
  154. package/src/prompts/sections.ts +50 -17
  155. package/src/prompts/system-prompt.ts +12 -4
  156. package/src/prompts/templates/system-sections.ts +22 -0
  157. package/src/providers/anthropic/client.ts +74 -28
  158. package/src/providers/gemini/client.ts +5 -1
  159. package/src/providers/minimax/client.ts +9 -0
  160. package/src/providers/model-intents.ts +2 -2
  161. package/src/providers/openai/chat-completions-provider.ts +2 -1
  162. package/src/providers/openai/responses-provider.ts +5 -1
  163. package/src/providers/retry.ts +8 -0
  164. package/src/providers/types.ts +11 -0
  165. package/src/runtime/AGENTS.md +6 -0
  166. package/src/runtime/__tests__/agent-wake.test.ts +2 -2
  167. package/src/runtime/agent-wake.ts +5 -5
  168. package/src/runtime/background-job-runner.ts +2 -2
  169. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
  170. package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
  171. package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
  172. package/src/runtime/migrations/vbundle-importer.ts +9 -4
  173. package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
  174. package/src/runtime/pre-first-message-gate.ts +1 -1
  175. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
  176. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
  177. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
  178. package/src/runtime/routes/__tests__/plugins-routes.test.ts +1 -0
  179. package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
  180. package/src/runtime/routes/btw-routes.ts +0 -14
  181. package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
  182. package/src/runtime/routes/conversation-list-routes.ts +77 -5
  183. package/src/runtime/routes/conversation-management-routes.ts +54 -0
  184. package/src/runtime/routes/gateway-log-routes.ts +14 -64
  185. package/src/runtime/routes/home-feed-routes.ts +10 -0
  186. package/src/runtime/routes/identity-intro-cache.ts +1 -1
  187. package/src/runtime/routes/identity-routes.ts +76 -20
  188. package/src/runtime/routes/inbound-message-handler.ts +0 -36
  189. package/src/runtime/routes/plugins-routes.ts +21 -0
  190. package/src/runtime/routes/schedule-routes.ts +19 -2
  191. package/src/runtime/routes/trust-rules-routes.ts +14 -67
  192. package/src/schedule/recurrence-engine.ts +34 -0
  193. package/src/schedule/scheduler.ts +1 -0
  194. package/src/skills/validate-input.ts +41 -1
  195. package/src/subagent/types.ts +26 -1
  196. package/src/telemetry/types.ts +15 -1
  197. package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
  198. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  199. package/src/tools/apps/executors.ts +1 -1
  200. package/src/tools/skills/skill-tool-factory.ts +19 -8
  201. package/src/usage/types.ts +8 -1
  202. package/src/util/platform.ts +16 -0
  203. package/src/watcher/engine.ts +1 -0
  204. package/src/workspace/adaptive-thinking-repair.ts +113 -0
  205. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
  206. package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
  207. package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
  208. package/src/workspace/migrations/registry.ts +4 -0
  209. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
  210. package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
  211. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
  212. package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
  213. package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
  214. package/src/__tests__/update-bulletin-job.test.ts +0 -292
  215. package/src/config/schemas/updates.ts +0 -14
  216. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
  217. package/src/memory/conversation-starters-cadence.ts +0 -78
  218. package/src/prompts/update-bulletin-job.ts +0 -180
  219. package/src/runtime/guardian-action-followup-executor.ts +0 -306
@@ -1364,6 +1364,40 @@ describe("custom baseURL initialization", () => {
1364
1364
  timeout: DEFAULT_SDK_TIMEOUT_MS,
1365
1365
  });
1366
1366
  });
1367
+
1368
+ test("MinimaxProvider sends reasoning_split so reasoning never leaks into content as <think> tags", async () => {
1369
+ fakeChunks = [textChunk("hello", "stop"), usageChunk(10, 5)];
1370
+ const provider = new MinimaxProvider("mm-user-key", "MiniMax-M3");
1371
+
1372
+ await provider.sendMessage([userMsg("hi")]);
1373
+
1374
+ expect(lastCreateParams!.reasoning_split).toBe(true);
1375
+ });
1376
+
1377
+ test("MinimaxProvider replays prior assistant thinking via reasoning_content", async () => {
1378
+ fakeChunks = [textChunk("done", "stop"), usageChunk(10, 5)];
1379
+ const provider = new MinimaxProvider("mm-user-key", "MiniMax-M3");
1380
+
1381
+ await provider.sendMessage([
1382
+ userMsg("question"),
1383
+ {
1384
+ role: "assistant",
1385
+ content: [
1386
+ { type: "thinking", thinking: "prior reasoning", signature: "" },
1387
+ { type: "text", text: "prior answer" },
1388
+ ],
1389
+ },
1390
+ userMsg("follow-up"),
1391
+ ]);
1392
+
1393
+ const sent = lastCreateParams!.messages as Array<Record<string, unknown>>;
1394
+ const assistantTurn = sent.find((m) => m.role === "assistant");
1395
+ expect(assistantTurn).toEqual({
1396
+ role: "assistant",
1397
+ content: "prior answer",
1398
+ reasoning_content: "prior reasoning",
1399
+ });
1400
+ });
1367
1401
  });
1368
1402
 
1369
1403
  // ---------------------------------------------------------------------------
@@ -107,7 +107,6 @@ describe("pre-chat onboarding contract", () => {
107
107
  "USER.md",
108
108
  "BOOTSTRAP.md",
109
109
  "BOOTSTRAP-REFERENCE.md",
110
- "UPDATES.md",
111
110
  "users",
112
111
  "channels",
113
112
  ]) {
@@ -2,6 +2,8 @@ import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import {
4
4
  computeNextRunAt,
5
+ describeRRuleExpression,
6
+ isSingleFireRRule,
5
7
  isValidScheduleExpression,
6
8
  } from "../schedule/recurrence-engine.js";
7
9
 
@@ -117,3 +119,46 @@ describe("recurrence engine — rrule", () => {
117
119
  expect(next).toBe(jan2);
118
120
  });
119
121
  });
122
+
123
+ describe("recurrence engine — rrule display helpers", () => {
124
+ const SINGLE_FIRE = "DTSTART:20990612T080000\nRRULE:FREQ=DAILY;COUNT=1";
125
+ const WEEKLY = "DTSTART:20990612T080000\nRRULE:FREQ=WEEKLY;BYDAY=MO,WE";
126
+
127
+ test("isSingleFireRRule detects COUNT=1 rules", () => {
128
+ expect(isSingleFireRRule(SINGLE_FIRE)).toBe(true);
129
+ expect(isSingleFireRRule(WEEKLY)).toBe(false);
130
+ expect(
131
+ isSingleFireRRule("DTSTART:20990612T080000\nRRULE:FREQ=DAILY;COUNT=2"),
132
+ ).toBe(false);
133
+ expect(isSingleFireRRule("not an rrule")).toBe(false);
134
+ });
135
+
136
+ test("isSingleFireRRule is false for set constructs", () => {
137
+ expect(
138
+ isSingleFireRRule(
139
+ "DTSTART:20990612T080000\nRRULE:FREQ=DAILY;COUNT=1\nRDATE:20990613T080000",
140
+ ),
141
+ ).toBe(false);
142
+ });
143
+
144
+ test("describeRRuleExpression humanizes rules", () => {
145
+ expect(describeRRuleExpression(SINGLE_FIRE)).toBe("One-time");
146
+ expect(describeRRuleExpression(WEEKLY)).toBe(
147
+ "Every week on Monday, Wednesday",
148
+ );
149
+ expect(
150
+ describeRRuleExpression(
151
+ "DTSTART:20990612T080000\nRRULE:FREQ=DAILY;COUNT=3",
152
+ ),
153
+ ).toBe("Every day for 3 times");
154
+ });
155
+
156
+ test("describeRRuleExpression falls back instead of leaking raw text", () => {
157
+ expect(
158
+ describeRRuleExpression(
159
+ "DTSTART:20990612T080000\nRRULE:FREQ=DAILY\nEXDATE:20990613T080000",
160
+ ),
161
+ ).toBe("Custom recurrence");
162
+ expect(describeRRuleExpression("garbage")).toBe("Custom recurrence");
163
+ });
164
+ });
@@ -492,6 +492,40 @@ describe("GET /schedules — default defer exclusion", () => {
492
492
  });
493
493
  });
494
494
 
495
+ test("humanizes rrule cadences and flags single-fire rrules as one-shot", () => {
496
+ createSchedule({
497
+ name: "RRule single fire",
498
+ cronExpression: "DTSTART:20990612T080000\nRRULE:FREQ=DAILY;COUNT=1",
499
+ syntax: "rrule",
500
+ message: "brush teeth",
501
+ });
502
+ createSchedule({
503
+ name: "RRule weekly",
504
+ cronExpression: "DTSTART:20990612T080000\nRRULE:FREQ=WEEKLY;BYDAY=MO,WE",
505
+ syntax: "rrule",
506
+ message: "standup",
507
+ });
508
+
509
+ const route = findRoute("schedules", "GET");
510
+ const result = route.handler({}) as {
511
+ schedules: Array<{
512
+ name: string;
513
+ cadenceDescription: string;
514
+ isOneShot: boolean;
515
+ }>;
516
+ };
517
+ const byName = new Map(result.schedules.map((s) => [s.name, s]));
518
+
519
+ expect(byName.get("RRule single fire")).toMatchObject({
520
+ cadenceDescription: "One-time",
521
+ isOneShot: true,
522
+ });
523
+ expect(byName.get("RRule weekly")).toMatchObject({
524
+ cadenceDescription: "Every week on Monday, Wednesday",
525
+ isOneShot: false,
526
+ });
527
+ });
528
+
495
529
  test("mutation responses also exclude deferred wakes", () => {
496
530
  createSchedule({
497
531
  name: "Agent schedule",
@@ -28,7 +28,7 @@ mock.module("../config/loader.js", () => ({
28
28
  getNestedValue: () => undefined,
29
29
  setNestedValue: () => {},
30
30
  API_KEY_PROVIDERS: [],
31
- _appendQuarantineBulletin: () => {},
31
+ _writeQuarantineNotice: () => {},
32
32
  invalidateConfigCache: () => {},
33
33
  }));
34
34
 
@@ -247,13 +247,18 @@ describe("session-manager", () => {
247
247
  await startSession(session.id);
248
248
  expect(getActiveSession(CONV_ID)).toBeDefined();
249
249
 
250
- // Wait for the idle timeout to fire
251
- await new Promise((r) => setTimeout(r, 200));
250
+ // Wait for the idle timeout to fire and the async stop to finish.
251
+ // The session passes through "stopping" while the server closes, so
252
+ // poll for the terminal state rather than sleeping a fixed interval.
253
+ const deadline = Date.now() + 5000;
254
+ const findSession = () =>
255
+ getSessionsForConversation(CONV_ID).find((x) => x.id === session.id);
256
+ while (findSession()?.status !== "stopped" && Date.now() < deadline) {
257
+ await new Promise((r) => setTimeout(r, 20));
258
+ }
252
259
 
253
260
  expect(getActiveSession(CONV_ID)).toBeUndefined();
254
- const all = getSessionsForConversation(CONV_ID);
255
- const s = all.find((x) => x.id === session.id);
256
- expect(s?.status).toBe("stopped");
261
+ expect(findSession()?.status).toBe("stopped");
257
262
  });
258
263
  });
259
264
 
@@ -347,6 +347,55 @@ describe("createSkillTool — required/type/enum validation", () => {
347
347
  expect(result.content).toContain('mode must be one of "a", "b"');
348
348
  });
349
349
 
350
+ test("coerces string booleans before validation and passes the coerced value to the executor", async () => {
351
+ const hash = computeSkillVersionHash(tempDir);
352
+ const tool = createSkillTool(
353
+ makeEntry({
354
+ executor: "echo.ts",
355
+ input_schema: {
356
+ type: "object",
357
+ properties: {
358
+ auto_open: { type: "boolean" },
359
+ name: { type: "string" },
360
+ },
361
+ },
362
+ }),
363
+ tempDir,
364
+ hash,
365
+ );
366
+
367
+ const result = await tool.execute(
368
+ { auto_open: "false", name: "x" },
369
+ makeContext(),
370
+ );
371
+
372
+ expect(result.isError).toBe(false);
373
+ const parsed = JSON.parse(result.content);
374
+ expect(parsed.input).toEqual({ auto_open: false, name: "x" });
375
+ });
376
+
377
+ test("rejects non-coercible boolean strings with a self-correcting message", async () => {
378
+ const hash = computeSkillVersionHash(tempDir);
379
+ const tool = createSkillTool(
380
+ makeEntry({
381
+ executor: "echo.ts",
382
+ input_schema: {
383
+ type: "object",
384
+ properties: { auto_open: { type: "boolean" } },
385
+ },
386
+ }),
387
+ tempDir,
388
+ hash,
389
+ );
390
+
391
+ const result = await tool.execute({ auto_open: "yes" }, makeContext());
392
+
393
+ expect(result.isError).toBe(true);
394
+ expect(result.content).toContain(
395
+ "auto_open must be a boolean — pass true or false as a JSON boolean, not a string",
396
+ );
397
+ });
398
+
350
399
  test("passes valid input through to the executor unchanged", async () => {
351
400
  const hash = computeSkillVersionHash(tempDir);
352
401
  const tool = createSkillTool(
@@ -7,7 +7,13 @@ import {
7
7
  } from "../subagent/index.js";
8
8
 
9
9
  /** All roles defined in the SubagentRole union. */
10
- const ALL_ROLES: SubagentRole[] = ["general", "researcher", "coder", "planner"];
10
+ const ALL_ROLES: SubagentRole[] = [
11
+ "general",
12
+ "researcher",
13
+ "coder",
14
+ "planner",
15
+ "investigator",
16
+ ];
11
17
 
12
18
  describe("SUBAGENT_ROLE_REGISTRY", () => {
13
19
  test("covers all values in the SubagentRole union", () => {
@@ -64,6 +70,23 @@ describe("SUBAGENT_ROLE_REGISTRY", () => {
64
70
  expect(SUBAGENT_ROLE_REGISTRY.planner.allowedTools!).toContain("recall");
65
71
  });
66
72
 
73
+ test('investigator includes "bash" for grep/find-based code and log investigation', () => {
74
+ expect(SUBAGENT_ROLE_REGISTRY.investigator.allowedTools!).toContain("bash");
75
+ });
76
+
77
+ test("investigator excludes file write tools (read-only investigation)", () => {
78
+ const tools = SUBAGENT_ROLE_REGISTRY.investigator.allowedTools!;
79
+ expect(tools).not.toContain("file_write");
80
+ expect(tools).not.toContain("file_edit");
81
+ });
82
+
83
+ test("investigator preamble defines the root-cause report contract", () => {
84
+ const preamble = SUBAGENT_ROLE_REGISTRY.investigator.systemPromptPreamble;
85
+ expect(preamble).toContain("Root cause");
86
+ expect(preamble).toContain("Evidence");
87
+ expect(preamble).toContain("notify_parent");
88
+ });
89
+
67
90
  test("no role references the old memory_recall tool name", () => {
68
91
  for (const [_role, config] of Object.entries(SUBAGENT_ROLE_REGISTRY)) {
69
92
  if (config.allowedTools !== undefined) {
@@ -1325,6 +1325,7 @@ describe("Subagent role-based spawn", () => {
1325
1325
  "researcher",
1326
1326
  "coder",
1327
1327
  "planner",
1328
+ "investigator",
1328
1329
  ]);
1329
1330
  // role is not required
1330
1331
  expect(def.input_schema.required).not.toContain("role");
@@ -110,6 +110,8 @@ mock.module("../credential-execution/managed-catalog.js", () => ({
110
110
  // Import after mock
111
111
  const { buildSystemPrompt, ensurePromptFiles, stripCommentLines } =
112
112
  await import("../prompts/system-prompt.js");
113
+ const { SYSTEM_PROMPT_CACHE_BOUNDARY } =
114
+ await import("../prompts/cache-boundary.js");
113
115
 
114
116
  describe("buildSystemPrompt", () => {
115
117
  beforeEach(() => {
@@ -128,7 +130,6 @@ describe("buildSystemPrompt", () => {
128
130
  "SOUL.md",
129
131
  "USER.md",
130
132
  "BOOTSTRAP.md",
131
- "UPDATES.md",
132
133
  "VOICE.md",
133
134
  "skills",
134
135
  "users",
@@ -603,16 +604,6 @@ describe("buildSystemPrompt", () => {
603
604
  });
604
605
  });
605
606
 
606
- test("never includes UPDATES.md content in system prompt", () => {
607
- const updatesBody = "# v1.2\n\nNew feature added. UNIQUE_UPDATES_MARKER.";
608
- writeFileSync(join(TEST_DIR, "UPDATES.md"), updatesBody);
609
- const result = buildSystemPrompt();
610
- expect(result).not.toContain("## Recent Updates");
611
- expect(result).not.toContain(updatesBody);
612
- expect(result).not.toContain("UNIQUE_UPDATES_MARKER");
613
- expect(result).not.toContain("Update Handling");
614
- });
615
-
616
607
  test("strips comment lines starting with _ from prompt files", () => {
617
608
  writeFileSync(
618
609
  join(TEST_DIR, "IDENTITY.md"),
@@ -879,6 +870,113 @@ describe("buildSystemPrompt", () => {
879
870
  expect(orgIdx).toBeGreaterThan(parallelIdx);
880
871
  });
881
872
 
873
+ describe("cache breakpoints", () => {
874
+ test("default breakpoint splits the prompt after 11-channel-persona", () => {
875
+ /**
876
+ * Tests that the bundled `cacheBreakpoint` on 11-channel-persona
877
+ * places the boundary marker between the stable sections and the
878
+ * volatile 12-voice section.
879
+ */
880
+ // GIVEN a workspace where a volatile section (12-voice) renders
881
+ writeFileSync(join(TEST_DIR, "VOICE.md"), "- Prefers lowercase.");
882
+
883
+ // WHEN the system prompt is built
884
+ const result = buildSystemPrompt();
885
+
886
+ // THEN the boundary marker sits before the voice content and
887
+ // after the stable instruction sections
888
+ const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
889
+ expect(boundaryIdx).toBeGreaterThan(-1);
890
+ expect(result.indexOf("## Communication")).toBeLessThan(boundaryIdx);
891
+ expect(result.indexOf("# Voice Profile")).toBeGreaterThan(boundaryIdx);
892
+ });
893
+
894
+ test("breakpoint splits even when the declaring section gates off", () => {
895
+ /**
896
+ * Tests that the split is resolved against the section ordering,
897
+ * not the rendered output — an absent channel persona must not
898
+ * merge the blocks around it.
899
+ */
900
+ // GIVEN no channel persona file exists but a volatile section renders
901
+ writeFileSync(join(TEST_DIR, "VOICE.md"), "- Prefers lowercase.");
902
+
903
+ // WHEN the system prompt is built
904
+ const result = buildSystemPrompt();
905
+
906
+ // THEN the boundary marker is still present
907
+ expect(result).toContain(SYSTEM_PROMPT_CACHE_BOUNDARY);
908
+ });
909
+
910
+ test("no boundary marker when nothing renders after the breakpoint", () => {
911
+ /**
912
+ * Tests that empty trailing blocks are dropped so the marker
913
+ * never dangles at the end of the prompt.
914
+ */
915
+ // GIVEN a fresh workspace with no volatile sections (no VOICE.md,
916
+ // no BOOTSTRAP.md, no connections)
917
+ // WHEN the system prompt is built
918
+ const result = buildSystemPrompt();
919
+
920
+ // THEN no boundary marker appears
921
+ expect(result).not.toContain(SYSTEM_PROMPT_CACHE_BOUNDARY);
922
+ });
923
+
924
+ test("workspace override of 11-channel-persona without cache_breakpoint clears the default", () => {
925
+ /**
926
+ * Tests that a workspace override takes full control of the
927
+ * section, including its breakpoint declaration.
928
+ */
929
+ // GIVEN a workspace override of 11-channel-persona with no
930
+ // cache_breakpoint frontmatter, and a volatile section that renders
931
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
932
+ writeFileSync(
933
+ join(SYSTEM_PROMPTS_DIR, "11-channel-persona.md"),
934
+ "Override channel persona.\n",
935
+ );
936
+ writeFileSync(join(TEST_DIR, "VOICE.md"), "- Prefers lowercase.");
937
+
938
+ // WHEN the system prompt is built
939
+ const result = buildSystemPrompt();
940
+
941
+ // THEN the override body renders but no boundary marker appears
942
+ expect(result).toContain("Override channel persona.");
943
+ expect(result).not.toContain(SYSTEM_PROMPT_CACHE_BOUNDARY);
944
+ });
945
+
946
+ test("frontmatter cache_breakpoint on a workspace section places the boundary there", () => {
947
+ /**
948
+ * Tests that users can declare the breakpoint position via
949
+ * `cache_breakpoint: true` frontmatter, and that only the first
950
+ * declaration (in id-sort order) is honored.
951
+ */
952
+ // GIVEN a custom workspace section declaring a cache breakpoint
953
+ // ahead of the bundled default on 11-channel-persona
954
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
955
+ writeFileSync(
956
+ join(SYSTEM_PROMPTS_DIR, "00-aaa-custom.md"),
957
+ "---\ncache_breakpoint: true\n---\nCustom head section.\n",
958
+ );
959
+ writeFileSync(join(TEST_DIR, "VOICE.md"), "- Prefers lowercase.");
960
+
961
+ // WHEN the system prompt is built
962
+ const result = buildSystemPrompt();
963
+
964
+ // THEN exactly one boundary appears, right after the custom section
965
+ const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
966
+ expect(boundaryIdx).toBeGreaterThan(-1);
967
+ expect(result.indexOf("Custom head section.")).toBeLessThan(
968
+ boundaryIdx,
969
+ );
970
+ expect(result.indexOf("## Communication")).toBeGreaterThan(boundaryIdx);
971
+
972
+ // AND the bundled default declaration on 11-channel-persona is
973
+ // ignored — no second marker
974
+ expect(
975
+ result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY, boundaryIdx + 1),
976
+ ).toBe(-1);
977
+ });
978
+ });
979
+
882
980
  describe("containerized section (slot 02)", () => {
883
981
  const CONTAINERIZED_FILE = join(
884
982
  SYSTEM_PROMPTS_DIR,
@@ -79,6 +79,7 @@ function makeCtx(
79
79
  toolResponse,
80
80
  messages,
81
81
  additionalContext: null,
82
+ model: "claude-test-model",
82
83
  maxInputTokens: 10_000,
83
84
  logger: noopLogger,
84
85
  };