@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,271 @@
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
+
10
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "open-claudia-provider-recall-"));
11
+ process.env.OPEN_CLAUDIA_TEST = "1";
12
+ process.env.OPEN_CLAUDIA_CONFIG_DIR = path.join(root, "config");
13
+ process.env.WORKSPACE = root;
14
+ process.env.RECALL_METRICS = "on";
15
+ process.env.RECALL_WARM_WALKER = "off";
16
+ fs.mkdirSync(process.env.OPEN_CLAUDIA_CONFIG_DIR, { recursive: true });
17
+
18
+ const fixture = path.join(__dirname, "test-fixtures", "fake-agent-cli.js");
19
+ const { createClaudeProvider } = require("./core/providers/claude");
20
+ const { createCodexProvider } = require("./core/providers/codex");
21
+ const { createUtilityPolicy } = require("./core/providers/utility-policy");
22
+ const { createUtilityAgentService } = require("./core/utility-agent");
23
+ const { createRecallFilter } = require("./core/recall-filter");
24
+ const discoverer = require("./core/recall/discoverer");
25
+ const metrics = require("./core/recall/metrics");
26
+ const warmWalker = require("./core/recall/warm-walker");
27
+ const systemPrompt = require("./core/system-prompt");
28
+
29
+ function makeProvider(kind, controls = {}) {
30
+ const common = {
31
+ hookTransport: false,
32
+ resolveExecutable: () => fixture,
33
+ defaultModel: `${kind}-high-fixture`,
34
+ buildEnv: () => ({
35
+ HOME: root,
36
+ PATH: process.env.PATH || path.dirname(process.execPath),
37
+ FAKE_AGENT_KIND: kind,
38
+ ...controls,
39
+ }),
40
+ getAuthStatus: () => ({ state: "authenticated", reason: null }),
41
+ };
42
+ return kind === "claude" ? createClaudeProvider(common) : createCodexProvider(common);
43
+ }
44
+
45
+ function makeRegistry(provider) {
46
+ return {
47
+ getProvider(id) {
48
+ if (id !== provider.id) throw Object.assign(new Error(`unknown ${id}`), { code: "UNKNOWN_PROVIDER" });
49
+ return provider;
50
+ },
51
+ listProviders: () => [provider],
52
+ providerStatus: (id) => ({
53
+ id,
54
+ availability: { state: "available", executable: fixture, reason: null },
55
+ compatibility: { state: "compatible", reason: null },
56
+ auth: { state: "authenticated", reason: null },
57
+ }),
58
+ };
59
+ }
60
+
61
+ function harness(kind, controls = {}, env = {}) {
62
+ const provider = makeProvider(kind, controls);
63
+ const registry = makeRegistry(provider);
64
+ const policy = createUtilityPolicy({ registry, env: { UTILITY_PROVIDER: kind, ...env } });
65
+ const utility = createUtilityAgentService({
66
+ registry,
67
+ policy,
68
+ env: { UTILITY_PROVIDER: kind, ...env },
69
+ });
70
+ return { provider, registry, policy, utility };
71
+ }
72
+
73
+ async function main() {
74
+ assert.strictEqual(typeof createRecallFilter, "function", "recall filter exposes an injectable provider-neutral service");
75
+ assert.strictEqual(typeof discoverer.runWalker, "function", "discoverer exposes its provider-aware walker boundary");
76
+
77
+ let capturedRecallRequest = null;
78
+ await createRecallFilter({
79
+ spawnUtilityAgent: async (_prompt, request) => {
80
+ capturedRecallRequest = request;
81
+ return { text: "[]" };
82
+ },
83
+ }).judgeRelevance("capture provider context", [{ id: "pack:a", name: "A" }], { provider: "claude" });
84
+ assert.strictEqual(capturedRecallRequest.activeProvider, "claude");
85
+ assert.strictEqual(Object.prototype.hasOwnProperty.call(capturedRecallRequest, "provider"), false,
86
+ "foreground recall context is not misclassified as an explicit provider override");
87
+
88
+ for (const kind of ["claude", "codex"]) {
89
+ const capture = path.join(root, `${kind}-filter.json`);
90
+ const h = harness(kind, {
91
+ FAKE_AGENT_CAPTURE: capture,
92
+ FAKE_AGENT_TEXT: '["pack:keep"]',
93
+ });
94
+ const filter = createRecallFilter({ spawnUtilityAgent: h.utility.spawnUtilityAgent });
95
+ const kept = await filter.judgeRelevance("keep this project", [
96
+ { id: "pack:keep", name: "Keep", description: "relevant" },
97
+ { id: "pack:drop", name: "Drop", description: "unrelated" },
98
+ ], { provider: kind, context: "same project" });
99
+ assert.deepStrictEqual([...kept], ["pack:keep"]);
100
+ const record = JSON.parse(fs.readFileSync(capture, "utf8"));
101
+ assert.strictEqual(record.stdin.present, true);
102
+ assert.ok(!record.argv.some((arg) => /keep this project/.test(arg)), "recall candidates stay out of argv");
103
+ if (kind === "claude") {
104
+ assert.strictEqual(record.argv[record.argv.indexOf("--model") + 1], h.provider.tierModel("low"));
105
+ } else {
106
+ assert.strictEqual(record.argv[record.argv.indexOf("--model") + 1], h.provider.tierModel("low"));
107
+ assert.ok(!record.argv.includes("haiku"), "Codex never receives a legacy Claude recall alias");
108
+ }
109
+ }
110
+
111
+ const failedFilter = createRecallFilter({
112
+ spawnUtilityAgent: async () => { throw new Error("fixture unavailable"); },
113
+ });
114
+ assert.strictEqual(
115
+ await failedFilter.judgeRelevance("query", [{ id: "pack:a", name: "A" }], { provider: "codex" }),
116
+ null,
117
+ "classic relevance filtering remains fail-open",
118
+ );
119
+
120
+ const legacyClaudeCapture = path.join(root, "legacy-claude-filter.json");
121
+ const legacyClaude = harness("claude", {
122
+ FAKE_AGENT_CAPTURE: legacyClaudeCapture,
123
+ FAKE_AGENT_TEXT: '["pack:a"]',
124
+ }, { RECALL_FILTER_MODEL: "legacy-claude-filter" });
125
+ await createRecallFilter({ spawnUtilityAgent: legacyClaude.utility.spawnUtilityAgent })
126
+ .judgeRelevance("query", [{ id: "pack:a", name: "A" }], { provider: "claude" });
127
+ assert.strictEqual(
128
+ JSON.parse(fs.readFileSync(legacyClaudeCapture, "utf8")).argv[
129
+ JSON.parse(fs.readFileSync(legacyClaudeCapture, "utf8")).argv.indexOf("--model") + 1
130
+ ],
131
+ "legacy-claude-filter",
132
+ "legacy exact recall model remains a Claude-only compatibility override",
133
+ );
134
+
135
+ const codexExactCapture = path.join(root, "exact-codex-filter.json");
136
+ const codexExact = harness("codex", {
137
+ FAKE_AGENT_CAPTURE: codexExactCapture,
138
+ FAKE_AGENT_TEXT: '["pack:a"]',
139
+ }, {
140
+ RECALL_FILTER_MODEL: "legacy-must-not-cross",
141
+ RECALL_FILTER_MODEL_CODEX: "codex-filter-exact",
142
+ });
143
+ await createRecallFilter({ spawnUtilityAgent: codexExact.utility.spawnUtilityAgent })
144
+ .judgeRelevance("query", [{ id: "pack:a", name: "A" }], { provider: "codex" });
145
+ const codexExactArgv = JSON.parse(fs.readFileSync(codexExactCapture, "utf8")).argv;
146
+ assert.strictEqual(codexExactArgv[codexExactArgv.indexOf("--model") + 1], "codex-filter-exact");
147
+ assert.ok(!codexExactArgv.includes("legacy-must-not-cross"));
148
+
149
+ const candidates = [
150
+ { id: "pack:a", name: "A", description: "alpha", excerpt: "alpha detail" },
151
+ { id: "pack:b", name: "B", description: "beta", excerpt: "beta detail" },
152
+ ];
153
+ for (const kind of ["claude", "codex"]) {
154
+ const h = harness(kind, { FAKE_AGENT_TEXT: '[{"id":"pack:a","why":"directly relevant"}]' });
155
+ const walk = await discoverer.walk("alpha question with enough words", "", candidates, {
156
+ provider: kind,
157
+ resolveUtilityProvider: h.policy.resolve,
158
+ spawnUtilityAgent: h.utility.spawnUtilityAgent,
159
+ warmWalker: { isEnabled: () => false },
160
+ });
161
+ assert.deepStrictEqual([...walk.kept], [["pack:a", "directly relevant"]]);
162
+ assert.strictEqual(walk.provider, kind);
163
+ assert.strictEqual(walk.model, h.provider.tierModel("low"));
164
+ assert.strictEqual(walk.source, "cold");
165
+ }
166
+
167
+ let warmCalls = 0;
168
+ let coldCalls = 0;
169
+ const warmClaude = makeProvider("claude");
170
+ const warmResult = await discoverer.runWalker("warm prompt", {
171
+ provider: "claude",
172
+ resolveUtilityProvider: () => ({
173
+ provider: warmClaude,
174
+ providerId: "claude",
175
+ model: warmClaude.tierModel("low"),
176
+ }),
177
+ warmWalker: {
178
+ isEnabled: (provider) => provider.id === "claude" && provider.capabilities.persistentUtilityStream.support === "native",
179
+ walkWarm: async (_prompt, opts) => {
180
+ warmCalls += 1;
181
+ assert.strictEqual(opts.provider.id, "claude");
182
+ return { text: "[]", costUsd: 0 };
183
+ },
184
+ },
185
+ spawnUtilityAgent: async () => { coldCalls += 1; return { text: "[]", provider: "claude", model: "cold" }; },
186
+ });
187
+ assert.strictEqual(warmResult.source, "warm");
188
+ assert.strictEqual(warmCalls, 1);
189
+ assert.strictEqual(coldCalls, 0);
190
+
191
+ const coldCodex = makeProvider("codex");
192
+ const codexWarmProbe = { checked: 0, walked: 0 };
193
+ const coldResult = await discoverer.runWalker("cold prompt", {
194
+ provider: "codex",
195
+ resolveUtilityProvider: () => ({ provider: coldCodex, providerId: "codex", model: coldCodex.tierModel("low") }),
196
+ warmWalker: {
197
+ isEnabled(provider) { codexWarmProbe.checked += 1; return provider.capabilities.persistentUtilityStream.support === "native"; },
198
+ walkWarm: async () => { codexWarmProbe.walked += 1; return { text: "[]" }; },
199
+ },
200
+ spawnUtilityAgent: async () => ({ text: "[]", provider: "codex", model: coldCodex.tierModel("low") }),
201
+ });
202
+ assert.strictEqual(coldResult.source, "cold");
203
+ assert.strictEqual(codexWarmProbe.checked, 1);
204
+ assert.strictEqual(codexWarmProbe.walked, 0, "unsupported Codex persistent stream is never attempted");
205
+
206
+ assert.strictEqual(warmWalker.isEnabled(coldCodex), false);
207
+ assert.strictEqual(warmWalker.isEnabled(warmClaude), false, "operator switch can disable even a native warm transport");
208
+ process.env.RECALL_WARM_WALKER = "on";
209
+ assert.strictEqual(warmWalker.isEnabled(warmClaude), true, "Claude warm path requires an advertised native capability");
210
+ assert.strictEqual(warmWalker.isEnabled(coldCodex), false, "Codex remains cold when persistentUtilityStream is unsupported");
211
+ process.env.RECALL_WARM_WALKER = "off";
212
+
213
+ metrics._resetForTest();
214
+ metrics.logTurn({
215
+ engine: "discoverer",
216
+ provider: "codex",
217
+ model: "codex-low-model",
218
+ query: "metric",
219
+ tier: "full",
220
+ seeds: [], activated: [], cand: [], kept: [],
221
+ });
222
+ const metricRecord = JSON.parse(fs.readFileSync(metrics.LOG_FILE, "utf8").trim());
223
+ assert.strictEqual(metricRecord.provider, "codex");
224
+ assert.strictEqual(metricRecord.model, "codex-low-model");
225
+
226
+ const recall = require("./core/recall");
227
+ const originalGetEngine = recall.getEngine;
228
+ let admittedProvider = null;
229
+ let releaseEngine;
230
+ const engineGate = new Promise((resolve) => { releaseEngine = resolve; });
231
+ recall.getEngine = () => ({
232
+ name: "fixture",
233
+ async run(ctx) {
234
+ admittedProvider = ctx.provider;
235
+ await engineGate;
236
+ assert.strictEqual(ctx.provider, "codex", "async recall keeps its admitted provider identity");
237
+ return { packBlock: "", entityBlock: "", toolBlock: "", episodeBlock: "", packMatches: [], entityMatches: [] };
238
+ },
239
+ });
240
+ const mutableState = {
241
+ userId: "fixture-user",
242
+ currentSession: { name: "project-a", dir: root },
243
+ settings: { backend: "claude", recallEngine: "discoverer" },
244
+ };
245
+ const recallBuild = systemPrompt.buildOptionalRecallContext("provider race", {
246
+ state: mutableState,
247
+ runContext: { provider: "codex", project: { name: "project-a", dir: root }, sessionId: null, runId: "recall-race" },
248
+ provider: coldCodex,
249
+ });
250
+ mutableState.settings.backend = "claude";
251
+ releaseEngine();
252
+ await recallBuild;
253
+ recall.getEngine = originalGetEngine;
254
+ assert.strictEqual(admittedProvider, "codex");
255
+
256
+ for (const file of ["core/recall-filter.js", "core/recall/discoverer.js", "core/recall/warm-walker.js"]) {
257
+ const source = fs.readFileSync(path.join(__dirname, file), "utf8");
258
+ assert.ok(!source.includes("spawnSubagent"), `${file} no longer uses the compatibility sub-agent path`);
259
+ assert.ok(!source.includes("CLAUDE_PATH"), `${file} has no hard-coded Claude executable dependency`);
260
+ }
261
+
262
+ console.log("provider recall OK");
263
+ }
264
+
265
+ main().catch((error) => {
266
+ console.error(error.stack || error.message);
267
+ process.exitCode = 1;
268
+ }).finally(async () => {
269
+ try { await warmWalker.shutdown(); } catch (_) {}
270
+ fs.rmSync(root, { recursive: true, force: true });
271
+ });
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const assert = require("assert");
6
+ const {
7
+ CAPABILITY_KEYS,
8
+ ProviderContractError,
9
+ validateInvocation,
10
+ validateProvider,
11
+ } = require("./core/providers/contract");
12
+ const {
13
+ ProviderRegistry,
14
+ ProviderRegistryError,
15
+ } = require("./core/providers");
16
+
17
+ function capabilities(overrides = {}) {
18
+ const result = {};
19
+ for (const key of CAPABILITY_KEYS) {
20
+ result[key] = {
21
+ support: "unsupported",
22
+ reason: `${key} is not implemented by this fixture`,
23
+ ...(key === "effort" ? { values: [] } : {}),
24
+ };
25
+ }
26
+ result.resume = { support: "native", reason: null };
27
+ result.readOnlyMode = { support: "native", reason: null };
28
+ return { ...result, ...overrides };
29
+ }
30
+
31
+ function provider(id, overrides = {}) {
32
+ return {
33
+ id,
34
+ label: id === "claude" ? "Claude Code" : "OpenAI Codex",
35
+ isAvailable: () => overrides.available !== false,
36
+ executable: () => `/fixture/${id}`,
37
+ defaultModel: () => `${id}-default`,
38
+ tierModel: (tier) => `${id}-${tier}`,
39
+ modelChoices: () => [`${id}-default`],
40
+ compatibilityStatus: () => {
41
+ if (overrides.compatibilityError) throw new Error(overrides.compatibilityError);
42
+ return overrides.compatibility || {
43
+ state: "compatible",
44
+ version: "1.2.3",
45
+ reason: null,
46
+ };
47
+ },
48
+ capabilities: capabilities(overrides.capabilities),
49
+ buildMainInvocation: (context) => ({
50
+ binary: `/fixture/${id}`,
51
+ args: [context.userPrompt],
52
+ env: { PATH: "/fixture" },
53
+ stdin: null,
54
+ parserState: {},
55
+ }),
56
+ buildUtilityInvocation: (context) => ({
57
+ binary: `/fixture/${id}`,
58
+ args: [context.userPrompt],
59
+ env: { PATH: "/fixture" },
60
+ stdin: null,
61
+ parserState: {},
62
+ }),
63
+ createEventDecoder: () => ({ push: () => [], end: () => [] }),
64
+ normalizeEvent: () => [],
65
+ authStatus: () => {
66
+ if (overrides.authError) throw new Error(overrides.authError);
67
+ return overrides.auth || {
68
+ state: "authenticated",
69
+ reason: null,
70
+ };
71
+ },
72
+ authHelp: () => `${id} auth help`,
73
+ };
74
+ }
75
+
76
+ const claude = provider("claude");
77
+ assert.strictEqual(validateProvider(claude), claude);
78
+ assert.deepStrictEqual(Object.keys(claude.capabilities).sort(), [...CAPABILITY_KEYS].sort());
79
+
80
+ const frozenContext = Object.freeze({ userPrompt: "registry sentinel" });
81
+ assert.deepStrictEqual(validateInvocation(claude.buildMainInvocation(frozenContext)), {
82
+ binary: "/fixture/claude",
83
+ args: ["registry sentinel"],
84
+ env: { PATH: "/fixture" },
85
+ stdin: null,
86
+ parserState: {},
87
+ });
88
+
89
+ assert.throws(
90
+ () => validateProvider({ ...claude, buildUtilityInvocation: undefined }),
91
+ (error) => error instanceof ProviderContractError && error.code === "INVALID_PROVIDER_CONTRACT",
92
+ );
93
+ assert.throws(
94
+ () => validateProvider({ ...claude, tierModel: undefined }),
95
+ (error) => error instanceof ProviderContractError && error.code === "INVALID_PROVIDER_CONTRACT",
96
+ );
97
+ assert.throws(
98
+ () => validateProvider({ ...claude, capabilities: { ...claude.capabilities, effort: { support: "native", reason: null } } }),
99
+ (error) => error instanceof ProviderContractError && /effort\.values/.test(error.message),
100
+ );
101
+ assert.throws(
102
+ () => validateInvocation({ binary: "claude", args: [], env: {} }),
103
+ (error) => error instanceof ProviderContractError && /stdin/.test(error.message),
104
+ );
105
+ assert.throws(
106
+ () => validateInvocation({ binary: "claude", args: [], env: { BAD: undefined }, stdin: null, parserState: {} }),
107
+ (error) => error instanceof ProviderContractError && /env keys and values/.test(error.message),
108
+ );
109
+ assert.throws(
110
+ () => validateProvider({ ...claude, capabilities: { ...claude.capabilities, budget: { support: "unsupported", reason: null } } }),
111
+ (error) => error instanceof ProviderContractError && /must explain/.test(error.message),
112
+ );
113
+
114
+ const registry = new ProviderRegistry();
115
+ assert.strictEqual(registry.registerProvider(claude), claude);
116
+ assert.strictEqual(registry.registerProvider(provider("codex")), registry.getProvider("codex"));
117
+ assert.deepStrictEqual(registry.listProviders().map((entry) => entry.id), ["claude", "codex"]);
118
+ assert.deepStrictEqual(registry.availableProviders().map((entry) => entry.id), ["claude", "codex"]);
119
+
120
+ const claudeStatus = registry.providerStatus("claude");
121
+ assert.deepStrictEqual(claudeStatus.availability, { state: "available", executable: "/fixture/claude", reason: null });
122
+ assert.strictEqual(claudeStatus.compatibility.state, "compatible");
123
+ assert.strictEqual(claudeStatus.auth.state, "authenticated");
124
+ assert.strictEqual(registry.resolveProviderForRun().id, "claude");
125
+ assert.strictEqual(registry.resolveProviderForRun("codex").id, "codex");
126
+
127
+ assert.throws(
128
+ () => registry.registerProvider(provider("claude")),
129
+ (error) => error instanceof ProviderRegistryError && error.code === "DUPLICATE_PROVIDER",
130
+ );
131
+ assert.throws(
132
+ () => registry.registerProvider(provider("cursor")),
133
+ (error) => error instanceof ProviderRegistryError && error.code === "UNKNOWN_PROVIDER",
134
+ );
135
+ assert.throws(
136
+ () => registry.getProvider("missing"),
137
+ (error) => error instanceof ProviderRegistryError && error.code === "UNKNOWN_PROVIDER",
138
+ );
139
+
140
+ const unavailable = new ProviderRegistry();
141
+ unavailable.registerProvider(provider("claude", { available: false }));
142
+ unavailable.registerProvider(provider("codex", { available: false }));
143
+ assert.deepStrictEqual(unavailable.availableProviders(), []);
144
+ assert.throws(
145
+ () => unavailable.resolveProviderForRun(),
146
+ (error) => error instanceof ProviderRegistryError && error.code === "NO_AVAILABLE_PROVIDER",
147
+ );
148
+
149
+ const incompatible = new ProviderRegistry();
150
+ incompatible.registerProvider(provider("claude", {
151
+ compatibility: { state: "incompatible", version: "0.1.0", reason: "fixture version is too old" },
152
+ }));
153
+ assert.throws(
154
+ () => incompatible.resolveProviderForRun("claude"),
155
+ (error) => error instanceof ProviderRegistryError && error.code === "PROVIDER_INCOMPATIBLE",
156
+ );
157
+
158
+ const unauthenticated = new ProviderRegistry();
159
+ unauthenticated.registerProvider(provider("codex", {
160
+ auth: { state: "unauthenticated", reason: "fixture is logged out" },
161
+ }));
162
+ assert.throws(
163
+ () => unauthenticated.resolveProviderForRun("codex"),
164
+ (error) => error instanceof ProviderRegistryError && error.code === "PROVIDER_UNAUTHENTICATED",
165
+ );
166
+
167
+ const noForegroundFallback = new ProviderRegistry();
168
+ noForegroundFallback.registerProvider(provider("claude", {
169
+ auth: { state: "unauthenticated", reason: "login required" },
170
+ }));
171
+ noForegroundFallback.registerProvider(provider("codex"));
172
+ assert.throws(
173
+ () => noForegroundFallback.resolveProviderForRun(),
174
+ (error) => error instanceof ProviderRegistryError && error.code === "PROVIDER_UNAUTHENTICATED" && error.details.providerId === "claude",
175
+ );
176
+
177
+ const brokenCompatibility = new ProviderRegistry();
178
+ brokenCompatibility.registerProvider(provider("claude", { compatibilityError: "broken version output" }));
179
+ assert.strictEqual(brokenCompatibility.providerStatus("claude").compatibility.state, "unknown");
180
+ const brokenAuth = new ProviderRegistry();
181
+ brokenAuth.registerProvider(provider("codex", { authError: "broken login status" }));
182
+ assert.strictEqual(brokenAuth.providerStatus("codex").auth.state, "unknown");
183
+
184
+ // Status caching: healthy auth-probed results are cached for the TTL so
185
+ // per-message admission does not respawn CLI probes; anything unhealthy
186
+ // re-probes every call so installs/logins recover immediately.
187
+ {
188
+ let clock = 0;
189
+ let compatProbes = 0;
190
+ let authProbes = 0;
191
+ const counted = provider("claude");
192
+ const innerCompat = counted.compatibilityStatus;
193
+ const innerAuth = counted.authStatus;
194
+ counted.compatibilityStatus = () => { compatProbes += 1; return innerCompat(); };
195
+ counted.authStatus = () => { authProbes += 1; return innerAuth(); };
196
+ const cachingRegistry = new ProviderRegistry({ statusTtlMs: 1000, now: () => clock });
197
+ cachingRegistry.registerProvider(counted);
198
+
199
+ cachingRegistry.providerStatus("claude");
200
+ cachingRegistry.providerStatus("claude");
201
+ cachingRegistry.resolveProviderForRun("claude");
202
+ assert.strictEqual(compatProbes, 1, "healthy status must be probed once within the TTL");
203
+ assert.strictEqual(authProbes, 1, "healthy auth must be probed once within the TTL");
204
+
205
+ const noAuthView = cachingRegistry.providerStatus("claude", { probeAuth: false });
206
+ assert.strictEqual(noAuthView.auth.state, "not_checked", "cache-served no-auth view must not leak auth state");
207
+ assert.strictEqual(compatProbes, 1);
208
+
209
+ const refreshed = cachingRegistry.providerStatus("claude", { refresh: true });
210
+ assert.strictEqual(refreshed.auth.state, "authenticated");
211
+ assert.strictEqual(compatProbes, 2, "refresh must bypass the cache");
212
+
213
+ clock = 5000;
214
+ cachingRegistry.providerStatus("claude");
215
+ assert.strictEqual(compatProbes, 3, "an expired entry must re-probe");
216
+
217
+ // Disappearance mid-TTL must be detected immediately: availability is a
218
+ // cheap lookup that always runs fresh, and it invalidates the cache.
219
+ let vanished = false;
220
+ counted.isAvailable = () => !vanished;
221
+ cachingRegistry.providerStatus("claude");
222
+ vanished = true;
223
+ assert.strictEqual(cachingRegistry.providerStatus("claude").availability.state, "missing",
224
+ "a cached healthy status must not mask a provider that disappeared");
225
+ vanished = false;
226
+ cachingRegistry.providerStatus("claude");
227
+ assert.strictEqual(compatProbes, 4, "disappearance must evict the cached status");
228
+
229
+ let coldProbes = 0;
230
+ const loggedOutProvider = provider("codex", { auth: { state: "unauthenticated", reason: "fixture is logged out" } });
231
+ const loggedOutCompat = loggedOutProvider.compatibilityStatus;
232
+ loggedOutProvider.compatibilityStatus = () => { coldProbes += 1; return loggedOutCompat(); };
233
+ cachingRegistry.registerProvider(loggedOutProvider);
234
+ cachingRegistry.providerStatus("codex");
235
+ cachingRegistry.providerStatus("codex");
236
+ assert.strictEqual(coldProbes, 2, "unhealthy status must never be cached");
237
+
238
+ // A no-auth probe must not seed the cache for auth-requiring callers.
239
+ let seedAuthProbes = 0;
240
+ const seeded = provider("claude");
241
+ const seededAuth = seeded.authStatus;
242
+ seeded.authStatus = () => { seedAuthProbes += 1; return seededAuth(); };
243
+ const seedRegistry = new ProviderRegistry({ statusTtlMs: 1000, now: () => 0 });
244
+ seedRegistry.registerProvider(seeded);
245
+ seedRegistry.providerStatus("claude", { probeAuth: false });
246
+ assert.strictEqual(seedAuthProbes, 0);
247
+ seedRegistry.resolveProviderForRun("claude");
248
+ assert.strictEqual(seedAuthProbes, 1, "auth-requiring callers must trigger a real auth probe after a no-auth probe");
249
+ }
250
+
251
+ console.log("provider registry OK");
@@ -0,0 +1,41 @@
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
+ const { createCodexProvider } = require("./core/providers/codex");
9
+
10
+ const baselinePath = path.join(__dirname, "test-fixtures", "current-provider-parity.json");
11
+ assert.ok(fs.existsSync(baselinePath), "current provider-parity baseline fixture must exist");
12
+ const baseline = JSON.parse(fs.readFileSync(baselinePath, "utf8"));
13
+ assert.strictEqual(baseline.codexResumedPlan.knownDefect, true);
14
+ assert.strictEqual(baseline.codexResumedPlan.invertInTask, 7);
15
+
16
+ const provider = createCodexProvider({
17
+ hookTransport: false,
18
+ resolveExecutable: () => "/fixture/codex",
19
+ buildEnv: () => ({ PATH: "/fixture" }),
20
+ getAuthStatus: () => ({ state: "authenticated", reason: null }),
21
+ });
22
+ const invocation = provider.buildMainInvocation(Object.freeze({
23
+ coreInstructions: "OPEN CLAUDIA CORE",
24
+ dynamicContext: "",
25
+ userPrompt: baseline.codexResumedPlan.promptSentinel,
26
+ resumeSessionId: baseline.codexResumedPlan.sessionId,
27
+ providerSettings: Object.freeze({ permissionMode: "plan" }),
28
+ }));
29
+
30
+ const args = invocation.args;
31
+ const sessionAt = args.indexOf(baseline.codexResumedPlan.sessionId);
32
+ const sandboxConfigAt = args.indexOf(baseline.codexResumedPlan.targetConstraints.configurationValue);
33
+ const developerConfigAt = args.indexOf(`developer_instructions=${JSON.stringify("OPEN CLAUDIA CORE")}`);
34
+ assert.ok(sessionAt > 0, "resumed invocation contains the session id");
35
+ assert.ok(sandboxConfigAt > 0 && args[sandboxConfigAt - 1] === "-c", "plan mode uses the documented config override");
36
+ assert.ok(sandboxConfigAt < sessionAt, "resume options precede the positional session id");
37
+ assert.ok(developerConfigAt > 0 && developerConfigAt < sessionAt, "developer instructions are present on resume");
38
+ assert.ok(!args.includes("--sandbox"), "resume never receives the unsupported --sandbox flag");
39
+ assert.strictEqual(args.at(-1), baseline.codexResumedPlan.promptSentinel);
40
+
41
+ console.log("provider resume parity OK");