vellum 0.2.12 → 0.2.14

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 (209) hide show
  1. package/README.md +32 -0
  2. package/bun.lock +2 -2
  3. package/docs/skills.md +4 -4
  4. package/package.json +2 -2
  5. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +213 -3
  6. package/src/__tests__/app-git-history.test.ts +176 -0
  7. package/src/__tests__/app-git-service.test.ts +169 -0
  8. package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
  9. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +8 -8
  10. package/src/__tests__/browser-skill-endstate.test.ts +6 -6
  11. package/src/__tests__/call-bridge.test.ts +105 -13
  12. package/src/__tests__/call-domain.test.ts +163 -0
  13. package/src/__tests__/call-orchestrator.test.ts +171 -0
  14. package/src/__tests__/call-routes-http.test.ts +246 -6
  15. package/src/__tests__/channel-approval-routes.test.ts +438 -0
  16. package/src/__tests__/channel-approval.test.ts +266 -0
  17. package/src/__tests__/channel-approvals.test.ts +393 -0
  18. package/src/__tests__/channel-delivery-store.test.ts +447 -0
  19. package/src/__tests__/checker.test.ts +607 -1048
  20. package/src/__tests__/cli.test.ts +1 -56
  21. package/src/__tests__/config-schema.test.ts +402 -5
  22. package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
  23. package/src/__tests__/conflict-policy.test.ts +121 -0
  24. package/src/__tests__/conflict-store.test.ts +2 -0
  25. package/src/__tests__/contacts-tools.test.ts +3 -3
  26. package/src/__tests__/contradiction-checker.test.ts +99 -1
  27. package/src/__tests__/credential-security-invariants.test.ts +22 -6
  28. package/src/__tests__/credential-vault-unit.test.ts +780 -0
  29. package/src/__tests__/elevenlabs-client.test.ts +271 -0
  30. package/src/__tests__/ephemeral-permissions.test.ts +73 -23
  31. package/src/__tests__/filesystem-tools.test.ts +579 -0
  32. package/src/__tests__/gateway-only-enforcement.test.ts +114 -4
  33. package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
  34. package/src/__tests__/handlers-cu-observation-blob.test.ts +2 -1
  35. package/src/__tests__/handlers-ipc-blob-probe.test.ts +2 -1
  36. package/src/__tests__/handlers-slack-config.test.ts +2 -1
  37. package/src/__tests__/handlers-telegram-config.test.ts +855 -0
  38. package/src/__tests__/handlers-twitter-config.test.ts +141 -1
  39. package/src/__tests__/hooks-runner.test.ts +6 -2
  40. package/src/__tests__/host-file-edit-tool.test.ts +124 -0
  41. package/src/__tests__/host-file-read-tool.test.ts +62 -0
  42. package/src/__tests__/host-file-write-tool.test.ts +59 -0
  43. package/src/__tests__/host-shell-tool.test.ts +251 -0
  44. package/src/__tests__/ingress-reconcile.test.ts +581 -0
  45. package/src/__tests__/ipc-snapshot.test.ts +100 -41
  46. package/src/__tests__/ipc-validate.test.ts +50 -0
  47. package/src/__tests__/key-migration.test.ts +23 -0
  48. package/src/__tests__/memory-regressions.test.ts +99 -0
  49. package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
  50. package/src/__tests__/oauth-callback-registry.test.ts +11 -4
  51. package/src/__tests__/playbook-execution.test.ts +502 -0
  52. package/src/__tests__/playbook-tools.test.ts +4 -6
  53. package/src/__tests__/public-ingress-urls.test.ts +34 -0
  54. package/src/__tests__/qdrant-manager.test.ts +267 -0
  55. package/src/__tests__/recurrence-engine-rruleset.test.ts +97 -0
  56. package/src/__tests__/recurrence-engine.test.ts +9 -0
  57. package/src/__tests__/recurrence-types.test.ts +8 -0
  58. package/src/__tests__/registry.test.ts +1 -1
  59. package/src/__tests__/runtime-runs.test.ts +1 -25
  60. package/src/__tests__/schedule-store.test.ts +16 -14
  61. package/src/__tests__/schedule-tools.test.ts +83 -0
  62. package/src/__tests__/scheduler-recurrence.test.ts +111 -10
  63. package/src/__tests__/secret-allowlist.test.ts +18 -17
  64. package/src/__tests__/secret-ingress-handler.test.ts +11 -0
  65. package/src/__tests__/secret-scanner.test.ts +43 -0
  66. package/src/__tests__/session-conflict-gate.test.ts +442 -6
  67. package/src/__tests__/session-init.benchmark.test.ts +3 -0
  68. package/src/__tests__/session-process-bridge.test.ts +242 -0
  69. package/src/__tests__/session-skill-tools.test.ts +1 -1
  70. package/src/__tests__/shell-identity.test.ts +256 -0
  71. package/src/__tests__/skill-projection.benchmark.test.ts +11 -1
  72. package/src/__tests__/subagent-tools.test.ts +637 -54
  73. package/src/__tests__/task-management-tools.test.ts +936 -0
  74. package/src/__tests__/task-runner.test.ts +2 -2
  75. package/src/__tests__/terminal-tools.test.ts +840 -0
  76. package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
  77. package/src/__tests__/tool-executor.test.ts +85 -151
  78. package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
  79. package/src/__tests__/trust-store.test.ts +28 -453
  80. package/src/__tests__/twilio-provider.test.ts +153 -3
  81. package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
  82. package/src/__tests__/twilio-routes-twiml.test.ts +127 -0
  83. package/src/__tests__/twilio-routes.test.ts +17 -262
  84. package/src/__tests__/twitter-auth-handler.test.ts +2 -1
  85. package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
  86. package/src/__tests__/twitter-cli-routing.test.ts +252 -0
  87. package/src/__tests__/twitter-oauth-client.test.ts +209 -0
  88. package/src/__tests__/workspace-policy.test.ts +213 -0
  89. package/src/calls/call-bridge.ts +92 -19
  90. package/src/calls/call-domain.ts +157 -5
  91. package/src/calls/call-orchestrator.ts +96 -8
  92. package/src/calls/call-store.ts +6 -0
  93. package/src/calls/elevenlabs-client.ts +97 -0
  94. package/src/calls/elevenlabs-config.ts +31 -0
  95. package/src/calls/twilio-provider.ts +91 -0
  96. package/src/calls/twilio-routes.ts +50 -6
  97. package/src/calls/types.ts +3 -1
  98. package/src/calls/voice-quality.ts +114 -0
  99. package/src/cli/twitter.ts +200 -21
  100. package/src/cli.ts +1 -20
  101. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +52 -4
  102. package/src/config/bundled-skills/contacts/tools/contact-search.ts +55 -4
  103. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +61 -4
  104. package/src/config/bundled-skills/messaging/SKILL.md +17 -2
  105. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +4 -1
  106. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  107. package/src/config/bundled-skills/messaging/tools/shared.ts +5 -0
  108. package/src/config/bundled-skills/phone-calls/SKILL.md +207 -19
  109. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +95 -6
  110. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +51 -6
  111. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +73 -6
  112. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +110 -6
  113. package/src/config/bundled-skills/public-ingress/SKILL.md +22 -5
  114. package/src/config/bundled-skills/twitter/SKILL.md +103 -17
  115. package/src/config/defaults.ts +26 -2
  116. package/src/config/schema.ts +178 -9
  117. package/src/config/types.ts +3 -0
  118. package/src/config/vellum-skills/telegram-setup/SKILL.md +56 -61
  119. package/src/daemon/assistant-attachments.ts +4 -2
  120. package/src/daemon/handlers/apps.ts +69 -0
  121. package/src/daemon/handlers/config.ts +543 -24
  122. package/src/daemon/handlers/index.ts +1 -0
  123. package/src/daemon/handlers/sessions.ts +22 -6
  124. package/src/daemon/handlers/shared.ts +2 -1
  125. package/src/daemon/handlers/skills.ts +5 -20
  126. package/src/daemon/ipc-contract-inventory.json +28 -0
  127. package/src/daemon/ipc-contract.ts +168 -10
  128. package/src/daemon/ipc-validate.ts +17 -0
  129. package/src/daemon/lifecycle.ts +2 -0
  130. package/src/daemon/server.ts +78 -72
  131. package/src/daemon/session-attachments.ts +1 -1
  132. package/src/daemon/session-conflict-gate.ts +62 -6
  133. package/src/daemon/session-notifiers.ts +1 -1
  134. package/src/daemon/session-process.ts +62 -3
  135. package/src/daemon/session-tool-setup.ts +1 -2
  136. package/src/daemon/tls-certs.ts +189 -0
  137. package/src/daemon/video-thumbnail.ts +5 -3
  138. package/src/hooks/manager.ts +5 -9
  139. package/src/memory/app-git-service.ts +295 -0
  140. package/src/memory/app-store.ts +21 -0
  141. package/src/memory/conflict-intent.ts +47 -4
  142. package/src/memory/conflict-policy.ts +73 -0
  143. package/src/memory/conflict-store.ts +9 -1
  144. package/src/memory/contradiction-checker.ts +28 -0
  145. package/src/memory/conversation-key-store.ts +15 -0
  146. package/src/memory/db.ts +81 -0
  147. package/src/memory/embedding-local.ts +3 -13
  148. package/src/memory/external-conversation-store.ts +234 -0
  149. package/src/memory/job-handlers/conflict.ts +22 -2
  150. package/src/memory/jobs-worker.ts +67 -28
  151. package/src/memory/runs-store.ts +54 -7
  152. package/src/memory/schema.ts +20 -0
  153. package/src/messaging/provider.ts +9 -0
  154. package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
  155. package/src/messaging/providers/telegram-bot/client.ts +104 -0
  156. package/src/messaging/providers/telegram-bot/types.ts +15 -0
  157. package/src/messaging/registry.ts +1 -0
  158. package/src/permissions/checker.ts +48 -44
  159. package/src/permissions/defaults.ts +11 -0
  160. package/src/permissions/prompter.ts +0 -4
  161. package/src/permissions/shell-identity.ts +227 -0
  162. package/src/permissions/trust-store.ts +76 -53
  163. package/src/permissions/types.ts +0 -19
  164. package/src/permissions/workspace-policy.ts +114 -0
  165. package/src/providers/retry.ts +12 -37
  166. package/src/runtime/assistant-event-hub.ts +41 -4
  167. package/src/runtime/channel-approval-parser.ts +60 -0
  168. package/src/runtime/channel-approval-types.ts +71 -0
  169. package/src/runtime/channel-approvals.ts +145 -0
  170. package/src/runtime/gateway-client.ts +16 -0
  171. package/src/runtime/http-server.ts +29 -9
  172. package/src/runtime/routes/call-routes.ts +52 -2
  173. package/src/runtime/routes/channel-routes.ts +296 -16
  174. package/src/runtime/routes/conversation-routes.ts +12 -5
  175. package/src/runtime/routes/events-routes.ts +97 -28
  176. package/src/runtime/routes/run-routes.ts +2 -7
  177. package/src/runtime/run-orchestrator.ts +0 -3
  178. package/src/schedule/recurrence-engine.ts +26 -2
  179. package/src/schedule/recurrence-types.ts +1 -1
  180. package/src/schedule/schedule-store.ts +12 -3
  181. package/src/security/secret-scanner.ts +7 -0
  182. package/src/tasks/ephemeral-permissions.ts +0 -2
  183. package/src/tasks/task-scheduler.ts +2 -1
  184. package/src/tools/calls/call-start.ts +8 -0
  185. package/src/tools/execution-target.ts +21 -0
  186. package/src/tools/execution-timeout.ts +49 -0
  187. package/src/tools/executor.ts +6 -135
  188. package/src/tools/network/web-search.ts +9 -32
  189. package/src/tools/policy-context.ts +29 -0
  190. package/src/tools/schedule/update.ts +8 -1
  191. package/src/tools/terminal/parser.ts +16 -18
  192. package/src/tools/types.ts +4 -11
  193. package/src/twitter/oauth-client.ts +102 -0
  194. package/src/twitter/router.ts +101 -0
  195. package/src/util/debounce.ts +88 -0
  196. package/src/util/network-info.ts +47 -0
  197. package/src/util/platform.ts +29 -4
  198. package/src/util/promise-guard.ts +37 -0
  199. package/src/util/retry.ts +98 -0
  200. package/src/util/truncate.ts +1 -1
  201. package/src/workspace/git-service.ts +129 -112
  202. package/src/tools/contacts/contact-merge.ts +0 -55
  203. package/src/tools/contacts/contact-search.ts +0 -58
  204. package/src/tools/contacts/contact-upsert.ts +0 -64
  205. package/src/tools/playbooks/index.ts +0 -4
  206. package/src/tools/playbooks/playbook-create.ts +0 -96
  207. package/src/tools/playbooks/playbook-delete.ts +0 -52
  208. package/src/tools/playbooks/playbook-list.ts +0 -74
  209. package/src/tools/playbooks/playbook-update.ts +0 -111
