@vellumai/assistant 0.4.10 → 0.4.12

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 (203) hide show
  1. package/ARCHITECTURE.md +418 -378
  2. package/Dockerfile +1 -1
  3. package/README.md +16 -9
  4. package/package.json +1 -1
  5. package/src/__tests__/account-registry.test.ts +1 -0
  6. package/src/__tests__/actor-token-service.test.ts +1 -0
  7. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  8. package/src/__tests__/asset-materialize-tool.test.ts +7 -0
  9. package/src/__tests__/asset-search-tool.test.ts +7 -0
  10. package/src/__tests__/browser-fill-credential.test.ts +1 -0
  11. package/src/__tests__/call-start-guardian-guard.test.ts +1 -0
  12. package/src/__tests__/channel-approval-routes.test.ts +29 -0
  13. package/src/__tests__/channel-guardian.test.ts +2143 -1546
  14. package/src/__tests__/channel-retry-sweep.test.ts +169 -14
  15. package/src/__tests__/claude-code-tool-profiles.test.ts +1 -0
  16. package/src/__tests__/computer-use-tools.test.ts +1 -0
  17. package/src/__tests__/contacts-tools.test.ts +1 -0
  18. package/src/__tests__/conversation-attention-telegram.test.ts +1 -0
  19. package/src/__tests__/credential-policy-validate.test.ts +97 -0
  20. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  21. package/src/__tests__/credential-vault-unit.test.ts +1 -0
  22. package/src/__tests__/credential-vault.test.ts +1 -0
  23. package/src/__tests__/delete-managed-skill-tool.test.ts +1 -0
  24. package/src/__tests__/file-edit-tool.test.ts +1 -0
  25. package/src/__tests__/file-read-tool.test.ts +1 -0
  26. package/src/__tests__/file-write-tool.test.ts +1 -0
  27. package/src/__tests__/followup-tools.test.ts +1 -0
  28. package/src/__tests__/gateway-only-guard.test.ts +1 -1
  29. package/src/__tests__/guardian-control-plane-policy.test.ts +5 -4
  30. package/src/__tests__/guardian-grant-minting.test.ts +3 -0
  31. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +4 -3
  32. package/src/__tests__/guardian-routing-state.test.ts +8 -0
  33. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +75 -61
  34. package/src/__tests__/headless-browser-interactions.test.ts +1 -0
  35. package/src/__tests__/headless-browser-navigate.test.ts +1 -0
  36. package/src/__tests__/headless-browser-read-tools.test.ts +1 -0
  37. package/src/__tests__/headless-browser-snapshot.test.ts +1 -0
  38. package/src/__tests__/host-file-edit-tool.test.ts +1 -0
  39. package/src/__tests__/host-file-read-tool.test.ts +1 -0
  40. package/src/__tests__/host-file-write-tool.test.ts +1 -0
  41. package/src/__tests__/host-shell-tool.test.ts +1 -0
  42. package/src/__tests__/lifecycle-docs-guard.test.ts +207 -0
  43. package/src/__tests__/managed-skill-lifecycle.test.ts +1 -0
  44. package/src/__tests__/media-reuse-story.e2e.test.ts +8 -0
  45. package/src/__tests__/messaging-send-tool.test.ts +1 -0
  46. package/src/__tests__/playbook-execution.test.ts +1 -0
  47. package/src/__tests__/playbook-tools.test.ts +1 -0
  48. package/src/__tests__/registry.test.ts +235 -187
  49. package/src/__tests__/relay-server.test.ts +4 -0
  50. package/src/__tests__/scaffold-managed-skill-tool.test.ts +1 -0
  51. package/src/__tests__/schedule-tools.test.ts +1 -0
  52. package/src/__tests__/secret-onetime-send.test.ts +4 -0
  53. package/src/__tests__/secret-scanner-executor.test.ts +2 -0
  54. package/src/__tests__/secure-keys.test.ts +27 -0
  55. package/src/__tests__/send-notification-tool.test.ts +2 -0
  56. package/src/__tests__/session-agent-loop.test.ts +521 -256
  57. package/src/__tests__/session-surfaces-task-progress.test.ts +1 -0
  58. package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
  59. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
  60. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
  61. package/src/__tests__/shell-credential-ref.test.ts +1 -0
  62. package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -0
  63. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  64. package/src/__tests__/skill-load-tool.test.ts +1 -0
  65. package/src/__tests__/skill-script-runner-host.test.ts +1 -0
  66. package/src/__tests__/skill-script-runner-sandbox.test.ts +1 -0
  67. package/src/__tests__/skill-script-runner.test.ts +1 -0
  68. package/src/__tests__/skill-tool-factory.test.ts +1 -0
  69. package/src/__tests__/skills.test.ts +334 -276
  70. package/src/__tests__/starter-task-flow.test.ts +7 -17
  71. package/src/__tests__/subagent-tools.test.ts +1 -1
  72. package/src/__tests__/swarm-recursion.test.ts +1 -0
  73. package/src/__tests__/swarm-session-integration.test.ts +1 -0
  74. package/src/__tests__/swarm-tool.test.ts +1 -0
  75. package/src/__tests__/task-management-tools.test.ts +1 -0
  76. package/src/__tests__/task-tools.test.ts +1 -0
  77. package/src/__tests__/terminal-tools.test.ts +1 -0
  78. package/src/__tests__/tool-approval-handler.test.ts +2 -2
  79. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  80. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
  81. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  82. package/src/__tests__/tool-executor-shell-integration.test.ts +1 -0
  83. package/src/__tests__/tool-executor.test.ts +1 -0
  84. package/src/__tests__/trust-context-guards.test.ts +218 -0
  85. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +6 -0
  86. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +6 -0
  87. package/src/__tests__/trusted-contact-multichannel.test.ts +1 -0
  88. package/src/__tests__/trusted-contact-verification.test.ts +1 -0
  89. package/src/__tests__/view-image-tool.test.ts +1 -0
  90. package/src/agent/loop.ts +9 -2
  91. package/src/calls/guardian-dispatch.ts +4 -4
  92. package/src/cli/mcp.ts +183 -3
  93. package/src/config/bundled-skills/agentmail/SKILL.md +4 -4
  94. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +449 -0
  95. package/src/config/bundled-skills/doordash/SKILL.md +171 -0
  96. package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +203 -0
  97. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +164 -0
  98. package/src/config/bundled-skills/doordash/doordash-cli.ts +1193 -0
  99. package/src/config/bundled-skills/doordash/doordash-entry.ts +22 -0
  100. package/src/config/bundled-skills/doordash/lib/cart-queries.ts +787 -0
  101. package/src/config/bundled-skills/doordash/lib/client.ts +1071 -0
  102. package/src/config/bundled-skills/doordash/lib/order-queries.ts +85 -0
  103. package/src/config/bundled-skills/doordash/lib/queries.ts +28 -0
  104. package/src/config/bundled-skills/doordash/lib/query-extractor.ts +94 -0
  105. package/src/config/bundled-skills/doordash/lib/search-queries.ts +203 -0
  106. package/src/config/bundled-skills/doordash/lib/session.ts +93 -0
  107. package/src/config/bundled-skills/doordash/lib/shared/errors.ts +61 -0
  108. package/src/config/bundled-skills/doordash/lib/shared/ipc.ts +32 -0
  109. package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +380 -0
  110. package/src/config/bundled-skills/doordash/lib/shared/platform.ts +35 -0
  111. package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +43 -0
  112. package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +49 -0
  113. package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +6 -0
  114. package/src/config/bundled-skills/doordash/lib/store-queries.ts +246 -0
  115. package/src/config/bundled-skills/doordash/lib/types.ts +367 -0
  116. package/src/config/bundled-skills/google-calendar/SKILL.md +4 -5
  117. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +42 -41
  118. package/src/config/bundled-skills/messaging/SKILL.md +59 -42
  119. package/src/config/bundled-skills/messaging/TOOLS.json +2 -2
  120. package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +5 -1
  121. package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +11 -2
  122. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +10 -3
  123. package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +5 -1
  124. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +5 -1
  125. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -1
  126. package/src/config/bundled-skills/notion/SKILL.md +240 -0
  127. package/src/config/bundled-skills/notion-oauth-setup/SKILL.md +127 -0
  128. package/src/config/bundled-skills/oauth-setup/SKILL.md +144 -0
  129. package/src/config/bundled-skills/phone-calls/SKILL.md +91 -162
  130. package/src/config/bundled-skills/skills-catalog/SKILL.md +32 -29
  131. package/src/config/{vellum-skills → bundled-skills}/sms-setup/SKILL.md +29 -22
  132. package/src/config/{vellum-skills → bundled-skills}/telegram-setup/SKILL.md +17 -14
  133. package/src/config/{vellum-skills → bundled-skills}/twilio-setup/SKILL.md +21 -6
  134. package/src/config/bundled-tool-registry.ts +281 -267
  135. package/src/config/system-prompt.ts +4 -2
  136. package/src/daemon/computer-use-session.ts +1 -0
  137. package/src/daemon/handlers/skills.ts +334 -234
  138. package/src/daemon/ipc-contract/messages.ts +2 -0
  139. package/src/daemon/ipc-contract/surfaces.ts +2 -0
  140. package/src/daemon/lifecycle.ts +358 -221
  141. package/src/daemon/response-tier.ts +2 -0
  142. package/src/daemon/server.ts +453 -193
  143. package/src/daemon/session-agent-loop-handlers.ts +42 -2
  144. package/src/daemon/session-agent-loop.ts +4 -1
  145. package/src/daemon/session-lifecycle.ts +3 -0
  146. package/src/daemon/session-memory.ts +2 -2
  147. package/src/daemon/session-process.ts +1 -0
  148. package/src/daemon/session-runtime-assembly.ts +2 -2
  149. package/src/daemon/session-surfaces.ts +22 -20
  150. package/src/daemon/session-tool-setup.ts +2 -1
  151. package/src/daemon/session.ts +5 -2
  152. package/src/mcp/client.ts +55 -6
  153. package/src/mcp/manager.ts +9 -0
  154. package/src/mcp/mcp-oauth-provider.ts +347 -0
  155. package/src/memory/channel-delivery-store.ts +1 -0
  156. package/src/memory/db-init.ts +4 -0
  157. package/src/memory/delivery-status.ts +43 -0
  158. package/src/memory/guardian-bindings.ts +3 -3
  159. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +108 -0
  160. package/src/memory/migrations/index.ts +1 -0
  161. package/src/memory/migrations/registry.ts +6 -0
  162. package/src/memory/schema.ts +1 -1
  163. package/src/messaging/outreach-classifier.ts +12 -5
  164. package/src/messaging/provider-types.ts +2 -0
  165. package/src/messaging/providers/gmail/adapter.ts +9 -3
  166. package/src/messaging/providers/gmail/client.ts +2 -0
  167. package/src/runtime/actor-trust-resolver.ts +13 -4
  168. package/src/runtime/channel-retry-sweep.ts +31 -14
  169. package/src/runtime/guardian-context-resolver.ts +25 -64
  170. package/src/runtime/guardian-outbound-actions.ts +399 -108
  171. package/src/runtime/guardian-vellum-migration.ts +1 -23
  172. package/src/runtime/guardian-verification-templates.ts +66 -30
  173. package/src/runtime/http-errors.ts +33 -20
  174. package/src/runtime/http-server.ts +706 -291
  175. package/src/runtime/http-types.ts +26 -16
  176. package/src/runtime/local-actor-identity.ts +4 -6
  177. package/src/runtime/middleware/actor-token.ts +2 -8
  178. package/src/runtime/routes/channel-route-shared.ts +0 -1
  179. package/src/runtime/routes/inbound-message-handler.ts +3 -4
  180. package/src/runtime/routes/secret-routes.ts +57 -2
  181. package/src/runtime/routes/surface-action-routes.ts +66 -0
  182. package/src/runtime/routes/trust-rules-routes.ts +140 -0
  183. package/src/runtime/tool-grant-request-helper.ts +1 -1
  184. package/src/security/keychain-to-encrypted-migration.ts +59 -0
  185. package/src/security/secure-keys.ts +17 -0
  186. package/src/skills/frontmatter.ts +9 -7
  187. package/src/tools/apps/executors.ts +2 -1
  188. package/src/tools/credentials/policy-validate.ts +22 -0
  189. package/src/tools/guardian-control-plane-policy.ts +2 -2
  190. package/src/tools/tool-manifest.ts +44 -42
  191. package/src/tools/types.ts +10 -1
  192. package/src/__tests__/skill-mirror-parity.test.ts +0 -176
  193. package/src/config/vellum-skills/catalog.json +0 -63
  194. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +0 -295
  195. package/src/skills/vellum-catalog-remote.ts +0 -166
  196. package/src/tools/skills/vellum-catalog.ts +0 -168
  197. /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/SKILL.md +0 -0
  198. /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/TOOLS.json +0 -0
  199. /package/src/config/{vellum-skills → bundled-skills}/deploy-fullstack-vercel/SKILL.md +0 -0
  200. /package/src/config/{vellum-skills → bundled-skills}/document-writer/SKILL.md +0 -0
  201. /package/src/config/{vellum-skills → bundled-skills}/guardian-verify-setup/SKILL.md +0 -0
  202. /package/src/config/{vellum-skills → bundled-skills}/slack-oauth-setup/SKILL.md +0 -0
  203. /package/src/config/{vellum-skills → bundled-skills}/trusted-contacts/SKILL.md +0 -0
