@inetafrica/open-claudia 2.14.8 → 3.0.0

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 (157) hide show
  1. package/.env.example +30 -4
  2. package/CHANGELOG.md +21 -0
  3. package/README.md +87 -66
  4. package/bin/agent.js +44 -18
  5. package/bin/cli.js +30 -28
  6. package/bin/dream.js +5 -11
  7. package/bin/loopback-client.js +29 -5
  8. package/bin/schedule.js +19 -5
  9. package/bin/tool.js +23 -5
  10. package/bot-agent.js +5 -2350
  11. package/bot.js +105 -7
  12. package/channels/telegram/adapter.js +6 -1
  13. package/channels/voice/adapter.js +1 -0
  14. package/channels/voice/manage.js +43 -5
  15. package/config-dir.js +3 -11
  16. package/core/actions.js +149 -40
  17. package/core/approvals.js +136 -29
  18. package/core/auth-flow.js +103 -19
  19. package/core/config-dir.js +19 -0
  20. package/core/config.js +127 -69
  21. package/core/doctor.js +111 -57
  22. package/core/dream.js +219 -62
  23. package/core/enforcer.js +0 -0
  24. package/core/entities.js +2 -1
  25. package/core/fsutil.js +55 -0
  26. package/core/handlers.js +503 -217
  27. package/core/ideas.js +2 -1
  28. package/core/identity.js +8 -11
  29. package/core/io.js +24 -3
  30. package/core/jobs.js +589 -72
  31. package/core/lessons.js +3 -2
  32. package/core/loopback.js +45 -8
  33. package/core/memory-mutation-queue.js +241 -0
  34. package/core/migration-backup.js +1060 -0
  35. package/core/pack-review.js +295 -88
  36. package/core/packs.js +4 -3
  37. package/core/persona.js +2 -1
  38. package/core/process-tree.js +19 -2
  39. package/core/provider-migration.js +39 -0
  40. package/core/provider-status.js +80 -0
  41. package/core/providers/claude-events.js +117 -0
  42. package/core/providers/claude-hook.js +114 -0
  43. package/core/providers/claude.js +296 -0
  44. package/core/providers/codex-events.js +121 -0
  45. package/core/providers/codex-hook.js +280 -0
  46. package/core/providers/codex.js +286 -0
  47. package/core/providers/contract.js +153 -0
  48. package/core/providers/env.js +148 -0
  49. package/core/providers/events.js +170 -0
  50. package/core/providers/hook-command.js +22 -0
  51. package/core/providers/index.js +240 -0
  52. package/core/providers/utility-policy.js +269 -0
  53. package/core/recall/classic.js +2 -2
  54. package/core/recall/discoverer.js +71 -22
  55. package/core/recall/metrics.js +27 -1
  56. package/core/recall/tuning.js +4 -1
  57. package/core/recall/warm-walker.js +151 -108
  58. package/core/recall-filter.js +86 -61
  59. package/core/router.js +79 -7
  60. package/core/run-context.js +185 -0
  61. package/core/runner.js +1415 -1282
  62. package/core/scheduler.js +515 -210
  63. package/core/side-chat.js +346 -0
  64. package/core/state.js +1096 -95
  65. package/core/subagent.js +72 -98
  66. package/core/system-prompt.js +462 -279
  67. package/core/tool-guard.js +73 -39
  68. package/core/tools.js +22 -5
  69. package/core/transcripts.js +30 -1
  70. package/core/usage-log.js +19 -4
  71. package/core/utility-agent.js +654 -0
  72. package/core/web-auth.js +29 -13
  73. package/core/web-sessions.js +78 -0
  74. package/docs/CHANNEL_DESIGN.md +181 -0
  75. package/docs/MULTI_CHANNEL_PLAN.md +254 -0
  76. package/docs/PROVIDER_MIGRATION.md +67 -0
  77. package/health.js +50 -39
  78. package/package.json +51 -4
  79. package/setup.js +198 -38
  80. package/soul.md +39 -0
  81. package/test-ability-extraction.js +5 -0
  82. package/test-approval-async.js +63 -4
  83. package/test-cursor-removal.js +334 -0
  84. package/test-delivery-contract.js +88 -0
  85. package/test-enforcer.js +70 -27
  86. package/test-fixtures/current-provider-parity.json +132 -0
  87. package/test-fixtures/fake-agent-cli.js +477 -0
  88. package/test-fixtures/migrations/claude-only/jobs.json +3 -0
  89. package/test-fixtures/migrations/claude-only/sessions.json +5 -0
  90. package/test-fixtures/migrations/claude-only/state.json +5 -0
  91. package/test-fixtures/migrations/cursor-selected/crons.json +3 -0
  92. package/test-fixtures/migrations/cursor-selected/jobs.json +3 -0
  93. package/test-fixtures/migrations/cursor-selected/sessions.json +5 -0
  94. package/test-fixtures/migrations/cursor-selected/state.json +6 -0
  95. package/test-fixtures/migrations/dual-provider/jobs.json +3 -0
  96. package/test-fixtures/migrations/dual-provider/sessions.json +7 -0
  97. package/test-fixtures/migrations/dual-provider/state.json +10 -0
  98. package/test-fixtures/migrations/malformed-partial/jobs.json +1 -0
  99. package/test-fixtures/migrations/malformed-partial/sessions.json +1 -0
  100. package/test-fixtures/migrations/malformed-partial/sessions.json.bak +3 -0
  101. package/test-fixtures/migrations/malformed-partial/state.json +1 -0
  102. package/test-fixtures/migrations/malformed-partial/state.json.bak +5 -0
  103. package/test-fixtures/migrations/missing-project/jobs.json +3 -0
  104. package/test-fixtures/migrations/missing-project/sessions.json +3 -0
  105. package/test-fixtures/migrations/missing-project/state.json +4 -0
  106. package/test-fixtures/migrations/multi-user/jobs.json +4 -0
  107. package/test-fixtures/migrations/multi-user/sessions.json +4 -0
  108. package/test-fixtures/migrations/multi-user/state.json +6 -0
  109. package/test-fixtures/provider-event-samples.json +17 -0
  110. package/test-learning-e2e.js +5 -0
  111. package/test-memory-mutation-queue.js +191 -0
  112. package/test-provider-boot-matrix.js +399 -0
  113. package/test-provider-bootstrap.js +158 -0
  114. package/test-provider-capabilities.js +232 -0
  115. package/test-provider-claude.js +206 -0
  116. package/test-provider-codex.js +228 -0
  117. package/test-provider-compaction.js +371 -0
  118. package/test-provider-core-prompt.js +264 -0
  119. package/test-provider-coupling-audit.js +90 -0
  120. package/test-provider-dream.js +312 -0
  121. package/test-provider-enforcer.js +252 -0
  122. package/test-provider-env-isolation.js +150 -0
  123. package/test-provider-events.js +141 -0
  124. package/test-provider-fixture.js +332 -0
  125. package/test-provider-gateway.js +508 -0
  126. package/test-provider-language.js +89 -0
  127. package/test-provider-migration-backup.js +424 -0
  128. package/test-provider-pack-review.js +436 -0
  129. package/test-provider-parity-e2e.js +537 -0
  130. package/test-provider-prompt-parity.js +89 -0
  131. package/test-provider-recall.js +271 -0
  132. package/test-provider-registry.js +251 -0
  133. package/test-provider-resume-parity.js +41 -0
  134. package/test-provider-run-lifecycle.js +349 -0
  135. package/test-provider-runner.js +271 -0
  136. package/test-provider-scheduler.js +689 -0
  137. package/test-provider-session-commands.js +185 -0
  138. package/test-provider-session-history.js +205 -0
  139. package/test-provider-side-chat.js +337 -0
  140. package/test-provider-state-migration.js +316 -0
  141. package/test-provider-stream-decoder.js +69 -0
  142. package/test-provider-subagent.js +228 -0
  143. package/test-provider-tool-hooks.js +360 -0
  144. package/test-recall-discoverer.js +1 -0
  145. package/test-recall-engine.js +3 -3
  146. package/test-recall-evolution.js +18 -0
  147. package/test-run-lock.js +63 -0
  148. package/test-runner-watchdog-static.js +16 -8
  149. package/test-single-runtime.js +35 -0
  150. package/test-telegram-poll-recovery.js +56 -0
  151. package/test-tool-guard.js +56 -0
  152. package/test-tools.js +19 -0
  153. package/test-unified-identity.js +3 -1
  154. package/test-usage-accounting.js +92 -20
  155. package/test-utility-provider-policy.js +486 -0
  156. package/test-web-sessions.js +74 -0
  157. package/web.js +159 -45