@@ -0,0 +1,121 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import {
3
+ isConflictKindEligible,
4
+ isConflictKindPairEligible,
5
+ isTransientTrackingStatement,
6
+ isDurableInstructionStatement,
7
+ isStatementConflictEligible,
8
+ } from '../memory/conflict-policy.js';
9
+
10
+ describe('conflict-policy', () => {
11
+ const config = { conflictableKinds: ['preference', 'profile', 'constraint'] };
12
+
13
+ describe('isConflictKindEligible', () => {
14
+ test('returns true for eligible kind', () => {
15
+ expect(isConflictKindEligible('preference', config)).toBe(true);
16
+ expect(isConflictKindEligible('profile', config)).toBe(true);
17
+ expect(isConflictKindEligible('constraint', config)).toBe(true);
18
+ });
19
+
20
+ test('returns false for ineligible kind', () => {
21
+ expect(isConflictKindEligible('project', config)).toBe(false);
22
+ expect(isConflictKindEligible('todo', config)).toBe(false);
23
+ expect(isConflictKindEligible('fact', config)).toBe(false);
24
+ });
25
+ });
26
+
27
+ describe('isConflictKindPairEligible', () => {
28
+ test('returns true when both kinds are eligible', () => {
29
+ expect(isConflictKindPairEligible('preference', 'profile', config)).toBe(true);
30
+ });
31
+
32
+ test('returns false when existing kind is ineligible', () => {
33
+ expect(isConflictKindPairEligible('project', 'preference', config)).toBe(false);
34
+ });
35
+
36
+ test('returns false when candidate kind is ineligible', () => {
37
+ expect(isConflictKindPairEligible('preference', 'todo', config)).toBe(false);
38
+ });
39
+
40
+ test('returns false when both kinds are ineligible', () => {
41
+ expect(isConflictKindPairEligible('project', 'todo', config)).toBe(false);
42
+ });
43
+ });
44
+
45
+ describe('isTransientTrackingStatement', () => {
46
+ test('detects PR URLs', () => {
47
+ expect(isTransientTrackingStatement('Track https://github.com/org/repo/pull/5526')).toBe(true);
48
+ });
49
+
50
+ test('detects issue/ticket references', () => {
51
+ expect(isTransientTrackingStatement('Track PR #5526 and #5525')).toBe(true);
52
+ expect(isTransientTrackingStatement('See issue #42 for details')).toBe(true);
53
+ expect(isTransientTrackingStatement('Filed ticket 1234')).toBe(true);
54
+ });
55
+
56
+ test('detects tracking language', () => {
57
+ expect(isTransientTrackingStatement('While we wait for CI to pass')).toBe(true);
58
+ expect(isTransientTrackingStatement('This PR needs review')).toBe(true);
59
+ });
60
+
61
+ test('does not flag generic time words as transient', () => {
62
+ expect(isTransientTrackingStatement('The deadline is today')).toBe(false);
63
+ expect(isTransientTrackingStatement('I need this right now')).toBe(false);
64
+ });
65
+
66
+ test('does not flag durable statements', () => {
67
+ expect(isTransientTrackingStatement('Always answer with concise bullet points')).toBe(false);
68
+ expect(isTransientTrackingStatement('User prefers dark mode')).toBe(false);
69
+ });
70
+
71
+ test('does not false-positive on non-PR URLs', () => {
72
+ expect(isTransientTrackingStatement('Visit https://example.com for docs')).toBe(false);
73
+ });
74
+ });
75
+
76
+ describe('isDurableInstructionStatement', () => {
77
+ test('detects durable instruction cues', () => {
78
+ expect(isDurableInstructionStatement('Always answer with concise bullet points')).toBe(true);
79
+ expect(isDurableInstructionStatement('Never use semicolons in JavaScript')).toBe(true);
80
+ expect(isDurableInstructionStatement('Use concise format for status updates')).toBe(true);
81
+ expect(isDurableInstructionStatement('The default database is Postgres')).toBe(true);
82
+ });
83
+
84
+ test('rejects statements without durable cues', () => {
85
+ expect(isDurableInstructionStatement('Check the build output')).toBe(false);
86
+ expect(isDurableInstructionStatement('Run the migration script')).toBe(false);
87
+ });
88
+ });
89
+
90
+ describe('isStatementConflictEligible', () => {
91
+ test('rejects transient statements for any kind', () => {
92
+ expect(isStatementConflictEligible('preference', 'Track PR #5526')).toBe(false);
93
+ expect(isStatementConflictEligible('instruction', 'This PR needs review')).toBe(false);
94
+ });
95
+
96
+ test('accepts durable instruction statements', () => {
97
+ expect(isStatementConflictEligible('instruction', 'Always use TypeScript strict mode')).toBe(true);
98
+ expect(isStatementConflictEligible('style', 'Default to concise format')).toBe(true);
99
+ });
100
+
101
+ test('rejects non-durable instruction statements', () => {
102
+ expect(isStatementConflictEligible('instruction', 'Run the build first')).toBe(false);
103
+ expect(isStatementConflictEligible('style', 'Check the output')).toBe(false);
104
+ });
105
+
106
+ test('accepts non-transient statements for non-instruction kinds', () => {
107
+ expect(isStatementConflictEligible('preference', 'User prefers dark mode')).toBe(true);
108
+ expect(isStatementConflictEligible('fact', 'User works at Acme Corp')).toBe(true);
109
+ });
110
+
111
+ test('rejects kinds not in conflictableKinds when config is provided', () => {
112
+ const policyConfig = { conflictableKinds: ['preference', 'profile'] };
113
+ expect(isStatementConflictEligible('fact', 'User works at Acme Corp', policyConfig)).toBe(false);
114
+ expect(isStatementConflictEligible('preference', 'User prefers dark mode', policyConfig)).toBe(true);
115
+ });
116
+
117
+ test('skips kind check when config is omitted', () => {
118
+ expect(isStatementConflictEligible('fact', 'User works at Acme Corp')).toBe(true);
119
+ });
120
+ });
121
+ });
@@ -233,6 +233,8 @@ describe('conflict-store', () => {
233
233
  expect(details).toHaveLength(1);
234
234
  expect(details[0].existingStatement).toBe('Existing statement details');
235
235
  expect(details[0].candidateStatement).toBe('Candidate statement details');
236
+ expect(details[0].existingKind).toBe('fact');
237
+ expect(details[0].candidateKind).toBe('fact');
236
238
  });