@@ -5,113 +5,128 @@
5
5
  * so the user does not have to manually ask whether verification succeeded.
6
6
  */
7
7
 
8
- import { readFileSync } from 'node:fs';
9
- import { resolve } from 'node:path';
8
+ import { readFileSync } from "node:fs";
9
+ import { resolve } from "node:path";
10
10
 
11
- import { describe, expect, test } from 'bun:test';
11
+ import { describe, expect, test } from "bun:test";
12
12
 
13
13
  // ---------------------------------------------------------------------------
14
14
  // Locate the skill SKILL.md
15
15
  // ---------------------------------------------------------------------------
16
16
 
17
- const ASSISTANT_DIR = resolve(import.meta.dirname ?? __dirname, '..', '..');
17
+ const ASSISTANT_DIR = resolve(import.meta.dirname ?? __dirname, "..", "..");
18
18
  const SKILL_PATH = resolve(
19
19
  ASSISTANT_DIR,
20
- 'src',
21
- 'config',
22
- 'vellum-skills',
23
- 'guardian-verify-setup',
24
- 'SKILL.md',
20
+ "src",
21
+ "config",
22
+ "bundled-skills",
23
+ "guardian-verify-setup",
24
+ "SKILL.md",
25
25
  );
26
26
 
27
- const skillContent = readFileSync(SKILL_PATH, 'utf-8');
27
+ const skillContent = readFileSync(SKILL_PATH, "utf-8");
28
28
 
