@momentiq/dark-factory-cli 0.1.0-alpha.4

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 (132) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +150 -0
  3. package/dist/adapters/_shared.d.ts +45 -0
  4. package/dist/adapters/_shared.d.ts.map +1 -0
  5. package/dist/adapters/_shared.js +200 -0
  6. package/dist/adapters/_shared.js.map +1 -0
  7. package/dist/adapters/codex-sdk.d.ts +279 -0
  8. package/dist/adapters/codex-sdk.d.ts.map +1 -0
  9. package/dist/adapters/codex-sdk.js +930 -0
  10. package/dist/adapters/codex-sdk.js.map +1 -0
  11. package/dist/adapters/critic-result-schema.d.ts +215 -0
  12. package/dist/adapters/critic-result-schema.d.ts.map +1 -0
  13. package/dist/adapters/critic-result-schema.js +153 -0
  14. package/dist/adapters/critic-result-schema.js.map +1 -0
  15. package/dist/adapters/critic.d.ts +62 -0
  16. package/dist/adapters/critic.d.ts.map +1 -0
  17. package/dist/adapters/critic.js +79 -0
  18. package/dist/adapters/critic.js.map +1 -0
  19. package/dist/adapters/cursor-sdk.d.ts +153 -0
  20. package/dist/adapters/cursor-sdk.d.ts.map +1 -0
  21. package/dist/adapters/cursor-sdk.js +818 -0
  22. package/dist/adapters/cursor-sdk.js.map +1 -0
  23. package/dist/adapters/gemini-sdk.d.ts +113 -0
  24. package/dist/adapters/gemini-sdk.d.ts.map +1 -0
  25. package/dist/adapters/gemini-sdk.js +532 -0
  26. package/dist/adapters/gemini-sdk.js.map +1 -0
  27. package/dist/adapters/grok-direct-sdk.d.ts +148 -0
  28. package/dist/adapters/grok-direct-sdk.d.ts.map +1 -0
  29. package/dist/adapters/grok-direct-sdk.js +694 -0
  30. package/dist/adapters/grok-direct-sdk.js.map +1 -0
  31. package/dist/branch-protection/audit_branch_protection.py +759 -0
  32. package/dist/branch-protection/index.d.ts +25 -0
  33. package/dist/branch-protection/index.d.ts.map +1 -0
  34. package/dist/branch-protection/index.js +70 -0
  35. package/dist/branch-protection/index.js.map +1 -0
  36. package/dist/branch-protection/spec-default.yaml +314 -0
  37. package/dist/cli.d.ts +3 -0
  38. package/dist/cli.d.ts.map +1 -0
  39. package/dist/cli.js +581 -0
  40. package/dist/cli.js.map +1 -0
  41. package/dist/cycle-doc-validator/index.d.ts +43 -0
  42. package/dist/cycle-doc-validator/index.d.ts.map +1 -0
  43. package/dist/cycle-doc-validator/index.js +69 -0
  44. package/dist/cycle-doc-validator/index.js.map +1 -0
  45. package/dist/cycle-doc-validator/validate_cycle_doc.py +1260 -0
  46. package/dist/cycle-tracker-sync/attribute_pr_cycle_ref.py +384 -0
  47. package/dist/cycle-tracker-sync/index.d.ts +20 -0
  48. package/dist/cycle-tracker-sync/index.d.ts.map +1 -0
  49. package/dist/cycle-tracker-sync/index.js +71 -0
  50. package/dist/cycle-tracker-sync/index.js.map +1 -0
  51. package/dist/cycle-tracker-sync/sync_cycle_trackers.py +1093 -0
  52. package/dist/evidence/audit-trail.d.ts +59 -0
  53. package/dist/evidence/audit-trail.d.ts.map +1 -0
  54. package/dist/evidence/audit-trail.js +283 -0
  55. package/dist/evidence/audit-trail.js.map +1 -0
  56. package/dist/evidence/index.d.ts +4 -0
  57. package/dist/evidence/index.d.ts.map +1 -0
  58. package/dist/evidence/index.js +29 -0
  59. package/dist/evidence/index.js.map +1 -0
  60. package/dist/evidence/per-sha.d.ts +13 -0
  61. package/dist/evidence/per-sha.d.ts.map +1 -0
  62. package/dist/evidence/per-sha.js +97 -0
  63. package/dist/evidence/per-sha.js.map +1 -0
  64. package/dist/evidence/quality-gates.d.ts +19 -0
  65. package/dist/evidence/quality-gates.d.ts.map +1 -0
  66. package/dist/evidence/quality-gates.js +212 -0
  67. package/dist/evidence/quality-gates.js.map +1 -0
  68. package/dist/git.d.ts +40 -0
  69. package/dist/git.d.ts.map +1 -0
  70. package/dist/git.js +414 -0
  71. package/dist/git.js.map +1 -0
  72. package/dist/glob.d.ts +4 -0
  73. package/dist/glob.d.ts.map +1 -0
  74. package/dist/glob.js +99 -0
  75. package/dist/glob.js.map +1 -0
  76. package/dist/index.d.ts +17 -0
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/index.js +33 -0
  79. package/dist/index.js.map +1 -0
  80. package/dist/paths.d.ts +12 -0
  81. package/dist/paths.d.ts.map +1 -0
  82. package/dist/paths.js +42 -0
  83. package/dist/paths.js.map +1 -0
  84. package/dist/policy/baseline.d.ts +15 -0
  85. package/dist/policy/baseline.d.ts.map +1 -0
  86. package/dist/policy/baseline.js +115 -0
  87. package/dist/policy/baseline.js.map +1 -0
  88. package/dist/policy/config.d.ts +64 -0
  89. package/dist/policy/config.d.ts.map +1 -0
  90. package/dist/policy/config.js +363 -0
  91. package/dist/policy/config.js.map +1 -0
  92. package/dist/policy/gate.d.ts +80 -0
  93. package/dist/policy/gate.d.ts.map +1 -0
  94. package/dist/policy/gate.js +1019 -0
  95. package/dist/policy/gate.js.map +1 -0
  96. package/dist/policy/index.d.ts +7 -0
  97. package/dist/policy/index.d.ts.map +1 -0
  98. package/dist/policy/index.js +14 -0
  99. package/dist/policy/index.js.map +1 -0
  100. package/dist/policy/merge-queue.d.ts +183 -0
  101. package/dist/policy/merge-queue.d.ts.map +1 -0
  102. package/dist/policy/merge-queue.js +310 -0
  103. package/dist/policy/merge-queue.js.map +1 -0
  104. package/dist/policy/profile.d.ts +98 -0
  105. package/dist/policy/profile.d.ts.map +1 -0
  106. package/dist/policy/profile.js +156 -0
  107. package/dist/policy/profile.js.map +1 -0
  108. package/dist/policy/tdd-classifier.d.ts +18 -0
  109. package/dist/policy/tdd-classifier.d.ts.map +1 -0
  110. package/dist/policy/tdd-classifier.js +79 -0
  111. package/dist/policy/tdd-classifier.js.map +1 -0
  112. package/dist/prompt.d.ts +13 -0
  113. package/dist/prompt.d.ts.map +1 -0
  114. package/dist/prompt.js +175 -0
  115. package/dist/prompt.js.map +1 -0
  116. package/dist/report.d.ts +88 -0
  117. package/dist/report.d.ts.map +1 -0
  118. package/dist/report.js +376 -0
  119. package/dist/report.js.map +1 -0
  120. package/dist/runner.d.ts +30 -0
  121. package/dist/runner.d.ts.map +1 -0
  122. package/dist/runner.js +456 -0
  123. package/dist/runner.js.map +1 -0
  124. package/dist/security.d.ts +2 -0
  125. package/dist/security.d.ts.map +1 -0
  126. package/dist/security.js +19 -0
  127. package/dist/security.js.map +1 -0
  128. package/dist/trusted-surface/rebind.d.ts +10 -0
  129. package/dist/trusted-surface/rebind.d.ts.map +1 -0
  130. package/dist/trusted-surface/rebind.js +154 -0
  131. package/dist/trusted-surface/rebind.js.map +1 -0
  132. package/package.json +78 -0