237
239
 
238
240
  test('applyConflictResolution keeps candidate and resolves conflict row', () => {
@@ -32,9 +32,9 @@ mock.module('../config/loader.js', () => ({
32
32
  import type { Database } from 'bun:sqlite';
33
33
  import { initializeDb, getDb, resetDb } from '../memory/db.js';
34
34
  import type { ToolContext } from '../tools/types.js';
35
- import { executeContactUpsert } from '../tools/contacts/contact-upsert.js';
36
- import { executeContactSearch } from '../tools/contacts/contact-search.js';
37
- import { executeContactMerge } from '../tools/contacts/contact-merge.js';
35
+ import { executeContactUpsert } from '../config/bundled-skills/contacts/tools/contact-upsert.js';
36
+ import { executeContactSearch } from '../config/bundled-skills/contacts/tools/contact-search.js';
37
+ import { executeContactMerge } from '../config/bundled-skills/contacts/tools/contact-merge.js';
38
38
 
39
39
  initializeDb();
40
40
 
@@ -51,9 +51,18 @@ mock.module('../util/logger.js', () => ({
51
51
  }),
52
52
  }));
53
53
 
54
+ let mockConflictableKinds: string[] = [
55
+ 'preference', 'profile', 'constraint', 'instruction', 'style',
56
+ ];
57
+
54
58
  mock.module('../config/loader.js', () => ({
55
59
  getConfig: () => ({
56
60
  apiKeys: { anthropic: 'test-key' },
61
+ memory: {
62
+ conflicts: {
63
+ conflictableKinds: mockConflictableKinds,
64
+ },
65
+ },
57
66
  }),
58
67
  }));
59
68
 
@@ -67,6 +76,9 @@ beforeAll(() => {
67
76
 
68
77
  beforeEach(() => {
69
78
  classifyCallCount = 0;
79
+ mockConflictableKinds = [
80
+ 'preference', 'profile', 'constraint', 'instruction', 'style',
81
+ ];
70
82
  const db = getDb();
71
83
  db.run('DELETE FROM memory_item_conflicts');
72
84
  db.run('DELETE FROM memory_item_sources');
@@ -88,12 +100,13 @@ function insertMemoryItem(params: {
88
100
  statement: string;
89
101
  scopeId?: string;
90
102
  status?: 'active' | 'pending_clarification';
103
+ kind?: string;
91
104
  }): void {
92
105
  const now = Date.now();
93
106
  const db = getDb();
94
107
  db.insert(memoryItems).values({
95
108
  id: params.id,
96
- kind: 'preference',
109
+ kind: params.kind ?? 'preference',
97
110
  subject: 'framework preference',
98
111
  statement: params.statement,
99
112
  status: params.status ?? 'active',
@@ -213,4 +226,89 @@ describe('checkContradictions', () => {
213
226
  expect(candidate?.status).toBe('active');
214
227
  expect(conflicts).toHaveLength(0);
215
228
  });
229
+
230
+ test('project kind ambiguous contradiction does not generate pending conflict with default config', async () => {
231
+ nextRelationship = 'ambiguous_contradiction';
232
+ nextExplanation = 'Project items may conflict but are not durable.';
233
+
234
+ insertMemoryItem({
235
+ id: 'item-existing-project',
236
+ statement: 'The backend uses Node.js.',
237
+ kind: 'project',
238
+ });
239
+ insertMemoryItem({
240
+ id: 'item-candidate-project',
241
+ statement: 'The backend uses Deno.',
242
+ kind: 'project',
243
+ });
244
+
245
+ await checkContradictions('item-candidate-project');
246
+
247
+ expect(classifyCallCount).toBe(0);
248
+ const db = getDb();
249
+ const conflicts = db.select().from(memoryItemConflicts).all();
250
+ expect(conflicts).toHaveLength(0);
251
+ });
252
+
253
+ test('skips classification when item kind is not in conflictableKinds', async () => {
254
+ mockConflictableKinds = ['instruction', 'style'];
255
+ nextRelationship = 'ambiguous_contradiction';
256
+
257
+ insertMemoryItem({
258
+ id: 'item-existing-ineligible',
259
+ statement: 'User prefers React for frontend work.',
260
+ });
261
+ insertMemoryItem({
262
+ id: 'item-candidate-ineligible',
263
+ statement: 'User prefers Vue for frontend work.',
264
+ });
265
+
266
+ await checkContradictions('item-candidate-ineligible');
267
+
268
+ expect(classifyCallCount).toBe(0);
269
+ const db = getDb();
270
+ const conflicts = db.select().from(memoryItemConflicts).all();
271
+ expect(conflicts).toHaveLength(0);
272
+ });
273
+
274
+ test('skips classification when candidate statement contains PR-tracking content', async () => {
275
+ nextRelationship = 'ambiguous_contradiction';
276
+
277
+ insertMemoryItem({
278
+ id: 'item-existing-pr-tracking',
279
+ statement: 'Track PR #5526 for review.',
280
+ });
281
+ insertMemoryItem({
282
+ id: 'item-candidate-pr-tracking',
283
+ statement: 'Track PR #5525 for review.',
284
+ });
285
+
286
+ await checkContradictions('item-candidate-pr-tracking');
287
+
288
+ expect(classifyCallCount).toBe(0);
289
+ const db = getDb();
290
+ const conflicts = db.select().from(memoryItemConflicts).all();
291
+ expect(conflicts).toHaveLength(0);
292
+ });
293
+
294
+ test('durable preference contradiction still runs normal flow', async () => {
295
+ nextRelationship = 'ambiguous_contradiction';
296
+ nextExplanation = 'Both are valid preferences that conflict.';
297
+
298
+ insertMemoryItem({
299
+ id: 'item-existing-durable',
300
+ statement: 'User prefers React for frontend work.',
301
+ });
302
+ insertMemoryItem({
303
+ id: 'item-candidate-durable',
304
+ statement: 'User prefers Vue for frontend work.',
305
+ });
306
+
307
+ await checkContradictions('item-candidate-durable');
308
+
309
+ expect(classifyCallCount).toBe(1);
310
+ const db = getDb();
311
+ const conflicts = db.select().from(memoryItemConflicts).all();
312
+ expect(conflicts).toHaveLength(1);
313
+ });
216
314
  });
@@ -185,10 +185,17 @@ describe('Invariant 2: no generic plaintext secret read API', () => {
185
185
  'email/providers/index.ts', // email provider API key lookup
186
186
  'tools/network/script-proxy/session-manager.ts', // proxy credential injection at runtime
187
187
  'messaging/registry.ts', // checks stored credentials for connected providers
188
+ 'calls/call-domain.ts', // caller identity resolution (user phone number lookup)
189
+ 'calls/elevenlabs-config.ts', // ElevenLabs voice quality API key lookup
188
190
  'calls/twilio-config.ts', // call infrastructure credential lookup
189
191
  'calls/twilio-provider.ts', // call infrastructure credential lookup
192
+ 'cli/config-commands.ts', // CLI credential management commands
190
193
  'runtime/http-server.ts', // HTTP server credential lookup
191
194
  'daemon/handlers/twitter-auth.ts', // Twitter OAuth token storage
195
+ 'twitter/oauth-client.ts', // Twitter OAuth API client (reads access token for API calls)
196
+ 'calls/elevenlabs-config.ts', // ElevenLabs credential lookup
197
+ 'cli/config-commands.ts', // CLI config management
198
+ 'messaging/providers/telegram-bot/adapter.ts', // Telegram bot token lookup for connectivity check
192
199
  ]);
193
200
 
194
201
  const thisDir = dirname(fileURLToPath(import.meta.url));
@@ -267,18 +274,27 @@ describe('Invariant 3: secrets never logged in plaintext', () => {
267
274
  });
268
275
  } else if (tc.component === 'ipc_decode') {
269
276
  // PR 24 — IPC decode log hygiene: the TS daemon's IPC parser must
270
- // not have any logging that could leak raw message content
277
+ // not log raw message content that could contain secrets.
278
+ // Logging metadata (line length, error type) is acceptable; logging
279
+ // the raw line, trimmed content, or error.message is not.
271
280
  test(`${tc.label}`, () => {
272
281
  const thisDir = dirname(fileURLToPath(import.meta.url));
273
282
  const ipcSrc = readFileSync(
274
283
  resolve(thisDir, '../daemon/ipc-protocol.ts'),
275
284
  'utf-8',
276
285
  );
277
- // The IPC parser must not use a logger at all it handles raw
278
- // bytes that could contain secrets in malformed messages. Verify
279
- // no getLogger import and no log.* calls exist in the source.
280
- expect(ipcSrc).not.toContain('getLogger');
281
- expect(ipcSrc).not.toMatch(/\blog\.\w+\(/);
286
+ // Verify log calls never include raw content fieldsonly safe
287
+ // metadata like lineLength and errorType are permitted.
288
+ // `trimmed.length` is safe (numeric); `trimmed` alone would leak raw content.
289
+ // Use [^\n]* instead of [^)]* so that inner parentheses (e.g.
290
+ // helper calls like formatErr(err)) don't terminate the match
291
+ // early — avoiding false negatives — while still scoping each
292
+ // pattern to a single line (no cross-statement matching).
293
+ expect(ipcSrc).not.toMatch(/\blog\.\w+\([^\n]*[{,]\s*trimmed[^.]/);
294
+ expect(ipcSrc).not.toMatch(/\blog\.\w+\([^\n]*[{,]\s*line[^L]/);
295
+ expect(ipcSrc).not.toMatch(/\blog\.\w+\([^\n]*[{,]\s*data\b/);
296
+ expect(ipcSrc).not.toMatch(/\blog\.\w+\([^\n]*[{,]\s*buffer\b/);
297
+ expect(ipcSrc).not.toMatch(/\blog\.\w+\([^\n]*err\.message\b/);
282
298
  });
283
299
  } else {
284
300
  // PR 25 — secret prompter log hygiene: verify the prompter source