29
29
  // ---------------------------------------------------------------------------
30
30
  // Tests
31
31
  // ---------------------------------------------------------------------------
32
32
 
33
- describe('guardian-verify-setup skill — voice auto-followup', () => {
34
- test('voice path in Step 3 references the auto-check polling loop', () => {
33
+ describe("guardian-verify-setup skill — voice auto-followup", () => {
34
+ test("voice path in Step 3 references the auto-check polling loop", () => {
35
35
  // The voice success instruction in Step 3 must direct the assistant to
36
36
  // begin the polling loop rather than waiting for the user to report back.
37
37
  expect(skillContent).toContain(
38
- 'immediately begin the voice auto-check polling loop',
38
+ "immediately begin the voice auto-check polling loop",
39
39
  );
40
40
  });
41
41
 
42
- test('voice path in Step 4 (resend) references the auto-check polling loop', () => {
42
+ test("voice path in Step 4 (resend) references the auto-check polling loop", () => {
43
43
  // After a voice resend, the same auto-check behavior must kick in.
44
- const resendSection = skillContent.split('## Step 4')[1]?.split('## Step 5')[0] ?? '';
45
- expect(resendSection).toContain(
46
- 'voice auto-check polling loop',
47
- );
44
+ const resendSection =
45
+ skillContent.split("## Step 4")[1]?.split("## Step 5")[0] ?? "";
46
+ expect(resendSection).toContain("voice auto-check polling loop");
48
47
  });
49
48
 
50
- test('contains a Voice Auto-Check Polling section', () => {
51
- expect(skillContent).toContain('## Voice Auto-Check Polling');
49
+ test("contains a Voice Auto-Check Polling section", () => {
50
+ expect(skillContent).toContain("## Voice Auto-Check Polling");
52
51
  });
53
52
 
54
- test('polling section specifies the correct status endpoint for voice', () => {
53
+ test("polling section specifies the correct status endpoint for voice", () => {
55
54
  const pollingSection =
56
- skillContent.split('## Voice Auto-Check Polling')[1]?.split('## Step 6')[0] ?? '';
55
+ skillContent
56
+ .split("## Voice Auto-Check Polling")[1]
57
+ ?.split("## Step 6")[0] ?? "";
57
58
  expect(pollingSection).toContain(
58
- '/v1/integrations/guardian/status?channel=voice',
59
+ "/v1/integrations/guardian/status?channel=voice",
59
60
  );
60
61
  });
61
62
 
62
- test('polling section includes ~15 second interval', () => {
63
+ test("polling section includes ~15 second interval", () => {
63
64
  const pollingSection =
64
- skillContent.split('## Voice Auto-Check Polling')[1]?.split('## Step 6')[0] ?? '';
65
- expect(pollingSection).toContain('~15 seconds');
65
+ skillContent
66
+ .split("## Voice Auto-Check Polling")[1]
67
+ ?.split("## Step 6")[0] ?? "";
68
+ expect(pollingSection).toContain("~15 seconds");
66
69
  });
67
70
 
68
- test('polling section includes 2-minute timeout', () => {
71
+ test("polling section includes 2-minute timeout", () => {
69
72
  const pollingSection =
70
- skillContent.split('## Voice Auto-Check Polling')[1]?.split('## Step 6')[0] ?? '';
71
- expect(pollingSection).toContain('2 minutes');
73
+ skillContent
74
+ .split("## Voice Auto-Check Polling")[1]
75
+ ?.split("## Step 6")[0] ?? "";
76
+ expect(pollingSection).toContain("2 minutes");
72
77
  });
73
78
 
74
- test('polling section checks for bound: true', () => {
79
+ test("polling section checks for bound: true", () => {
75
80
  const pollingSection =
76
- skillContent.split('## Voice Auto-Check Polling')[1]?.split('## Step 6')[0] ?? '';
77
- expect(pollingSection).toContain('bound: true');
81
+ skillContent
82
+ .split("## Voice Auto-Check Polling")[1]
83
+ ?.split("## Step 6")[0] ?? "";
84
+ expect(pollingSection).toContain("bound: true");
78
85
  });
79
86
 
80
- test('polling section includes proactive success confirmation', () => {
87
+ test("polling section includes proactive success confirmation", () => {
81
88
  const pollingSection =
82
- skillContent.split('## Voice Auto-Check Polling')[1]?.split('## Step 6')[0] ?? '';
83
- expect(pollingSection).toContain('proactive success message');
89
+ skillContent
90
+ .split("## Voice Auto-Check Polling")[1]
91
+ ?.split("## Step 6")[0] ?? "";
92
+ expect(pollingSection).toContain("proactive success message");
84
93
  });
85
94
 
86
- test('polling section includes timeout fallback with resend/restart offer', () => {
95
+ test("polling section includes timeout fallback with resend/restart offer", () => {
87
96
  const pollingSection =
88
- skillContent.split('## Voice Auto-Check Polling')[1]?.split('## Step 6')[0] ?? '';
89
- expect(pollingSection).toContain('timeout');
90
- expect(pollingSection).toContain('resend');
97
+ skillContent
98
+ .split("## Voice Auto-Check Polling")[1]
99
+ ?.split("## Step 6")[0] ?? "";
100
+ expect(pollingSection).toContain("timeout");
101
+ expect(pollingSection).toContain("resend");
91
102
  });
92
103
 
93
- test('polling section includes rebind guard against false-success from pre-existing binding', () => {
104
+ test("polling section includes rebind guard against false-success from pre-existing binding", () => {
94
105
  const pollingSection =
95
- skillContent.split('## Voice Auto-Check Polling')[1]?.split('## Step 6')[0] ?? '';
106
+ skillContent
107
+ .split("## Voice Auto-Check Polling")[1]
108
+ ?.split("## Step 6")[0] ?? "";
96
109
  // Must mention rebind guard concept
97
- expect(pollingSection).toContain('Rebind guard');
110
+ expect(pollingSection).toContain("Rebind guard");
98
111
  // Must instruct not to trust the first bound: true in a rebind flow
99
112
  expect(pollingSection).toContain(
100
- 'do NOT treat the first `bound: true` poll result as success',
113
+ "do NOT treat the first `bound: true` poll result as success",
101
114
  );
102
115
  // Must reference bound_at timestamp comparison as the primary mechanism
103
- expect(pollingSection).toContain('bound_at');
116
+ expect(pollingSection).toContain("bound_at");
104
117
  // Must have a fallback for when bound_at is unavailable
105
- expect(pollingSection).toContain('second poll onward');
118
+ expect(pollingSection).toContain("second poll onward");
106
119
  // Must clarify non-rebind flows are unaffected
107
- expect(pollingSection).toContain('Non-rebind flows');
120
+ expect(pollingSection).toContain("Non-rebind flows");
108
121
  });
109
122
 
110
- test('polling is voice-only — does not apply to SMS or Telegram', () => {
123
+ test("polling is voice-only — does not apply to SMS or Telegram", () => {
111
124
  const pollingSection =
112
- skillContent.split('## Voice Auto-Check Polling')[1]?.split('## Step 6')[0] ?? '';
113
- expect(pollingSection).toContain('voice-only');
114
- expect(pollingSection).toContain('Do NOT poll for SMS or Telegram');
125
+ skillContent
126
+ .split("## Voice Auto-Check Polling")[1]
127
+ ?.split("## Step 6")[0] ?? "";
128
+ expect(pollingSection).toContain("voice-only");
129
+ expect(pollingSection).toContain("Do NOT poll for SMS or Telegram");
115
130
  });
116
131
 
117
132
  test('no instruction requires waiting for user to ask "did it work?"', () => {
@@ -119,23 +134,22 @@ describe('guardian-verify-setup skill — voice auto-followup', () => {
119
134
  // confirm that voice verification worked. The auto-check polling loop
120
135
  // makes this unnecessary.
121
136
  const voiceAutoCheckSection =
122
- skillContent.split('## Voice Auto-Check Polling')[1]?.split('## Step 6')[0] ?? '';
123
- expect(voiceAutoCheckSection).toContain(
124
- 'Do NOT require the user to ask',
125
- );
137
+ skillContent
138
+ .split("## Voice Auto-Check Polling")[1]
139
+ ?.split("## Step 6")[0] ?? "";
140
+ expect(voiceAutoCheckSection).toContain("Do NOT require the user to ask");
126
141
  // The voice bullet in Step 3 should not instruct the assistant to wait
127
142
  // for the user to confirm or ask if it worked. Narrow to just the voice
128
143
  // bullet line to avoid false positives from Telegram's "wait for the
129
144
  // user to confirm they clicked the link" which is unrelated to voice.
130
- const step3Section = skillContent
131
- .split('## Step 3')[1]
132
- ?.split('## Step 4')[0] ?? '';
145
+ const step3Section =
146
+ skillContent.split("## Step 3")[1]?.split("## Step 4")[0] ?? "";
133
147
  const voiceBullet = step3Section
134
- .split('\n')
148
+ .split("\n")
135
149
  .filter((line) => /^\s*-\s+\*\*Voice\*\*/.test(line))
136
- .join('\n');
150
+ .join("\n");
137
151
  expect(voiceBullet).not.toHaveLength(0);
138
- expect(voiceBullet).not.toContain('wait for the user to confirm');
139
- expect(voiceBullet).not.toContain('ask the user if it worked');
152
+ expect(voiceBullet).not.toContain("wait for the user to confirm");
153
+ expect(voiceBullet).not.toContain("ask the user if it worked");
140
154
  });
141
155
  });
@@ -87,6 +87,7 @@ const ctx: ToolContext = {
87
87
  sessionId: 'test-session',
88
88
  conversationId: 'test-conversation',
89
89
  workingDir: '/tmp',
90
+ guardianTrustClass: 'guardian',
90
91
  };
91
92
 
92
93
  function resetMockPage() {
@@ -58,6 +58,7 @@ const ctx: ToolContext = {
58
58
  sessionId: 'test-session',
59
59
  conversationId: 'test-conversation',
60
60
  workingDir: '/tmp',
61
+ guardianTrustClass: 'guardian',
61
62
  };
62
63
 
63
64
  function resetMockPage() {
@@ -67,6 +67,7 @@ const ctx: ToolContext = {
67
67
  sessionId: 'test-session',
68
68
  conversationId: 'test-conversation',
69
69
  workingDir: '/tmp',
70
+ guardianTrustClass: 'guardian',
70
71
  };
71
72
 
72
73
  function resetMockPage() {
@@ -66,6 +66,7 @@ const ctx: ToolContext = {
66
66
  sessionId: 'test-session',
67
67
  conversationId: 'test-conversation',
68
68
  workingDir: '/tmp',
69
+ guardianTrustClass: 'guardian',
69
70
  };
70
71
 
71
72
  function resetMockPage() {
@@ -14,6 +14,7 @@ function makeContext(): ToolContext {
14
14
  workingDir: '/tmp',
15
15
  sessionId: 'test-session',
16
16
  conversationId: 'test-conversation',
17
+ guardianTrustClass: 'guardian',
17
18
  };
18
19
  }
19
20
 
@@ -14,6 +14,7 @@ function makeContext(): ToolContext {
14
14
  workingDir: '/tmp',
15
15
  sessionId: 'test-session',
16
16
  conversationId: 'test-conversation',
17
+ guardianTrustClass: 'guardian',
17
18
  };
18
19
  }
19
20
 
@@ -14,6 +14,7 @@ function makeContext(): ToolContext {
14
14
  workingDir: '/tmp',
15
15
  sessionId: 'test-session',
16
16
  conversationId: 'test-conversation',
17
+ guardianTrustClass: 'guardian',
17
18
  };
18
19
  }
19
20
 
@@ -74,6 +74,7 @@ function makeContext(): ToolContext {
74
74
  workingDir: '/tmp',
75
75
  sessionId: 'test-session',
76
76
  conversationId: 'test-conversation',
77
+ guardianTrustClass: 'guardian',
77
78
  };
78
79
  }
79
80
 
@@ -0,0 +1,207 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+
5
+ import { describe, expect, it } from 'bun:test';
6
+
7
+ /**
8
+ * Guard test: prevent stale lifecycle instructions from being reintroduced
9
+ * into documentation. The canonical lifecycle commands are `vellum wake`,
10
+ * `vellum ps`, and `vellum sleep`. Repo-local slash commands live in
11
+ * `.claude/skills/`, not `.claude/commands/`.
12
+ *
13
+ * See AGENTS.md for the conventions these tests enforce.
14
+ */
15
+
16
+ const REPO_ROOT = join(import.meta.dir, '../../..');
17
+
18
+ describe('lifecycle docs guard', () => {
19
+ it('repo-local commands live in skills directory, not commands directory', () => {
20
+ const staleLocations = [
21
+ '.claude/commands/update.md',
22
+ '.claude/commands/release.md',
23
+ ];
24
+
25
+ const violations = staleLocations.filter((p) =>
26
+ existsSync(join(REPO_ROOT, p)),
27
+ );
28
+
29
+ if (violations.length > 0) {
30
+ const message = [
31
+ 'Found repo-local commands in .claude/commands/ — they should live in .claude/skills/.',
32
+ '',
33
+ 'Stale files:',
34
+ ...violations.map((f) => ` - ${f}`),
35
+ '',
36
+ 'Move them to .claude/skills/<name>/SKILL.md instead.',
37
+ ].join('\n');
38
+
39
+ expect(violations, message).toEqual([]);
40
+ }
41
+
42
+ // Verify the correct locations exist
43
+ const expectedLocations = [
44
+ '.claude/skills/update/SKILL.md',
45
+ '.claude/skills/release/SKILL.md',
46
+ ];
47
+
48
+ const missing = expectedLocations.filter(
49
+ (p) => !existsSync(join(REPO_ROOT, p)),
50
+ );
51
+
52
+ if (missing.length > 0) {
53
+ const message = [
54
+ 'Expected repo-local skill files are missing:',
55
+ ...missing.map((f) => ` - ${f}`),
56
+ ].join('\n');
57
+
58
+ expect(missing, message).toEqual([]);
59
+ }
60
+ });
61
+
62
+ it('key docs reference vellum lifecycle commands', () => {
63
+ const checks: Array<{
64
+ file: string;
65
+ pattern: string;
66
+ description: string;
67
+ }> = [
68
+ {
69
+ file: 'README.md',
70
+ pattern: 'vellum wake\\|vellum ps\\|vellum sleep',
71
+ description:
72
+ 'README.md should mention vellum wake, vellum ps, or vellum sleep',
73
+ },
74
+ {
75
+ file: 'assistant/README.md',
76
+ pattern: 'vellum wake\\|vellum ps',
77
+ description:
78
+ 'assistant/README.md should mention vellum wake or vellum ps',
79
+ },
80
+ {
81
+ file: 'AGENTS.md',
82
+ pattern: 'vellum ps\\|vellum sleep\\|vellum wake',
83
+ description:
84
+ 'AGENTS.md should mention vellum ps, vellum sleep, or vellum wake in the /update command description',
85
+ },
86
+ ];
87
+
88
+ const failures: string[] = [];
89
+
90
+ for (const check of checks) {
91
+ try {
92
+ execSync(`git grep -q '${check.pattern}' -- '${check.file}'`, {
93
+ encoding: 'utf-8',
94
+ cwd: REPO_ROOT,
95
+ });
96
+ } catch {
97
+ failures.push(check.description);
98
+ }
99
+ }
100
+
101
+ if (failures.length > 0) {
102
+ const message = [
103
+ 'Key docs are missing vellum lifecycle command references:',
104
+ '',
105
+ ...failures.map((f) => ` - ${f}`),
106
+ '',
107
+ 'These docs should reference vellum CLI lifecycle commands (wake/ps/sleep).',
108
+ ].join('\n');
109
+
110
+ expect(failures, message).toEqual([]);
111
+ }
112
+ });
113
+
114
+ it('no docs use stale daemon startup as primary instruction', () => {
115
+ // Files that are allowed to contain these patterns
116
+ const allowedPrefixes = [
117
+ 'cli/', // CLI source code
118
+ 'assistant/src/', // assistant runtime source
119
+ 'CLAUDE.md', // project instructions
120
+ 'AGENTS.md', // agent conventions (may reference patterns for context)
121
+ ];
122
+
123
+ const stalePatterns = [
124
+ {
125
+ pattern: 'bun run src/index.ts daemon start',
126
+ label: 'bun run src/index.ts daemon start',
127
+ },
128
+ ];
129
+
130
+ const violations: string[] = [];
131
+
132
+ for (const { pattern, label } of stalePatterns) {
133
+ let grepOutput = '';
134
+ try {
135
+ grepOutput = execSync(
136
+ `git grep -n '${pattern}' -- '*.md'`,
137
+ { encoding: 'utf-8', cwd: REPO_ROOT },
138
+ ).trim();
139
+ } catch {
140
+ // No matches — happy path
141
+ continue;
142
+ }
143
+
144
+ const lines = grepOutput.split('\n').filter((l) => l.length > 0);
145
+
146
+ for (const line of lines) {
147
+ const filePath = line.split(':')[0];
148
+
149
+ // Skip allowed file prefixes
150
+ if (allowedPrefixes.some((prefix) => filePath.startsWith(prefix))) {
151
+ continue;
152
+ }
153
+
154
+ // Skip test files
155
+ if (filePath.includes('__tests__') || filePath.endsWith('.test.ts')) {
156
+ continue;
157
+ }
158
+
159
+ // Check if this specific occurrence is inside a <details> section or
160
+ // a "Development" / "raw bun commands" context by examining only the
161
+ // 10 lines before this match. We extract the line number from the
162
+ // grep output (format "filePath:lineNum:content") and use sed to
163
+ // get a targeted range, so each match is evaluated independently.
164
+ const parts = line.split(':');
165
+ const lineNum = parseInt(parts[1], 10);
166
+
167
+ if (!Number.isNaN(lineNum)) {
168
+ try {
169
+ const startLine = Math.max(1, lineNum - 10);
170
+ const context = execSync(
171
+ `sed -n '${startLine},${lineNum}p' '${filePath}'`,
172
+ { encoding: 'utf-8', cwd: REPO_ROOT },
173
+ );
174
+
175
+ const contextLower = context.toLowerCase();
176
+ const isInDetails = contextLower.includes('<details>');
177
+ const isDevContext =
178
+ contextLower.includes('development:') ||
179
+ contextLower.includes('low-level development') ||
180
+ contextLower.includes('raw bun commands');
181
+
182
+ if (isInDetails || isDevContext) {
183
+ continue;
184
+ }
185
+ } catch {
186
+ // If context extraction fails, treat as a violation
187
+ }
188
+ }
189
+
190
+ violations.push(`${filePath}: contains "${label}" as primary instruction`);
191
+ }
192
+ }
193
+
194
+ if (violations.length > 0) {
195
+ const message = [
196
+ 'Found docs using stale daemon startup patterns as primary instructions.',
197
+ 'Use `vellum wake` / `vellum sleep` instead. Raw bun commands are acceptable',
198
+ 'only in collapsed <details> sections or dev-only contexts.',
199
+ '',
200
+ 'Violations:',
201
+ ...violations.map((v) => ` - ${v}`),
202
+ ].join('\n');
203
+
204
+ expect(violations, message).toEqual([]);
205
+ }
206
+ });
207
+ });
@@ -84,6 +84,7 @@ function makeContext(): ToolContext {
84
84
  workingDir: '/tmp',
85
85
  sessionId: 'test-session',
86
86
  conversationId: 'test-conversation',
87
+ guardianTrustClass: 'guardian',
87
88
  };
88
89
  }
89
90
 
@@ -235,6 +235,7 @@ describe('Story E2E: selfie yesterday -> generated image today', () => {
235
235
  workingDir: sandboxDir,
236
236
  sessionId: 'sess-story',
237
237
  conversationId: threadB.id,
238
+ guardianTrustClass: 'guardian',
238
239
  };
239
240
 
240
241
  const result = await assetSearchTool.execute(
@@ -253,6 +254,7 @@ describe('Story E2E: selfie yesterday -> generated image today', () => {
253
254
  workingDir: sandboxDir,
254
255
  sessionId: 'sess-story',
255
256
  conversationId: threadB.id,
257
+ guardianTrustClass: 'guardian',
256
258
  };
257
259
 
258
260
  const result = await assetSearchTool.execute(
@@ -269,6 +271,7 @@ describe('Story E2E: selfie yesterday -> generated image today', () => {
269
271
  workingDir: sandboxDir,
270
272
  sessionId: 'sess-story',
271
273
  conversationId: threadB.id,
274
+ guardianTrustClass: 'guardian',
272
275
  };
273
276
 
274
277
  const result = await assetMaterializeTool.execute(
@@ -296,6 +299,7 @@ describe('Story E2E: selfie yesterday -> generated image today', () => {
296
299
  workingDir: sandboxDir,
297
300
  sessionId: 'sess-story',
298
301
  conversationId: threadB.id,
302
+ guardianTrustClass: 'guardian',
299
303
  };
300
304
 
301
305
  // Step 3a: Search for the selfie
@@ -486,6 +490,7 @@ describe('Private-thread variant: cross-thread media blocking', () => {
486
490
  workingDir: sandboxDir,
487
491
  sessionId: 'sess-priv-test',
488
492
  conversationId: standardThread.id,
493
+ guardianTrustClass: 'guardian',
489
494
  };
490
495
 
491
496
  const result = await assetSearchTool.execute(
@@ -510,6 +515,7 @@ describe('Private-thread variant: cross-thread media blocking', () => {
510
515
  workingDir: sandboxDir,
511
516
  sessionId: 'sess-priv-test',
512
517
  conversationId: standardThread.id,
518
+ guardianTrustClass: 'guardian',
513
519
  };
514
520
 
515
521
  const result = await assetMaterializeTool.execute(
@@ -533,6 +539,7 @@ describe('Private-thread variant: cross-thread media blocking', () => {
533
539
  workingDir: sandboxDir,
534
540
  sessionId: 'sess-priv-test',
535
541
  conversationId: privateThread.id,
542
+ guardianTrustClass: 'guardian',
536
543
  };
537
544
 
538
545
  const searchResult = await assetSearchTool.execute(
@@ -563,6 +570,7 @@ describe('Private-thread variant: cross-thread media blocking', () => {
563
570
  workingDir: sandboxDir,
564
571
  sessionId: 'sess-priv-test',
565
572
  conversationId: privateThreadB.id,
573
+ guardianTrustClass: 'guardian',
566
574
  };
567
575
 
568
576
  const searchResult = await assetSearchTool.execute(
@@ -48,6 +48,7 @@ describe('messaging-send tool', () => {
48
48
  sessionId: 'sess-1',
49
49
  conversationId: 'conv-1',
50
50
  assistantId: 'ast-alpha',
51
+ guardianTrustClass: 'guardian' as const,
51
52
  },
52
53
  );
53
54
 
@@ -69,6 +69,7 @@ const ctx: ToolContext = {
69
69
  workingDir: '/tmp',
70
70
  sessionId: 'test-session',
71
71
  conversationId: 'test-conversation',
72
+ guardianTrustClass: 'guardian',
72
73
  };
73
74
 
74
75
  function insertPlaybookRow(overrides: Partial<{
@@ -61,6 +61,7 @@ const ctx: ToolContext = {
61
61
  workingDir: '/tmp',
62
62
  sessionId: 'test-session',
63
63
  conversationId: 'test-conversation',
64
+ guardianTrustClass: 'guardian',
64
65
  };
65
66
 
66
67
  function clearPlaybooks(): void {