@@ -0,0 +1,508 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const assert = require("assert");
6
+ const fs = require("fs");
7
+ const os = require("os");
8
+ const path = require("path");
9
+ const { spawnSync } = require("child_process");
10
+
11
+ function installStub(relativePath, exports) {
12
+ const filename = require.resolve(relativePath);
13
+ require.cache[filename] = { id: filename, filename, loaded: true, exports };
14
+ }
15
+
16
+ function deferred() {
17
+ let resolve;
18
+ const promise = new Promise((res) => { resolve = res; });
19
+ return { promise, resolve };
20
+ }
21
+
22
+ function nextTurn() {
23
+ return new Promise((resolve) => setImmediate(resolve));
24
+ }
25
+
26
+ async function routerProbe() {
27
+ const projectA = path.join(process.env.WORKSPACE, "project-a");
28
+ const projectB = path.join(process.env.WORKSPACE, "project-b");
29
+ fs.mkdirSync(projectA, { recursive: true });
30
+ fs.mkdirSync(projectB, { recursive: true });
31
+ const state = {
32
+ userId: "fixture-user",
33
+ currentSession: { name: "project-a", dir: projectA },
34
+ settings: { backend: "claude", model: "claude-fixture" },
35
+ activeSessions: { "project-a": { claude: "claude-session", codex: "codex-session" } },
36
+ providerSettings: {
37
+ claude: { model: "claude-fixture" },
38
+ codex: { model: "codex-fixture" },
39
+ },
40
+ messageQueue: [],
41
+ pendingCodexAuthProcess: null,
42
+ pendingClaudeAuthProcess: null,
43
+ pendingVaultUnlock: false,
44
+ onboardingStep: null,
45
+ lastInputWasVoice: false,
46
+ };
47
+ const admissions = [];
48
+ const runs = [];
49
+ const sends = [];
50
+ let runSequence = 0;
51
+
52
+ installStub("./core/state", {
53
+ currentState: () => state,
54
+ autoLinkOwnerChannel: () => null,
55
+ });
56
+ installStub("./core/runner", {
57
+ admitRunContext(prompt, cwd, replyToMsgId, opts = {}) {
58
+ const snapshot = Object.freeze({
59
+ runId: `gateway-run-${++runSequence}`,
60
+ provider: state.settings.backend,
61
+ providerSettings: Object.freeze({ ...state.providerSettings[state.settings.backend] }),
62
+ project: Object.freeze({ ...state.currentSession }),
63
+ sessionId: state.activeSessions[state.currentSession.name]?.[state.settings.backend] || null,
64
+ cwd,
65
+ replyToMsgId,
66
+ userPrompt: prompt,
67
+ origin: Object.freeze({ transport: opts.transport || null, channelId: "fixture-channel" }),
68
+ });
69
+ admissions.push(snapshot);
70
+ return snapshot;
71
+ },
72
+ async runAgent(prompt, cwd, replyToMsgId, opts = {}) {
73
+ runs.push({ prompt, cwd, replyToMsgId, opts });
74
+ return {
75
+ ok: true,
76
+ status: "succeeded",
77
+ runId: opts.runContext?.runId,
78
+ provider: opts.runContext?.provider,
79
+ project: opts.runContext?.project,
80
+ text: "fixture response",
81
+ delivery: { ok: true },
82
+ };
83
+ },
84
+ });
85
+ installStub("./core/media", { transcribeAudio: (file) => `transcript:${path.basename(file)}` });
86
+ installStub("./core/io", {
87
+ send: async (text, opts) => { sends.push({ text, opts }); return { ok: true, messageId: String(sends.length), editable: true }; },
88
+ deleteMessage: async () => true,
89
+ });
90
+ installStub("./core/access", { isChatAuthorized: () => true });
91
+ installStub("./core/onboarding", { isOnboarded: () => true, handleOnboarding: async () => {} });
92
+ installStub("./core/commands", { dispatch: async () => ({ matched: false }) });
93
+ installStub("./core/actions", { handleAction: async () => {} });
94
+ installStub("./core/handlers", {});
95
+ installStub("./core/intro-flow", { handleInbound: async () => {} });
96
+ installStub("./core/vault-store", {
97
+ vault: { exists: () => false, isUnlocked: () => false, unlock: () => false, list: () => ({}), get: () => null },
98
+ });
99
+ installStub("./core/auth-flow", {
100
+ looksLikeOpenAIKey: () => false,
101
+ looksLikeClaudeToken: () => false,
102
+ looksLikeClaudeAuthReply: () => false,
103
+ saveClaudeOAuthToken: () => {},
104
+ saveCodexApiKeyWithCli: async () => ({ ok: false, output: "" }),
105
+ clearPendingClaudeAuth: () => {},
106
+ clearPendingCodexAuth: () => {},
107
+ sendClaudeAuthStatusSummary: async () => {},
108
+ sendCodexAuthStatusSummary: async () => {},
109
+ });
110
+ installStub("./core/channel-wizard", {
111
+ tryCaptureOwner: async () => false,
112
+ isAwaiting: () => false,
113
+ handleText: async () => false,
114
+ handleAction: async () => false,
115
+ cancel: async () => {},
116
+ });
117
+
118
+ const router = require("./core/router");
119
+ let messageId = 0;
120
+ function adapter(type = "telegram", download) {
121
+ return {
122
+ id: `${type}-fixture`,
123
+ type,
124
+ typing: async () => {},
125
+ downloadMedia: download || (async (media) => path.join(process.env.OPEN_CLAUDIA_CONFIG_DIR, media.fileName || `${media.type || "media"}-${messageId}`)),
126
+ };
127
+ }
128
+ function envelope(type, overrides = {}) {
129
+ messageId += 1;
130
+ const channelAdapter = overrides.adapter || adapter(type === "voice" ? "voice" : "telegram");
131
+ return {
132
+ type,
133
+ adapter: channelAdapter,
134
+ channelId: "fixture-channel",
135
+ canonicalUserId: "fixture-user",
136
+ userId: "fixture-user",
137
+ messageId: `message-${messageId}`,
138
+ raw: null,
139
+ ...overrides,
140
+ adapter: channelAdapter,
141
+ };
142
+ }
143
+ function resetRouting() {
144
+ state.currentSession = { name: "project-a", dir: projectA };
145
+ state.settings = { backend: "claude", model: "claude-fixture" };
146
+ state.activeSessions["project-a"].claude = "claude-session";
147
+ }
148
+
149
+ // Voice admission precedes download/transcription. A switch during media
150
+ // preparation cannot retarget the admitted provider or project.
151
+ {
152
+ resetRouting();
153
+ const gate = deferred();
154
+ const voice = envelope("voice", {
155
+ adapter: adapter("voice", async () => { await gate.promise; return path.join(process.env.OPEN_CLAUDIA_CONFIG_DIR, "voice.ogg"); }),
156
+ media: [{ type: "voice", size: 10 }],
157
+ });
158
+ const pending = router.onMessage(voice);
159
+ await nextTurn();
160
+ const admitted = admissions.at(-1);
161
+ assert.strictEqual(admitted.provider, "claude");
162
+ assert.strictEqual(admitted.project.name, "project-a");
163
+ state.currentSession = { name: "project-b", dir: projectB };
164
+ state.settings = { backend: "codex", model: "codex-fixture" };
165
+ gate.resolve();
166
+ await pending;
167
+ const run = runs.at(-1);
168
+ assert.strictEqual(run.opts.runContext, admitted);
169
+ assert.strictEqual(run.opts.lastInputWasVoice, true);
170
+ assert.strictEqual(run.opts.voiceStream, true);
171
+ assert.match(run.prompt, /transcript:voice\.ogg/);
172
+ }
173
+
174
+ {
175
+ resetRouting();
176
+ await router.onMessage(envelope("audio", { media: [{ type: "audio" }] }));
177
+ assert.strictEqual(runs.at(-1).opts.runContext, admissions.at(-1));
178
+ assert.match(runs.at(-1).prompt, /^transcript:/);
179
+ }
180
+
181
+ {
182
+ resetRouting();
183
+ await router.onMessage(envelope("photo", {
184
+ caption: "compare these",
185
+ media: [{ type: "photo", fileName: "one.png" }, { type: "photo", fileName: "two.png" }],
186
+ }));
187
+ const run = runs.at(-1);
188
+ assert.strictEqual(run.opts.runContext, admissions.at(-1));
189
+ assert.deepStrictEqual(run.opts.imagePaths.map((imagePath) => path.basename(imagePath)), ["one.png", "two.png"]);
190
+ assert.match(run.prompt, /compare these/);
191
+ }
192
+
193
+ {
194
+ resetRouting();
195
+ await router.onMessage(envelope("document", {
196
+ caption: "inspect",
197
+ media: [{ type: "document", fileName: "screen.png", mimeType: "image/png", size: 12 }],
198
+ }));
199
+ const run = runs.at(-1);
200
+ assert.deepStrictEqual(run.opts.imagePaths.map((imagePath) => path.basename(imagePath)), ["screen.png"]);
201
+ assert.strictEqual(run.opts.runContext, admissions.at(-1));
202
+ }
203
+
204
+ {
205
+ resetRouting();
206
+ await router.onMessage(envelope("document", {
207
+ media: [{ type: "document", fileName: "notes.pdf", mimeType: "application/pdf", size: 12 }],
208
+ }));
209
+ const run = runs.at(-1);
210
+ assert.deepStrictEqual(run.opts.imagePaths, []);
211
+ assert.strictEqual(run.opts.attachments[0].mimeType, "application/pdf");
212
+ }
213
+
214
+ {
215
+ resetRouting();
216
+ const gate = deferred();
217
+ const text = envelope("text", {
218
+ text: "answer this",
219
+ reply: { document: { file_name: "quoted.txt", file_id: "file-id", mime_type: "text/plain" } },
220
+ adapter: adapter("telegram", async () => { await gate.promise; return path.join(process.env.OPEN_CLAUDIA_CONFIG_DIR, "quoted.txt"); }),
221
+ });
222
+ const pending = router.onMessage(text);
223
+ await nextTurn();
224
+ const admitted = admissions.at(-1);
225
+ state.settings = { backend: "codex", model: "codex-fixture" };
226
+ state.currentSession = { name: "project-b", dir: projectB };
227
+ gate.resolve();
228
+ await pending;
229
+ assert.strictEqual(runs.at(-1).opts.runContext, admitted);
230
+ assert.match(runs.at(-1).prompt, /quoted\.txt/);
231
+ }
232
+
233
+ {
234
+ resetRouting();
235
+ await router.onMessage(envelope("text", { text: "plain text" }));
236
+ assert.strictEqual(runs.at(-1).opts.runContext, admissions.at(-1));
237
+ assert.strictEqual(runs.at(-1).prompt, "plain text");
238
+ }
239
+
240
+ assert.ok(sends.length > 0, "media acknowledgement delivery remains active");
241
+ console.log("provider gateway router OK");
242
+ }
243
+
244
+ async function schedulerProbe() {
245
+ const projectA = path.join(process.env.WORKSPACE, "project-a");
246
+ const projectB = path.join(process.env.WORKSPACE, "project-b");
247
+ const state = {
248
+ userId: "fixture-user",
249
+ currentSession: { name: "project-a", dir: projectA },
250
+ settings: { backend: "claude", model: "claude-fixture" },
251
+ activeSessions: { "project-a": { claude: "claude-session" } },
252
+ providerSettings: { claude: { model: "claude-fixture" }, codex: { model: "codex-fixture" } },
253
+ runningProcess: null,
254
+ preparingRun: false,
255
+ };
256
+ fs.mkdirSync(state.currentSession.dir, { recursive: true });
257
+ const records = new Map();
258
+ const removed = [];
259
+ const admissions = [];
260
+ const outcomes = [];
261
+ let notificationHook = null;
262
+
263
+ installStub("./core/jobs", {
264
+ ensureMigrated: () => ({ schemaVersion: 2, jobs: [], archived: [] }),
265
+ get(id) { return records.get(id) || null; },
266
+ update(id, patch) {
267
+ const next = { ...(records.get(id) || {}), ...patch };
268
+ records.set(id, next);
269
+ return next;
270
+ },
271
+ remove(id) { removed.push(id); return true; },
272
+ listAll: () => [],
273
+ add: (job) => job,
274
+ nextId: () => "job-next",
275
+ });
276
+ installStub("./core/providers", {
277
+ providerStatus: () => ({
278
+ availability: { state: "available" },
279
+ compatibility: { state: "compatible" },
280
+ auth: { state: "authenticated" },
281
+ }),
282
+ });
283
+ installStub("./core/state", {
284
+ getUserState: () => state,
285
+ getProviderSession: (_state, provider, project) => (
286
+ state.activeSessions?.[project]?.[provider] || null
287
+ ),
288
+ userStates: new Map(),
289
+ });
290
+ installStub("./core/identity", {
291
+ canonicalForTelegram: () => "fixture-user",
292
+ canonicalForChannel: () => "fixture-user",
293
+ });
294
+ installStub("./core/io", {
295
+ send: async () => {
296
+ const hook = notificationHook;
297
+ notificationHook = null;
298
+ if (hook) await hook();
299
+ return { ok: true, messageId: "sent", editable: true };
300
+ },
301
+ });
302
+ installStub("./core/runner", {
303
+ admitRunContext(prompt, cwd, replyTo, opts = {}) {
304
+ const context = Object.freeze({
305
+ runId: `scheduled-${admissions.length + 1}`,
306
+ provider: opts.provider || state.settings.backend,
307
+ project: Object.freeze({ ...state.currentSession }),
308
+ providerSettings: Object.freeze({ ...state.providerSettings[opts.provider || state.settings.backend] }),
309
+ sessionId: opts.resumeSessionId || state.activeSessions[state.currentSession.name]?.[opts.provider || state.settings.backend] || null,
310
+ userPrompt: prompt,
311
+ cwd,
312
+ });
313
+ admissions.push(context);
314
+ return context;
315
+ },
316
+ async runAgent(_prompt, _cwd, _reply, opts) {
317
+ assert.strictEqual(opts.runContext, admissions.at(-1));
318
+ return outcomes.shift();
319
+ },
320
+ });
321
+
322
+ const scheduler = require("./core/scheduler");
323
+ const channelAdapter = { id: "telegram-fixture", type: "telegram", send: async () => "sent" };
324
+ scheduler.setAdapters([channelAdapter]);
325
+ const base = {
326
+ kind: "wakeup",
327
+ channelId: "fixture-channel",
328
+ canonicalUserId: "fixture-user",
329
+ userId: "fixture-user",
330
+ adapter: "telegram-fixture",
331
+ adapterType: "telegram",
332
+ provider: "claude",
333
+ providerSettings: { model: "claude-fixture" },
334
+ project: "project-a",
335
+ projectDir: state.currentSession.dir,
336
+ prompt: "scheduled prompt",
337
+ label: "fixture wakeup",
338
+ sessionId: "scheduled-session",
339
+ };
340
+
341
+ records.set("job-failed", { ...base, id: "job-failed", occurrenceId: "occ-failed", status: "pending", attemptCount: 0, maxAttempts: 3 });
342
+ outcomes.push({ ok: false, status: "failed", diagnostic: "fixture failed", error: { message: "fixture failed" }, delivery: { ok: true } });
343
+ notificationHook = async () => {
344
+ state.currentSession = { name: "project-b", dir: projectB };
345
+ state.settings = { backend: "codex", model: "codex-fixture" };
346
+ await nextTurn();
347
+ };
348
+ await scheduler.fireJob({ ...base, id: "job-failed" });
349
+ assert.strictEqual(records.get("job-failed").lastFireOk, false);
350
+ assert.ok(!removed.includes("job-failed"), "failed one-shot remains retryable");
351
+ assert.strictEqual(admissions[0].provider, "claude");
352
+ assert.strictEqual(admissions[0].project.name, "project-a");
353
+
354
+ state.currentSession = { name: "project-a", dir: projectA };
355
+ state.settings = { backend: "claude", model: "claude-fixture" };
356
+ records.set("job-success", { ...base, id: "job-success", occurrenceId: "occ-success", status: "pending", attemptCount: 0, maxAttempts: 3 });
357
+ outcomes.push({ ok: true, status: "succeeded", delivery: { ok: true } });
358
+ await scheduler.fireJob({ ...base, id: "job-success" });
359
+ assert.strictEqual(records.get("job-success").lastFireOk, true);
360
+ assert.ok(removed.includes("job-success"), "successful terminal one-shot is removed");
361
+ assert.strictEqual(admissions.length, 2);
362
+ scheduler.stopAll();
363
+ console.log("provider gateway scheduler OK");
364
+ }
365
+
366
+ function queueProbe() {
367
+ const projectA = path.join(process.env.WORKSPACE, "project-a");
368
+ const projectB = path.join(process.env.WORKSPACE, "project-b");
369
+ fs.mkdirSync(projectA, { recursive: true });
370
+ fs.mkdirSync(projectB, { recursive: true });
371
+ const state = {
372
+ userId: "fixture-user",
373
+ currentSession: { name: "project-a", dir: projectA },
374
+ settings: { backend: "claude", model: "claude-fixture", effort: "high" },
375
+ activeSessions: { "project-a": { claude: "claude-queued-session", codex: "codex-live-session" } },
376
+ providerSettings: {
377
+ claude: { model: "claude-fixture", effort: "high" },
378
+ codex: { model: "codex-fixture", effort: "low" },
379
+ },
380
+ messageQueue: [],
381
+ preparingRun: true,
382
+ runningProcess: null,
383
+ isCompacting: false,
384
+ lastInputWasVoice: false,
385
+ };
386
+ const store = Object.freeze({
387
+ adapter: { id: "telegram-fixture", type: "telegram" },
388
+ channelId: "fixture-channel",
389
+ canonicalUserId: "fixture-user",
390
+ userId: "fixture-user",
391
+ transport: "telegram",
392
+ });
393
+
394
+ installStub("./core/state", {
395
+ currentState: () => state,
396
+ saveState: () => {},
397
+ recordSession: () => {},
398
+ userOwnsProviderSession: () => false,
399
+ resetSessionUsage: () => {},
400
+ acquireRunLock: () => false,
401
+ });
402
+ installStub("./core/context", {
403
+ chatContext: { getStore: () => store, run: (_store, fn) => fn() },
404
+ currentChannelId: () => store.channelId,
405
+ currentAdapter: () => store.adapter,
406
+ currentUserId: () => store.userId,
407
+ });
408
+ installStub("./core/io", {
409
+ send: async () => ({ ok: true, messageId: "queued", editable: true }),
410
+ editMessage: async () => ({ ok: true }),
411
+ sendVoice: async () => ({ ok: true }),
412
+ sendVoiceEnd: async () => ({ ok: true }),
413
+ splitMessage: (text) => [text],
414
+ });
415
+ installStub("./core/tool-guard", { ensureHookSettings: () => ({ enabled: true }) });
416
+
417
+ const { runAgent } = require("./core/runner");
418
+ const pending = runAgent("queued prompt", projectA, "message-queued", { runId: "queued-run" });
419
+ assert.strictEqual(typeof pending.then, "function");
420
+ assert.strictEqual(state.messageQueue.length, 1);
421
+ const queued = state.messageQueue[0];
422
+ assert.ok(Object.isFrozen(queued.runContext));
423
+ assert.ok(Object.isFrozen(queued.runContext.project));
424
+ assert.ok(Object.isFrozen(queued.runContext.providerSettings));
425
+
426
+ state.currentSession = { name: "project-b", dir: projectB };
427
+ state.settings = { backend: "codex", model: "codex-fixture", effort: "low" };
428
+ state.activeSessions["project-a"].claude = "claude-changed-session";
429
+ state.activeSessions["project-a"].codex = "codex-changed-session";
430
+
431
+ assert.strictEqual(queued.runContext.runId, "queued-run");
432
+ assert.strictEqual(queued.runContext.provider, "claude");
433
+ assert.strictEqual(queued.runContext.project.name, "project-a");
434
+ assert.strictEqual(queued.runContext.project.dir, projectA);
435
+ assert.strictEqual(queued.runContext.providerSettings.model, "claude-fixture");
436
+ assert.strictEqual(queued.runContext.sessionId, "claude-queued-session");
437
+ console.log("provider gateway immutable queue OK");
438
+ }
439
+
440
+ async function childMain(mode) {
441
+ if (mode === "--router-probe") return routerProbe();
442
+ if (mode === "--scheduler-probe") return schedulerProbe();
443
+ if (mode === "--queue-probe") return queueProbe();
444
+ throw new Error(`unknown probe: ${mode}`);
445
+ }
446
+
447
+ function staticChecks() {
448
+ const files = [
449
+ "core/router.js",
450
+ "core/handlers.js",
451
+ "core/actions.js",
452
+ "core/scheduler.js",
453
+ "core/runner.js",
454
+ "bin/cli.js",
455
+ ];
456
+ const source = Object.fromEntries(files.map((file) => [file, fs.readFileSync(path.join(__dirname, file), "utf8")]));
457
+ for (const [file, text] of Object.entries(source)) {
458
+ assert.ok(!/\brunClaude\s*\(/.test(text), `${file} must have no active runClaude call`);
459
+ }
460
+ for (const file of ["core/router.js", "core/handlers.js", "core/actions.js", "core/scheduler.js"]) {
461
+ assert.match(source[file], /\brunAgent\b/, `${file} routes through runAgent`);
462
+ }
463
+ assert.match(source["core/router.js"], /\badmitRunContext\b/, "media/text ingress captures before preparation awaits");
464
+ assert.match(source["core/scheduler.js"], /result\.ok/, "scheduler consumes terminal RunResult success");
465
+ assert.match(source["core/runner.js"], /messageQueue\.push\(\{ runContext,/, "queued work stores its immutable run context");
466
+ assert.match(source["core/runner.js"], /executeAdmittedRun\(item\.runContext,/, "queue drain executes the admitted context");
467
+ assert.match(source["bin/cli.js"], /Open Claudia agent task/);
468
+ assert.ok(!source["bin/cli.js"].includes("inside a running Claude task"));
469
+ }
470
+
471
+ if (process.argv[2]) {
472
+ childMain(process.argv[2]).catch((error) => {
473
+ console.error(error.stack || error.message);
474
+ process.exit(1);
475
+ });
476
+ } else {
477
+ staticChecks();
478
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "open-claudia-provider-gateway-"));
479
+ const configDir = path.join(root, "config");
480
+ const workspace = path.join(root, "workspace");
481
+ fs.mkdirSync(configDir, { recursive: true });
482
+ fs.mkdirSync(workspace, { recursive: true });
483
+ try {
484
+ for (const mode of ["--router-probe", "--scheduler-probe", "--queue-probe"]) {
485
+ const child = spawnSync(process.execPath, [__filename, mode], {
486
+ cwd: __dirname,
487
+ env: {
488
+ HOME: root,
489
+ PATH: process.env.PATH || path.dirname(process.execPath),
490
+ OPEN_CLAUDIA_TEST: "1",
491
+ OPEN_CLAUDIA_CONFIG_DIR: configDir,
492
+ WORKSPACE: workspace,
493
+ PROJECT_TRANSCRIPTS: "1",
494
+ PACK_REVIEW: "off",
495
+ },
496
+ encoding: "utf8",
497
+ timeout: 30000,
498
+ maxBuffer: 2 * 1024 * 1024,
499
+ });
500
+ assert.ifError(child.error);
501
+ assert.strictEqual(child.status, 0, child.stderr || `${mode} failed`);
502
+ process.stdout.write(child.stdout);
503
+ }
504
+ console.log("provider gateway OK");
505
+ } finally {
506
+ fs.rmSync(root, { recursive: true, force: true });
507
+ }
508
+ }
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const assert = require("assert");
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+
9
+ const read = (relative) => fs.readFileSync(path.join(__dirname, relative), "utf8");
10
+
11
+ const manage = read("channels/voice/manage.js");
12
+ assert.doesNotMatch(manage, /DEFAULT_CLAUDE_MODEL/);
13
+ assert.match(manage, /defaultProvider/);
14
+ assert.match(manage, /providerId/);
15
+ assert.match(manage, /defaultModel/);
16
+
17
+ const cli = read("bin/cli.js");
18
+ assert.match(cli, /inside an Open Claudia agent task/);
19
+ assert.doesNotMatch(cli, /inside a Claude task/i);
20
+ assert.doesNotMatch(cli, /slow health checks \(Claude auth, Telegram API\)/);
21
+ assert.match(cli, /provider auth and channel API checks/);
22
+
23
+ const web = read("web.js");
24
+ assert.match(web, /Provider credentials/);
25
+ assert.match(web, /Authenticate the coding-agent providers you use/);
26
+ assert.match(web, /Configure channels and coding-agent providers/);
27
+
28
+ const setup = read("setup.js");
29
+ assert.match(setup, /Your Open Claudia bot is connected/);
30
+ assert.match(setup, /Description=Open Claudia Provider Bot/);
31
+ assert.doesNotMatch(setup, /Your Claude Code bot is connected|Description=Claude Code Telegram Bot/);
32
+
33
+ const pkg = JSON.parse(read("package.json"));
34
+ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
35
+ // 3.0.0 approved by Sumeet 2026-07-10 (provider-parity release; CHANGELOG
36
+ // already documents it as v3.0.0).
37
+ assert.strictEqual(pkg.version, "3.0.0", "release version must remain unchanged without explicit approval");
38
+ for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
39
+ assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
40
+ }
41
+ for (const shipped of ["docs/", "soul.md", "test-provider-language.js"]) {
42
+ assert.ok(pkg.files.includes(shipped), `package file missing: ${shipped}`);
43
+ }
44
+ assert.match(pkg.scripts.pretest, /node test-provider-language\.js/);
45
+
46
+ const soul = read("soul.md");
47
+ const runtimeIdentity = soul.split("## About Sumeet")[0];
48
+ assert.match(runtimeIdentity, /Open Claudia running the selected coding-agent provider/);
49
+ assert.doesNotMatch(runtimeIdentity, /bridge to Claude Code|Name: Just "Claude"/i);
50
+ assert.match(soul, /Uses Cursor IDE and Claude Code CLI daily/, "preserve the user-authored Cursor IDE fact");
51
+
52
+ const readme = read("README.md");
53
+ assert.match(readme, /## Utility provider policy/);
54
+ assert.match(readme, /Foreground turns never silently fall back/);
55
+ assert.match(readme, /## Provider session semantics/);
56
+ assert.match(readme, /capabilit(?:y|ies)/i);
57
+ assert.match(readme, /\[Provider migration snapshots and rollback\]\(docs\/PROVIDER_MIGRATION\.md\)/);
58
+ assert.match(readme, /native same-UID process/i);
59
+ assert.match(readme, /legacy-compatible service id com\.claude-telegram-bot/);
60
+ assert.match(readme, /com\.claude-telegram-bot\.plist/);
61
+ assert.doesNotMatch(readme, /com\.open-claudia\.plist/);
62
+
63
+ const channelDesign = read("docs/CHANNEL_DESIGN.md");
64
+ assert.match(channelDesign, /provider registry/i);
65
+ assert.match(channelDesign, /PROVIDER_MIGRATION\.md/);
66
+
67
+ const multiChannelPlan = read("docs/MULTI_CHANNEL_PLAN.md");
68
+ assert.match(multiChannelPlan, /historical design plan/i);
69
+ assert.match(multiChannelPlan, /## 1\. Current implementation/);
70
+ assert.doesNotMatch(multiChannelPlan, /## 1\. Current state \(what we have\)/);
71
+
72
+ const migration = read("docs/PROVIDER_MIGRATION.md");
73
+ assert.match(migration, /logical path, byte size, schema version, and SHA-256/i);
74
+ assert.match(migration, /post-upgrade changes are not merged/i);
75
+
76
+ const changelog = read("CHANGELOG.md");
77
+ assert.match(changelog, /^## v3\.0\.0 — provider parity and Cursor removal$/m);
78
+ assert.match(changelog, /^## v2\.15\.0$/m, "historical changelog sections must remain present");
79
+ const release = changelog.slice(
80
+ changelog.indexOf("## v3.0.0 — provider parity and Cursor removal"),
81
+ changelog.indexOf("## v2.15.0"),
82
+ );
83
+ assert.match(release, /recommend(?:ed|ation)[^\n]*v3\.0\.0/i);
84
+ assert.match(release, /released as `v3\.0\.0` \(2026-07-10\)/);
85
+ assert.doesNotMatch(release, /remains (?:at )?v?2\.15\.0/i);
86
+ assert.match(release, /authenticated release validation/i);
87
+ assert.match(release, /fresh[^\n]*resum(?:e|ed)[^\n]*read-only plan[^\n]*harmless tool[^\n]*utility JSON[^\n]*prompt-context sentinel/i);
88
+
89
+ console.log("provider language OK");