@@ -0,0 +1,930 @@
1
+ // Cycle 322.7 — OpenAI Codex direct-SDK adapter via `@openai/codex-sdk`.
2
+ //
3
+ // Why a fourth adapter (manifesto §11 + §12): after 322.3 added the Grok
4
+ // critic the gate runs three vendor lineages (Cursor → OpenAI proxy,
5
+ // Gemini → Google, Grok → xAI), but the Cursor-routed GPT-5.5 critic
6
+ // empirically fails through Cursor's proxy capacity layer (Cursor support
7
+ // T-C42979, 2026-05-08 + 2026-05-11). A *direct* OpenAI critic via the
8
+ // Codex SDK closes that gap while exploiting the existing $200 ChatGPT
9
+ // Pro subscription instead of burning OpenAI API tokens per commit.
10
+ //
11
+ // The Codex SDK is a thin TypeScript wrapper around the `@openai/codex`
12
+ // CLI. The SDK spawns the CLI subprocess and exchanges JSONL events:
13
+ // - `~/.codex/auth.json` holds the cached subscription OAuth token
14
+ // (set up via `codex login`); the SDK subprocess inherits it.
15
+ // - `CODEX_API_KEY` env supports API-key auth (the CI cold path).
16
+ // - Critically, the SDK supports BOTH modes — declaring CODEX_API_KEY
17
+ // in `requiredEnvVars` would force Doppler re-exec on every local
18
+ // invocation even when subscription auth is the intended path.
19
+ // Instead the adapter declares `requiredEnvVars = []` and the
20
+ // doctor check validates AT LEAST ONE auth source is configured.
21
+ //
22
+ // The adapter:
23
+ // - implements `CriticAdapter` from `critic.ts` (the post-322.2 shape
24
+ // with `requiredEnvVars`)
25
+ // - uses Codex's `outputSchema` parameter (passes a JSON Schema to the
26
+ // model so the response in `Turn.finalResponse` is schema-validated
27
+ // JSON natively — no `parseAssistantJson` fallback chain needed,
28
+ // though we keep it as defense-in-depth for occasional format drift)
29
+ // - mirrors the 322.1 retry shape (`runRetryLoop` + per-attempt
30
+ // telemetry + permanent vs retryable failure classification) from a
31
+ // single source of truth in `cursor-sdk.ts`, so the policy + budget
32
+ // are byte-identical across adapters
33
+ // - routes diagnostic-redaction + JSON parsing + reviewer-metadata
34
+ // merge + error-result construction through `_shared.ts` so the
35
+ // security boundary cannot drift
36
+ // - is read-only by structure at THREE layers:
37
+ // 1. `sandboxMode: "read-only"` — the Codex CLI sandbox prevents
38
+ // file writes
39
+ // 2. `approvalPolicy: "never"` — no interactive prompts
40
+ // 3. `networkAccessEnabled: false` — the agent cannot exfiltrate
41
+ // diff content to external services
42
+ // Note: even with all three knobs set, the Codex agent CAN execute
43
+ // shell commands locally (read-only sandbox blocks WRITES, not
44
+ // READS — the spike artifact captures one such command_execution).
45
+ // This is acceptable because the agent runs over the diff under
46
+ // review; the read-only sandbox prevents the agent from
47
+ // modifying the repo state in any way.
48
+ //
49
+ // The implementation uses the dependency-injection ESCAPE hatch on the
50
+ // constructor (`createCodex` factory) so unit tests can supply a mock
51
+ // SDK that bypasses the real Codex CLI subprocess + network surface —
52
+ // this matches the testing posture of the 322.2 Gemini and 322.3 Grok
53
+ // adapters. The static import below pins `@openai/codex-sdk` as a hard
54
+ // dependency for production callers; tests still need the package
55
+ // resolvable at module-load time (so the static import succeeds) but
56
+ // the `createCodex` factory swap means tests never invoke the real
57
+ // Codex constructor or spawn the CLI subprocess. The doctor probe
58
+ // (`codex_sdk_loaded`) re-imports dynamically so a doctor run on a
59
+ // machine without the SDK installed surfaces a clear remediation
60
+ // instead of crashing at startup.
61
+ import { execFile } from "node:child_process";
62
+ import { existsSync } from "node:fs";
63
+ import { createRequire } from "node:module";
64
+ import os from "node:os";
65
+ import path from "node:path";
66
+ import { promisify } from "node:util";
67
+ import { Codex } from "@openai/codex-sdk";
68
+ import { compileCriticPrompt } from "../prompt.js";
69
+ import { parseCriticResult, } from "@momentiq/dark-factory-schemas";
70
+ import { CRITIC_RESULT_JSON_SCHEMA } from "./critic-result-schema.js";
71
+ import { buildErrorResult, mergeAdapterMetadata, normalizeCriticEcho, parseAssistantJson, writeRedactedDiagnostic, } from "./_shared.js";
72
+ import { PERMANENT_ERROR_CODES, runRetryLoop, } from "./cursor-sdk.js";
73
+ // Promisified `execFile` for safe arg-array subprocess invocations.
74
+ // NB: this is `execFile` (no shell parsing), NOT `exec` — fixed arg
75
+ // arrays are immune to command injection. The codex doctor probe
76
+ // invokes `codex --version` via this helper.
77
+ const execFileAsync = promisify(execFile);
78
+ export const CODEX_SDK_ADAPTER_ID = "codex-sdk";
79
+ export const CODEX_API_KEY_ENV = "CODEX_API_KEY";
80
+ export const CODEX_HOME_ENV = "CODEX_HOME";
81
+ // Issue #2103 — auth-mode vocabulary the codex-sdk adapter accepts on
82
+ // `critic.auth` (set by `applyProfileAuth()` from
83
+ // `profile.auth[critic.id]`). Strict-no-fallback contract:
84
+ //
85
+ // - "chatgpt": ChatGPT Pro / Plus / Business / Enterprise subscription
86
+ // OAuth via `~/.codex/auth.json` (cached by `codex login`). The
87
+ // adapter pins `forced_login_method: "chatgpt"` AND does NOT pass
88
+ // `apiKey` to the SDK — so a stray `CODEX_API_KEY` in the env
89
+ // cannot route the run through per-token API billing.
90
+ //
91
+ // - "api": `CODEX_API_KEY` env var (developer API key). The adapter
92
+ // pins `forced_login_method: "api"` and requires the env var to be
93
+ // set; if missing, the adapter throws a permanent_failure at
94
+ // attemptReview() — never falls back to subscription OAuth even if
95
+ // `~/.codex/auth.json` exists.
96
+ //
97
+ // When `critic.auth` is undefined (no profile auth pin), the adapter
98
+ // throws a configuration error directing the operator to add
99
+ // `profiles.<name>.auth[<critic.id>]` to `.agent-review/config.json`.
100
+ // This is intentional: removing the old env-presence inference means
101
+ // the operator's intent must be declared, not inferred. The error
102
+ // message names the critic + profile so the fix is mechanical.
103
+ export const CODEX_AUTH_CHATGPT = "chatgpt";
104
+ export const CODEX_AUTH_API = "api";
105
+ export const CODEX_AUTH_MODES = [
106
+ CODEX_AUTH_CHATGPT,
107
+ CODEX_AUTH_API,
108
+ ];
109
+ // ---------------------------------------------------------------------------
110
+ // Bundled-binary resolution (#1471 P2 #2)
111
+ //
112
+ // The Codex SDK bundles the `codex` CLI as an `optionalDependency` on the
113
+ // `@openai/codex` npm package (which itself optional-depends on per-platform
114
+ // binary packages like `@openai/codex-linux-x64`, `@openai/codex-darwin-arm64`).
115
+ // A standard `npm ci` from `tools/agent-review/` puts the platform binary on
116
+ // disk at `node_modules/@openai/codex-<platform>-<arch>/vendor/<target>/codex/codex`
117
+ // without putting `codex` on PATH. The SDK itself resolves this binary
118
+ // internally via `findCodexPath()` (private to the SDK package; not exported).
119
+ //
120
+ // This helper mirrors the SDK's resolution so the doctor probe can probe the
121
+ // SAME binary the SDK will spawn at review time. Falls back to returning
122
+ // `null` when any step of the resolution fails (no @openai/codex package,
123
+ // unsupported platform, missing platform package), and the doctor then falls
124
+ // back to probing the PATH `codex` binary.
125
+ const CODEX_NPM_NAME = "@openai/codex";
126
+ const PLATFORM_PACKAGE_BY_TARGET = {
127
+ "x86_64-unknown-linux-musl": "@openai/codex-linux-x64",
128
+ "aarch64-unknown-linux-musl": "@openai/codex-linux-arm64",
129
+ "x86_64-apple-darwin": "@openai/codex-darwin-x64",
130
+ "aarch64-apple-darwin": "@openai/codex-darwin-arm64",
131
+ "x86_64-pc-windows-msvc": "@openai/codex-win32-x64",
132
+ "aarch64-pc-windows-msvc": "@openai/codex-win32-arm64",
133
+ };
134
+ /**
135
+ * Compute the (platform, arch) target triple Codex uses to key its
136
+ * per-platform binary packages. Returns `null` for unsupported platforms.
137
+ * Exported for direct unit testing of the resolution logic.
138
+ */
139
+ export function targetTripleForCurrentPlatform(platform = process.platform, arch = process.arch) {
140
+ switch (platform) {
141
+ case "linux":
142
+ case "android":
143
+ if (arch === "x64")
144
+ return "x86_64-unknown-linux-musl";
145
+ if (arch === "arm64")
146
+ return "aarch64-unknown-linux-musl";
147
+ return null;
148
+ case "darwin":
149
+ if (arch === "x64")
150
+ return "x86_64-apple-darwin";
151
+ if (arch === "arm64")
152
+ return "aarch64-apple-darwin";
153
+ return null;
154
+ case "win32":
155
+ if (arch === "x64")
156
+ return "x86_64-pc-windows-msvc";
157
+ if (arch === "arm64")
158
+ return "aarch64-pc-windows-msvc";
159
+ return null;
160
+ default:
161
+ return null;
162
+ }
163
+ }
164
+ /**
165
+ * Resolve the absolute path to the Codex CLI binary bundled inside
166
+ * `@openai/codex`'s per-platform optional dependency. Returns `null` when
167
+ * any step of the resolution fails (so the doctor can fall back to PATH).
168
+ *
169
+ * Mirrors `findCodexPath()` in `@openai/codex-sdk@0.130.0` —
170
+ * `node_modules/@openai/codex-sdk/dist/index.js:368-433`. The SDK does
171
+ * not export this function publicly, so we replicate the lookup here
172
+ * using the same `createRequire` + `package.json` walk so the doctor
173
+ * probes the EXACT binary the SDK will spawn at review time.
174
+ */
175
+ export function resolveBundledCodexCliPath() {
176
+ const targetTriple = targetTripleForCurrentPlatform();
177
+ if (!targetTriple)
178
+ return null;
179
+ const platformPackage = PLATFORM_PACKAGE_BY_TARGET[targetTriple];
180
+ if (!platformPackage)
181
+ return null;
182
+ // `createRequire(import.meta.url)` returns a `require` rooted at THIS
183
+ // compiled module. The SDK's `@openai/codex` direct dep is reachable
184
+ // from us because `@openai/codex-sdk` is OUR direct dep and `@openai/codex`
185
+ // is the SDK's direct dep. npm's hoisting algorithm lifts both into our
186
+ // node_modules tree so the resolution walk succeeds.
187
+ try {
188
+ const moduleRequire = createRequire(import.meta.url);
189
+ const codexPackageJsonPath = moduleRequire.resolve(`${CODEX_NPM_NAME}/package.json`);
190
+ const codexRequire = createRequire(codexPackageJsonPath);
191
+ const platformPackageJsonPath = codexRequire.resolve(`${platformPackage}/package.json`);
192
+ const vendorRoot = path.join(path.dirname(platformPackageJsonPath), "vendor");
193
+ const codexBinaryName = process.platform === "win32" ? "codex.exe" : "codex";
194
+ const binaryPath = path.join(vendorRoot, targetTriple, "codex", codexBinaryName);
195
+ return existsSync(binaryPath) ? binaryPath : null;
196
+ }
197
+ catch {
198
+ return null;
199
+ }
200
+ }
201
+ /**
202
+ * Probe `codex login status` to detect authenticated state regardless of
203
+ * where credentials are stored (~/.codex/auth.json or OS keyring per
204
+ * `cli_auth_credentials_store: auto`). The CLI exits 0 when authenticated
205
+ * and non-zero when not, which is the canonical detector for #1471 P2 #1.
206
+ *
207
+ * The `exec` parameter defaults to the promisified `child_process.execFile`
208
+ * but accepts a test override so the probe can be invoked deterministically.
209
+ *
210
+ * Exported and used by {@link CodexSdkAdapter.doctor} (the doctor's
211
+ * `defaultAuthProbe` routes here with its `execCodex` hook injected).
212
+ */
213
+ export async function probeCodexLoginStatus(cliPath, exec = (binaryPath, args, options) => execFileAsync(binaryPath, args, options ?? {}), timeoutMs = 5_000) {
214
+ try {
215
+ const { stdout } = await exec(cliPath, ["login", "status"], {
216
+ timeout: timeoutMs,
217
+ });
218
+ return { loggedIn: true, detail: stdout.trim() || "codex login status: ok" };
219
+ }
220
+ catch (err) {
221
+ const e = err;
222
+ return { loggedIn: false, detail: `codex login status failed: ${e.message}` };
223
+ }
224
+ }
225
+ // Reasoning-effort param id used in `critic.model.params`; mapped to
226
+ // Codex's `model_reasoning_effort` CLI config key inside the adapter.
227
+ const REASONING_EFFORT_PARAM_ID = "reasoning_effort";
228
+ // Allowed Codex reasoning-effort values per the SDK's ModelReasoningEffort
229
+ // union (`@openai/codex-sdk` 0.130 exports: "minimal" | "low" | "medium"
230
+ // | "high" | "xhigh"). Codex docs note `xhigh` is model-dependent; the
231
+ // default of `high` is universally supported.
232
+ const ALLOWED_REASONING_EFFORTS = new Set([
233
+ "minimal",
234
+ "low",
235
+ "medium",
236
+ "high",
237
+ "xhigh",
238
+ ]);
239
+ export const DEFAULT_REASONING_EFFORT = "high";
240
+ /**
241
+ * Resolve the Codex reasoning-effort from the critic config's
242
+ * `model.params`. Falls back to {@link DEFAULT_REASONING_EFFORT} when
243
+ * unset. Invalid values fall back to the default rather than corrupting
244
+ * the request body. Coerces string inputs (the config schema's `value`
245
+ * is `string | number | boolean`; reasoning effort is always a string
246
+ * enum). Exported for direct unit testing.
247
+ */
248
+ export function resolveCodexReasoningEffort(critic) {
249
+ const param = critic.model.params.find((p) => p.id === REASONING_EFFORT_PARAM_ID);
250
+ if (!param)
251
+ return DEFAULT_REASONING_EFFORT;
252
+ const v = param.value;
253
+ if (typeof v !== "string")
254
+ return DEFAULT_REASONING_EFFORT;
255
+ const norm = v.toLowerCase();
256
+ if (ALLOWED_REASONING_EFFORTS.has(norm))
257
+ return norm;
258
+ return DEFAULT_REASONING_EFFORT;
259
+ }
260
+ /**
261
+ * Probe a thrown error for a Codex SDK structured error code. The
262
+ * Codex SDK surfaces errors as plain `Error` instances; some SDK
263
+ * upgrades add a `code` field (mirroring openai's `APIError.code`).
264
+ * The adapter treats:
265
+ * - `code` matching {@link PERMANENT_ERROR_CODES} → permanent
266
+ * - everything else → retryable
267
+ * Exported for unit testing.
268
+ */
269
+ export function extractCodexErrorCode(err) {
270
+ if (!err || typeof err !== "object")
271
+ return null;
272
+ const e = err;
273
+ if (typeof e["code"] === "string")
274
+ return e["code"];
275
+ // Some SDK shapes nest under `cause.code`:
276
+ const cause = e["cause"];
277
+ if (cause && typeof cause === "object") {
278
+ const c = cause["code"];
279
+ if (typeof c === "string")
280
+ return c;
281
+ }
282
+ return null;
283
+ }
284
+ /**
285
+ * Issue #2103 — strict auth resolver. Validates `critic.auth` against
286
+ * the codex adapter vocabulary ({@link CODEX_AUTH_MODES}) and surfaces
287
+ * the missing-key case as a `permanent_failure` so the outer retry
288
+ * loop returns the error immediately (retrying a configuration
289
+ * mistake just wastes budget). Returns the resolved mode + an
290
+ * `apiKey` value that is INTENTIONALLY `undefined` under the
291
+ * "chatgpt" branch so the caller passes nothing to the SDK
292
+ * constructor — a stray `CODEX_API_KEY` cannot override the
293
+ * subscription path.
294
+ *
295
+ * Three failure shapes, all permanent (no retry):
296
+ *
297
+ * 1. `critic.auth === undefined`: profile didn't pin auth. Surfaces
298
+ * as `permanent_failure` with a message naming the critic + the
299
+ * config path the operator should edit
300
+ * (`profiles.<name>.auth[<critic.id>]`).
301
+ *
302
+ * 2. `critic.auth` is set but not in {@link CODEX_AUTH_MODES}:
303
+ * typo / unsupported value. The error message enumerates the
304
+ * valid set so the operator can self-correct.
305
+ *
306
+ * 3. `critic.auth === "api"` AND no `CODEX_API_KEY` in env (and no
307
+ * `apiKey` constructor override): the configured source is
308
+ * missing. Same shape as the prior "missing key" path, with the
309
+ * error explicitly mentioning the `api` auth mode so the
310
+ * operator doesn't try to fix it by running `codex login`.
311
+ */
312
+ export function resolveAuthOrFail(critic, options, attemptIdx) {
313
+ const authRaw = critic.auth;
314
+ if (authRaw === undefined) {
315
+ return {
316
+ kind: "permanent_failure",
317
+ errorCode: null,
318
+ statusMessage: null,
319
+ result: buildErrorResult({
320
+ critic,
321
+ message: `codex critic "${critic.id}" has no auth source pinned. ` +
322
+ `Add \`profiles.<name>.auth["${critic.id}"]\` to .agent-review/config.json ` +
323
+ `with one of: ${CODEX_AUTH_MODES.join(", ")}. ` +
324
+ `(Issue #2103 — env-presence inference was removed to prevent silent fallback to API-key billing.)`,
325
+ retryable: false,
326
+ retryCount: attemptIdx,
327
+ }),
328
+ };
329
+ }
330
+ if (!CODEX_AUTH_MODES.includes(authRaw)) {
331
+ return {
332
+ kind: "permanent_failure",
333
+ errorCode: null,
334
+ statusMessage: null,
335
+ result: buildErrorResult({
336
+ critic,
337
+ message: `codex critic "${critic.id}" has unsupported auth value "${authRaw}". ` +
338
+ `Expected one of: ${CODEX_AUTH_MODES.join(", ")}. ` +
339
+ `Edit \`profiles.<name>.auth["${critic.id}"]\` in .agent-review/config.json.`,
340
+ retryable: false,
341
+ retryCount: attemptIdx,
342
+ }),
343
+ };
344
+ }
345
+ const mode = authRaw;
346
+ if (mode === CODEX_AUTH_API) {
347
+ const apiKey = options.apiKey ?? process.env[CODEX_API_KEY_ENV];
348
+ if (!apiKey) {
349
+ return {
350
+ kind: "permanent_failure",
351
+ errorCode: null,
352
+ statusMessage: null,
353
+ result: buildErrorResult({
354
+ critic,
355
+ message: `codex critic "${critic.id}" is pinned to auth="api" but ${CODEX_API_KEY_ENV} is not set. ` +
356
+ `Either provision the key (e.g. via Doppler) or change the profile to auth="${CODEX_AUTH_CHATGPT}" and run \`codex login\`.`,
357
+ retryable: false,
358
+ retryCount: attemptIdx,
359
+ }),
360
+ };
361
+ }
362
+ return { kind: "ok", mode, apiKey };
363
+ }
364
+ // mode === "chatgpt" — withhold apiKey so a stray env var cannot
365
+ // override subscription auth at the SDK level.
366
+ return { kind: "ok", mode, apiKey: undefined };
367
+ }
368
+ export class CodexSdkAdapter {
369
+ options;
370
+ id = CODEX_SDK_ADAPTER_ID;
371
+ // Cycle 322.7 — empty array because the Codex SDK supports BOTH auth modes:
372
+ // - Local: ~/.codex/auth.json from `codex login` (ChatGPT subscription OAuth)
373
+ // - CI: CODEX_API_KEY env var (OpenAI API key)
374
+ // doctor() validates that at least ONE is configured; the SDK subprocess
375
+ // inherits whichever is present. Declaring CODEX_API_KEY as "required"
376
+ // here would force Doppler re-exec even when subscription auth is the
377
+ // intended path.
378
+ requiredEnvVars = [];
379
+ createCodex;
380
+ constructor(options = {}) {
381
+ this.options = options;
382
+ this.createCodex =
383
+ options.createCodex ??
384
+ ((opts) => new Codex({
385
+ ...(opts.apiKey !== undefined ? { apiKey: opts.apiKey } : {}),
386
+ ...(opts.config !== undefined
387
+ ? { config: opts.config }
388
+ : {}),
389
+ }));
390
+ }
391
+ async review(packet, critic, options) {
392
+ return runRetryLoop({
393
+ attempt: (idx) => this.attemptReview(packet, critic, options, idx),
394
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
395
+ ...(this.options.sleep !== undefined ? { sleep: this.options.sleep } : {}),
396
+ buildExhausted: ({ last, totalAttempts, aborted }) => {
397
+ const retriesUsed = Math.max(0, totalAttempts - 1);
398
+ const summary = aborted
399
+ ? last
400
+ ? `codex SDK run aborted after ${retriesUsed} retries: ${last.message}`
401
+ : "codex SDK run aborted before any attempt completed"
402
+ : last
403
+ ? `codex SDK run failed after ${retriesUsed} retries: ${last.message}`
404
+ : "codex SDK run failed with no captured failure metadata";
405
+ return buildErrorResult({
406
+ critic,
407
+ message: summary,
408
+ retryable: true,
409
+ ...(last?.errorCode != null ? { code: last.errorCode } : {}),
410
+ retryCount: retriesUsed,
411
+ ...(last?.runId !== null && last?.runId !== undefined
412
+ ? { runId: last.runId }
413
+ : {}),
414
+ ...(last?.agentId !== null && last?.agentId !== undefined
415
+ ? { agentId: last.agentId }
416
+ : {}),
417
+ });
418
+ },
419
+ });
420
+ }
421
+ async attemptReview(packet, critic, options, attemptIdx) {
422
+ const reasoningEffort = resolveCodexReasoningEffort(critic);
423
+ // Issue #2103 — strict-no-fallback auth resolution. The runner sets
424
+ // `critic.auth` via `applyProfileAuth()` from
425
+ // `profile.auth[critic.id]`; this adapter honors it without
426
+ // env-presence inference.
427
+ //
428
+ // - "chatgpt": pin `forced_login_method: "chatgpt"` AND withhold
429
+ // `apiKey` from the SDK — so even if `CODEX_API_KEY` is set in
430
+ // the env (Doppler leaks the CI key into local shells), the
431
+ // SDK falls through to `~/.codex/auth.json` and bills against
432
+ // the ChatGPT Pro subscription quota.
433
+ //
434
+ // - "api": pin `forced_login_method: "api"` and REQUIRE
435
+ // `CODEX_API_KEY` (or the `apiKey` constructor override for
436
+ // tests). Missing key returns `permanent_failure` immediately;
437
+ // no fallback to subscription OAuth.
438
+ //
439
+ // - undefined: configuration error. The operator must declare
440
+ // auth at the profile level (see CLAUDE.md / issue #2103). The
441
+ // adapter throws `permanent_failure` naming the critic so the
442
+ // fix is mechanical (add `profiles.<name>.auth[<critic.id>]`).
443
+ const authResolved = resolveAuthOrFail(critic, this.options, attemptIdx);
444
+ if (authResolved.kind === "permanent_failure")
445
+ return authResolved;
446
+ const { mode: forcedLoginMethod, apiKey } = authResolved;
447
+ const prompt = compileCriticPrompt({
448
+ packet,
449
+ critic,
450
+ blockingSeverities: options.blockingSeverities,
451
+ treatDiffAsUntrusted: true,
452
+ });
453
+ const startMs = Date.now();
454
+ if (attemptIdx === 0) {
455
+ options.emit?.({
456
+ ts: new Date().toISOString(),
457
+ event: "critic_run_started",
458
+ commit: packet.commit.sha,
459
+ criticId: critic.id,
460
+ adapter: this.id,
461
+ model: critic.model.id,
462
+ });
463
+ }
464
+ let codex;
465
+ try {
466
+ codex = this.createCodex({
467
+ ...(apiKey !== undefined ? { apiKey } : {}),
468
+ config: {
469
+ model: critic.model.id,
470
+ // Codex CLI key. Pinned via the constructor `config` map which
471
+ // the SDK serializes into `--config key=value` CLI overrides.
472
+ model_reasoning_effort: reasoningEffort,
473
+ // Reasoning tokens are model-internal; surfacing them in the
474
+ // event stream just bloats artifacts.
475
+ show_raw_agent_reasoning: false,
476
+ // Belt-and-suspenders against a stray CODEX_API_KEY clobbering
477
+ // the intended subscription path (or vice versa).
478
+ forced_login_method: forcedLoginMethod,
479
+ },
480
+ });
481
+ }
482
+ catch (err) {
483
+ const e = err;
484
+ options.emit?.({
485
+ ts: new Date().toISOString(),
486
+ event: "critic_run_error",
487
+ commit: packet.commit.sha,
488
+ criticId: critic.id,
489
+ adapter: this.id,
490
+ model: critic.model.id,
491
+ durationMs: Date.now() - startMs,
492
+ error: e.message,
493
+ status: "startup_failure",
494
+ retryCount: attemptIdx,
495
+ });
496
+ return {
497
+ kind: "retryable_failure",
498
+ errorCode: null,
499
+ statusMessage: null,
500
+ message: `codex SDK construction failed: ${e.message}`,
501
+ runId: null,
502
+ agentId: null,
503
+ };
504
+ }
505
+ let thread;
506
+ try {
507
+ thread = codex.startThread({
508
+ workingDirectory: packet.repoRoot,
509
+ // Defense in depth — critic must never write files even if a
510
+ // malicious diff convinces the model to try. Read-only sandbox
511
+ // blocks WRITES, not READS, so the agent may still run shell
512
+ // commands to explore the repo (see fixtures/spike-codex-2026-05.json).
513
+ sandboxMode: "read-only",
514
+ // No interactive prompts in non-interactive runs.
515
+ approvalPolicy: "never",
516
+ // Critic must not exfiltrate diff content to external services.
517
+ networkAccessEnabled: false,
518
+ // packet.repoRoot IS a git repo; let the CLI confirm.
519
+ skipGitRepoCheck: false,
520
+ });
521
+ }
522
+ catch (err) {
523
+ const e = err;
524
+ options.emit?.({
525
+ ts: new Date().toISOString(),
526
+ event: "critic_run_error",
527
+ commit: packet.commit.sha,
528
+ criticId: critic.id,
529
+ adapter: this.id,
530
+ model: critic.model.id,
531
+ durationMs: Date.now() - startMs,
532
+ error: e.message,
533
+ status: "startup_failure",
534
+ retryCount: attemptIdx,
535
+ });
536
+ return {
537
+ kind: "retryable_failure",
538
+ errorCode: null,
539
+ statusMessage: null,
540
+ message: `codex startThread failed: ${e.message}`,
541
+ runId: null,
542
+ agentId: null,
543
+ };
544
+ }
545
+ let turn;
546
+ try {
547
+ turn = await thread.run(prompt.text, {
548
+ outputSchema: CRITIC_RESULT_JSON_SCHEMA,
549
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
550
+ });
551
+ }
552
+ catch (err) {
553
+ const e = err;
554
+ const codeStr = extractCodexErrorCode(err);
555
+ // Permanent codes are the same classification used by the Cursor
556
+ // adapter: auth/quota/policy failures where retrying wastes
557
+ // budget AND can mask the real fault.
558
+ const permanent = codeStr !== null && PERMANENT_ERROR_CODES.has(codeStr);
559
+ const finalCode = codeStr ?? "transport_error";
560
+ options.emit?.({
561
+ ts: new Date().toISOString(),
562
+ event: "critic_run_error",
563
+ commit: packet.commit.sha,
564
+ criticId: critic.id,
565
+ adapter: this.id,
566
+ model: critic.model.id,
567
+ durationMs: Date.now() - startMs,
568
+ error: e.message,
569
+ status: permanent ? "run_failure_permanent" : "run_failure",
570
+ retryCount: attemptIdx,
571
+ errorCode: finalCode,
572
+ ...(thread.id !== null ? { runId: thread.id } : {}),
573
+ });
574
+ if (permanent) {
575
+ return {
576
+ kind: "permanent_failure",
577
+ errorCode: finalCode,
578
+ statusMessage: null,
579
+ result: buildErrorResult({
580
+ critic,
581
+ message: `codex SDK run failed (permanent, code=${finalCode}): ${e.message}`,
582
+ retryable: false,
583
+ code: finalCode,
584
+ retryCount: attemptIdx,
585
+ ...(thread.id !== null ? { runId: thread.id } : {}),
586
+ }),
587
+ };
588
+ }
589
+ return {
590
+ kind: "retryable_failure",
591
+ errorCode: finalCode,
592
+ statusMessage: null,
593
+ message: `codex SDK run failed: ${e.message}`,
594
+ runId: thread.id,
595
+ agentId: null,
596
+ };
597
+ }
598
+ // Parse path. With `outputSchema` set, Codex enforces schema-validated
599
+ // JSON at the model level; the `parseAssistantJson` fallback chain is
600
+ // defense in depth against occasional format drift in older models or
601
+ // SDK regressions.
602
+ const parseOutcome = parseAssistantJson(turn.finalResponse);
603
+ if (!parseOutcome.ok) {
604
+ const diagPath = writeRedactedDiagnostic({
605
+ diagnosticsDir: options.diagnosticsDir,
606
+ criticId: critic.id,
607
+ commit: packet.commit.sha,
608
+ rawText: turn.finalResponse,
609
+ });
610
+ options.emit?.({
611
+ ts: new Date().toISOString(),
612
+ event: "critic_run_error",
613
+ commit: packet.commit.sha,
614
+ criticId: critic.id,
615
+ adapter: this.id,
616
+ model: critic.model.id,
617
+ durationMs: Date.now() - startMs,
618
+ error: `invalid critic JSON: ${parseOutcome.message}`,
619
+ status: "invalid_json",
620
+ retryCount: attemptIdx,
621
+ ...(thread.id !== null ? { runId: thread.id } : {}),
622
+ });
623
+ return {
624
+ kind: "permanent_failure",
625
+ errorCode: null,
626
+ statusMessage: null,
627
+ result: buildErrorResult({
628
+ critic,
629
+ message: `codex critic returned invalid JSON: ${parseOutcome.message}`,
630
+ retryable: false,
631
+ ...(diagPath !== undefined ? { rawSamplePath: diagPath } : {}),
632
+ ...(thread.id !== null ? { runId: thread.id } : {}),
633
+ retryCount: attemptIdx,
634
+ }),
635
+ };
636
+ }
637
+ let result;
638
+ try {
639
+ // Drop schema-invalid `validation.qualityGateResults[]` entries
640
+ // (e.g. model emits `gate` instead of `command`) BEFORE strict
641
+ // parsing — the validation block is informational and gets
642
+ // overwritten below with deterministic packet evidence. Issue #1484.
643
+ const normalized = normalizeCriticEcho(parseOutcome.value);
644
+ const enriched = mergeAdapterMetadata(normalized, {
645
+ critic,
646
+ ...(thread.id !== null ? { runId: thread.id } : {}),
647
+ });
648
+ result = parseCriticResult(enriched, options.blockingSeverities);
649
+ }
650
+ catch (err) {
651
+ const e = err;
652
+ const diagPath = writeRedactedDiagnostic({
653
+ diagnosticsDir: options.diagnosticsDir,
654
+ criticId: critic.id,
655
+ commit: packet.commit.sha,
656
+ rawText: turn.finalResponse,
657
+ });
658
+ options.emit?.({
659
+ ts: new Date().toISOString(),
660
+ event: "critic_run_error",
661
+ commit: packet.commit.sha,
662
+ criticId: critic.id,
663
+ adapter: this.id,
664
+ model: critic.model.id,
665
+ durationMs: Date.now() - startMs,
666
+ error: `schema validation failed: ${e.message}`,
667
+ status: "schema_violation",
668
+ retryCount: attemptIdx,
669
+ ...(thread.id !== null ? { runId: thread.id } : {}),
670
+ });
671
+ return {
672
+ kind: "permanent_failure",
673
+ errorCode: null,
674
+ statusMessage: null,
675
+ result: buildErrorResult({
676
+ critic,
677
+ message: `codex critic JSON failed schema validation: ${e.message}`,
678
+ retryable: false,
679
+ ...(diagPath !== undefined ? { rawSamplePath: diagPath } : {}),
680
+ ...(thread.id !== null ? { runId: thread.id } : {}),
681
+ retryCount: attemptIdx,
682
+ }),
683
+ };
684
+ }
685
+ const durationMs = Date.now() - startMs;
686
+ const enriched = {
687
+ ...result,
688
+ durationMs,
689
+ validation: {
690
+ qualityGateResults: packet.validation.evidence,
691
+ qualityGatesMissing: packet.validation.missing,
692
+ },
693
+ };
694
+ const blockerCount = enriched.findings.filter((f) => f.severity === "blocker").length;
695
+ const highCount = enriched.findings.filter((f) => f.severity === "high").length;
696
+ options.emit?.({
697
+ ts: new Date().toISOString(),
698
+ event: "critic_run_finished",
699
+ commit: packet.commit.sha,
700
+ criticId: critic.id,
701
+ adapter: this.id,
702
+ model: critic.model.id,
703
+ ...(thread.id !== null ? { runId: thread.id } : {}),
704
+ durationMs,
705
+ ...(enriched.verdict !== undefined ? { verdict: enriched.verdict } : {}),
706
+ findingCount: enriched.findings.length,
707
+ blockerCount,
708
+ highCount,
709
+ ...(typeof turn.usage?.input_tokens === "number"
710
+ ? { tokensIn: turn.usage.input_tokens }
711
+ : {}),
712
+ ...(typeof turn.usage?.output_tokens === "number"
713
+ ? { tokensOut: turn.usage.output_tokens }
714
+ : {}),
715
+ status: "complete",
716
+ retryCount: attemptIdx,
717
+ });
718
+ return { kind: "success", result: enriched };
719
+ }
720
+ async doctor(critic) {
721
+ const checks = [];
722
+ const apiKey = this.options.apiKey ?? process.env[CODEX_API_KEY_ENV];
723
+ const codexHome = this.options.codexHome ?? process.env[CODEX_HOME_ENV] ?? path.join(os.homedir(), ".codex");
724
+ const authPath = path.join(codexHome, "auth.json");
725
+ const authExists = existsSync(authPath);
726
+ // Cycle 322.7 follow-up #1492 + #1471 P2 #2: probe the BUNDLED Codex
727
+ // CLI binary (resolved via `@openai/codex` optional dep) before
728
+ // falling back to PATH. The SDK uses the bundled binary internally
729
+ // via `findCodexPath()` (not exported), so probing the same binary
730
+ // here is the only way for the doctor to reflect the SDK's runtime
731
+ // truth on a fresh `npm ci`-only install (no `npm install -g`).
732
+ //
733
+ // Resolution precedence:
734
+ // 1. `options.codexCliPathResolver` (test hook)
735
+ // 2. `resolveBundledCodexCliPath()` (production: walks
736
+ // @openai/codex package + platform-specific binary package)
737
+ // 3. PATH literal `codex` (fallback for workstations using
738
+ // `brew install codex` or `npm install -g @openai/codex`)
739
+ const resolveBundled = this.options.codexCliPathResolver ?? resolveBundledCodexCliPath;
740
+ const bundledPath = resolveBundled();
741
+ const execCodex = this.options.execCodex ??
742
+ ((binaryPath, args, opts) => execFileAsync(binaryPath, args, opts ?? {}));
743
+ // Cycle 322.7 follow-up #1471 P2 #1: probe `codex login status` to
744
+ // detect keyring-backed auth. The default probe delegates to the
745
+ // exported `probeCodexLoginStatus`, passing the SAME `execCodex`
746
+ // hook so test overrides apply uniformly. The probed binary is the
747
+ // bundled one when resolvable, falling back to PATH `codex` — the
748
+ // same binary the SDK will spawn at review time, so the doctor's
749
+ // verdict reflects runtime truth. Skipped (treated as
750
+ // `loggedIn: false` with informational detail) when no codex
751
+ // binary is resolvable at all — the codex_cli_on_path check below
752
+ // will surface that as the actionable failure.
753
+ const defaultAuthProbe = () => probeCodexLoginStatus(bundledPath ?? "codex", execCodex);
754
+ const authProbe = this.options.codexAuthProbe ?? defaultAuthProbe;
755
+ const authProbeOutcome = await authProbe();
756
+ // Issue #2103 — strict auth check honors `critic.auth` when set:
757
+ // - "chatgpt": ONLY subscription sources count (file or keyring).
758
+ // A stray CODEX_API_KEY in the env does NOT pass the check —
759
+ // the whole point of the pin is to surface "subscription auth
760
+ // is not actually configured here" so the operator fixes it
761
+ // instead of silently routing to API billing.
762
+ // - "api": ONLY CODEX_API_KEY counts. Subscription presence is
763
+ // ignored (it would mislead the operator into thinking they're
764
+ // set up for CI when they aren't).
765
+ // - undefined (no profile context, direct adapter usage, tests):
766
+ // legacy "AT LEAST ONE of three sources" — preserves
767
+ // back-compat for non-profile call sites.
768
+ //
769
+ // The detail string discloses which path is active so operators
770
+ // can debug "I logged in but doctor still says missing".
771
+ const subscriptionActive = authExists || authProbeOutcome.loggedIn;
772
+ let hasAuth;
773
+ let authDetail;
774
+ let authRemediation;
775
+ if (critic.auth === CODEX_AUTH_CHATGPT) {
776
+ hasAuth = subscriptionActive;
777
+ if (authExists) {
778
+ authDetail = `${authPath} exists (subscription auth, file-backed; auth=${CODEX_AUTH_CHATGPT})`;
779
+ }
780
+ else if (authProbeOutcome.loggedIn) {
781
+ authDetail = `codex login status reports authenticated (keyring-backed; auth=${CODEX_AUTH_CHATGPT}): ${authProbeOutcome.detail}`;
782
+ }
783
+ else {
784
+ authDetail = `critic pinned to auth="${CODEX_AUTH_CHATGPT}" but no subscription source: ${authPath} missing; ${authProbeOutcome.detail}`;
785
+ }
786
+ authRemediation = `run \`codex login\` once on this workstation to authenticate the ChatGPT Pro/Plus/Business/Enterprise subscription (supports keyring or ${authPath} depending on cli_auth_credentials_store)`;
787
+ }
788
+ else if (critic.auth === CODEX_AUTH_API) {
789
+ hasAuth = Boolean(apiKey);
790
+ authDetail = apiKey
791
+ ? `${CODEX_API_KEY_ENV} set (API-key auth; auth=${CODEX_AUTH_API})`
792
+ : `critic pinned to auth="${CODEX_AUTH_API}" but ${CODEX_API_KEY_ENV} is unset`;
793
+ authRemediation = `export ${CODEX_API_KEY_ENV}=... (typically via Doppler for the CI profile)`;
794
+ }
795
+ else {
796
+ // Back-compat: no profile pin → any of the three sources works.
797
+ hasAuth = Boolean(apiKey) || subscriptionActive;
798
+ if (apiKey) {
799
+ authDetail = `${CODEX_API_KEY_ENV} set (API-key auth; no profile pin)`;
800
+ }
801
+ else if (authExists) {
802
+ authDetail = `${authPath} exists (subscription auth, file-backed; no profile pin)`;
803
+ }
804
+ else if (authProbeOutcome.loggedIn) {
805
+ authDetail = `codex login status reports authenticated (keyring-backed; no profile pin): ${authProbeOutcome.detail}`;
806
+ }
807
+ else {
808
+ authDetail = `no auth source: ${CODEX_API_KEY_ENV} unset; ${authPath} missing; ${authProbeOutcome.detail}`;
809
+ }
810
+ authRemediation = `run \`codex login\` once on this workstation (subscription auth) or \`export ${CODEX_API_KEY_ENV}=...\` (CI / Doppler), then pin via \`profiles.<name>.auth["${critic.id}"]\` in .agent-review/config.json`;
811
+ }
812
+ checks.push({
813
+ name: "codex_auth_present",
814
+ passed: hasAuth,
815
+ detail: authDetail,
816
+ ...(hasAuth ? {} : { remediation: authRemediation }),
817
+ });
818
+ // SDK package presence — same pattern as gemini-sdk.ts:559-577.
819
+ let sdkLoaded = false;
820
+ try {
821
+ const mod = (await import("@openai/codex-sdk"));
822
+ sdkLoaded = typeof mod["Codex"] === "function";
823
+ }
824
+ catch {
825
+ sdkLoaded = false;
826
+ }
827
+ checks.push({
828
+ name: "codex_sdk_loaded",
829
+ passed: sdkLoaded,
830
+ detail: sdkLoaded
831
+ ? "@openai/codex-sdk imported"
832
+ : "@openai/codex-sdk missing or shape unexpected",
833
+ ...(sdkLoaded
834
+ ? {}
835
+ : { remediation: "make agent-review-deps && make agent-review-build" }),
836
+ });
837
+ // The SDK spawns the `codex` binary as a subprocess. Probe the
838
+ // bundled binary first (resolved via @openai/codex's optional
839
+ // dep), then fall back to PATH `codex` (workstations using
840
+ // `brew install codex` or `npm install -g @openai/codex`). The
841
+ // probe MUST pass for one of the two — without a working binary,
842
+ // every Codex critic invocation fails at first turn (issue #1492).
843
+ let cliCheck;
844
+ const probeArgs = ["--version"];
845
+ let probedBinaryPath;
846
+ let probedSource = null;
847
+ let probeError = null;
848
+ let stdout = "";
849
+ if (bundledPath) {
850
+ probedBinaryPath = bundledPath;
851
+ try {
852
+ const result = await execCodex(bundledPath, probeArgs, { timeout: 5_000 });
853
+ stdout = result.stdout;
854
+ probedSource = "bundled";
855
+ }
856
+ catch (err) {
857
+ probeError = err;
858
+ }
859
+ }
860
+ else {
861
+ probedBinaryPath = "codex";
862
+ }
863
+ if (probedSource === null) {
864
+ // Either bundled was unresolvable, or it was resolvable but the
865
+ // probe failed. Fall back to PATH `codex` so the doctor accounts
866
+ // for workstations that don't have a bundled binary (npm ci
867
+ // skipped optional deps, platform mismatch, etc.).
868
+ try {
869
+ const result = await execCodex("codex", probeArgs, { timeout: 5_000 });
870
+ stdout = result.stdout;
871
+ probedBinaryPath = "codex";
872
+ probedSource = "PATH";
873
+ }
874
+ catch (err) {
875
+ // Preserve the first error if PATH probe also fails. This
876
+ // gives operators a more actionable message ("bundled was
877
+ // missing too") than just "spawn codex ENOENT" from PATH.
878
+ if (probeError === null)
879
+ probeError = err;
880
+ }
881
+ }
882
+ if (probedSource !== null) {
883
+ const sourceLabel = probedSource === "bundled"
884
+ ? `bundled @openai/codex binary at ${probedBinaryPath}`
885
+ : `codex on PATH`;
886
+ cliCheck = {
887
+ name: "codex_cli_on_path",
888
+ passed: true,
889
+ detail: `${sourceLabel} reports: ${stdout.trim()}`,
890
+ };
891
+ }
892
+ else {
893
+ const probeMsg = probeError?.message ?? "unknown error";
894
+ cliCheck = {
895
+ name: "codex_cli_on_path",
896
+ passed: false,
897
+ detail: `codex CLI not resolvable: ${probeMsg} (neither bundled @openai/codex binary nor PATH codex responded to --version)`,
898
+ remediation:
899
+ // Pin the suggested CLI version to the same one @openai/codex-sdk's
900
+ // direct dep declares (currently 0.130.0; check
901
+ // tools/agent-review/node_modules/@openai/codex-sdk/package.json
902
+ // after a bump). Aligns workstation install posture with the
903
+ // version pin in .github/workflows/agent-critic.yml so doctor +
904
+ // CI + runtime CLI semantics stay deterministic.
905
+ "install the Codex CLI pinned to the SDK's resolved version (currently 0.130.0). Recommended for CI: `npm install -g @openai/codex@0.130.0` (also satisfies the SDK's bundled-binary lookup if `npm ci` failed to install the optional platform package). Workstations: `brew install codex` is also supported on macOS but is unpinned — prefer `npm install -g @openai/codex@<lockfile-version>` to match the SDK's bundled-binary semantics. After install, run `codex login` once to seed authentication (keyring or ~/.codex/auth.json depending on cli_auth_credentials_store).",
906
+ };
907
+ }
908
+ checks.push(cliCheck);
909
+ // Sanity: critic.model.id should look like a Codex model id (gpt-*,
910
+ // o*, etc). The Codex SDK doesn't expose a `models.list` surface, so
911
+ // this check stays heuristic. The runtime error from an invalid
912
+ // model id is loud enough that we don't lose visibility — but a
913
+ // doctor-time heuristic catches obvious config typos earlier.
914
+ const familyOk = /^(gpt-|o\d)/i.test(critic.model.id);
915
+ checks.push({
916
+ name: "codex_model_id_family",
917
+ passed: familyOk,
918
+ detail: familyOk
919
+ ? `${critic.model.id} matches gpt-*/o* family pattern`
920
+ : `${critic.model.id} does NOT match expected Codex model family (gpt-* or o*)`,
921
+ ...(familyOk
922
+ ? {}
923
+ : {
924
+ remediation: "the configured Codex critic's model.id should look like a Codex model (e.g., 'gpt-5.5-codex'). Update .agent-review/config.json:critics[].model.id.",
925
+ }),
926
+ });
927
+ return checks;
928
+ }
929
+ }
930
+ //# sourceMappingURL=codex-sdk.js.map