@kbediako/codex-orchestrator 0.2.0 → 0.2.1

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 (41) hide show
  1. package/README.md +43 -83
  2. package/dist/bin/codex-orchestrator.js +2 -0
  3. package/dist/orchestrator/src/cli/adapters/CommandBuilder.js +50 -0
  4. package/dist/orchestrator/src/cli/adapters/cloudFailureDiagnostics.js +117 -5
  5. package/dist/orchestrator/src/cli/coStatusAttachCliShell.js +2 -2
  6. package/dist/orchestrator/src/cli/coStatusCliShell.js +28 -6
  7. package/dist/orchestrator/src/cli/codexCliShell.js +48 -1
  8. package/dist/orchestrator/src/cli/codexDefaultsSetup.js +217 -26
  9. package/dist/orchestrator/src/cli/control/controlHostSupervision.js +28 -6
  10. package/dist/orchestrator/src/cli/control/controlRuntime.js +17 -6
  11. package/dist/orchestrator/src/cli/control/controlStatusDashboard.js +6 -1
  12. package/dist/orchestrator/src/cli/control/selectedRunProjection.js +49 -2
  13. package/dist/orchestrator/src/cli/doctor.js +142 -48
  14. package/dist/orchestrator/src/cli/init.js +94 -1
  15. package/dist/orchestrator/src/cli/providerLinearChildLaneRunner.js +64 -1
  16. package/dist/orchestrator/src/cli/providerLinearWorkerRunner.js +1165 -69
  17. package/dist/orchestrator/src/cli/rlm/alignment.js +3 -3
  18. package/dist/orchestrator/src/cli/services/commandRunner.js +31 -0
  19. package/dist/orchestrator/src/cli/utils/cloudPreflight.js +202 -6
  20. package/dist/orchestrator/src/cli/utils/codexFeatures.js +60 -0
  21. package/dist/orchestrator/src/manager.js +74 -4
  22. package/dist/scripts/lib/docs-catalog.js +35 -1
  23. package/docs/README.md +333 -0
  24. package/docs/book/README.md +19 -0
  25. package/docs/book/codex-cli-0124-adoption.md +68 -0
  26. package/docs/book/local-hook-impact.md +73 -0
  27. package/docs/book/operations.md +60 -0
  28. package/docs/book/public-posture.md +34 -0
  29. package/docs/book/setup.md +91 -0
  30. package/docs/book/skills.md +11 -0
  31. package/docs/guides/codex-version-policy.md +104 -0
  32. package/docs/public/downstream-setup.md +25 -18
  33. package/package.json +4 -1
  34. package/plugins/codex-orchestrator/.codex-plugin/plugin.json +1 -1
  35. package/plugins/codex-orchestrator/launcher.mjs +6 -4
  36. package/schemas/manifest.json +17 -0
  37. package/skills/README.md +26 -0
  38. package/skills/collab-subagents-first/SKILL.md +1 -1
  39. package/skills/delegation-usage/DELEGATION_GUIDE.md +12 -7
  40. package/skills/delegation-usage/SKILL.md +13 -8
  41. package/templates/codex/AGENTS.md +12 -10
@@ -46,9 +46,9 @@ export const DEFAULT_ALIGNMENT_POLICY = {
46
46
  mandatory_turn_window: 20
47
47
  },
48
48
  route: {
49
- sentinel_model: 'gpt-5.4',
50
- high_reasoning_model: 'gpt-5.4',
51
- arbitration_model: 'gpt-5.4',
49
+ sentinel_model: 'gpt-5.5',
50
+ high_reasoning_model: 'gpt-5.5',
51
+ arbitration_model: 'gpt-5.5',
52
52
  high_reasoning_available: true
53
53
  }
54
54
  };
@@ -354,6 +354,8 @@ export async function runCommandStage(context, hooks = {}) {
354
354
  providerLinearWorkerProof = null;
355
355
  providerLinearWorkerProofRecord = null;
356
356
  }
357
+ manifest.provider_linear_worker_tokens =
358
+ buildProviderLinearWorkerManifestTokenUsage(providerLinearWorkerProof?.tokens) ?? null;
357
359
  if (result.status === 'succeeded' && providerLinearWorkerProofRecord === null) {
358
360
  providerLinearWorkerFailureReason = 'provider_linear_worker_proof_missing_or_unreadable';
359
361
  effectiveSummary = buildProviderLinearWorkerTerminalSummary({
@@ -773,6 +775,35 @@ async function loadProviderLinearWorkerProof(proofPath) {
773
775
  return null;
774
776
  }
775
777
  }
778
+ function buildProviderLinearWorkerManifestTokenUsage(tokens) {
779
+ if (!tokens || typeof tokens !== 'object') {
780
+ return null;
781
+ }
782
+ const inputTokens = normalizeManifestTokenCount(tokens.input_tokens);
783
+ const outputTokens = normalizeManifestTokenCount(tokens.output_tokens);
784
+ const totalTokens = normalizeManifestTokenCount(tokens.total_tokens);
785
+ const reasoningOutputTokens = normalizeManifestTokenCount(tokens.reasoning_output_tokens);
786
+ if (inputTokens === null &&
787
+ outputTokens === null &&
788
+ totalTokens === null &&
789
+ reasoningOutputTokens === null) {
790
+ return null;
791
+ }
792
+ const usage = {
793
+ input_tokens: inputTokens,
794
+ output_tokens: outputTokens,
795
+ total_tokens: totalTokens
796
+ };
797
+ if (reasoningOutputTokens !== null) {
798
+ usage.reasoning_output_tokens = reasoningOutputTokens;
799
+ }
800
+ return usage;
801
+ }
802
+ function normalizeManifestTokenCount(value) {
803
+ return typeof value === 'number' && Number.isFinite(value)
804
+ ? Math.max(0, Math.trunc(value))
805
+ : null;
806
+ }
776
807
  function coerceTelemetryString(value) {
777
808
  if (typeof value !== 'string') {
778
809
  return null;
@@ -2,14 +2,32 @@ import process from 'node:process';
2
2
  import { spawn } from 'node:child_process';
3
3
  import { fingerprintAuthProvenanceValue } from './authProvenanceFingerprint.js';
4
4
  import { resolveCodexCliBin } from './codexCli.js';
5
+ function formatCommandSpawnError(error) {
6
+ if (error instanceof Error) {
7
+ const errno = error;
8
+ const code = typeof errno.code === 'string' ? errno.code : null;
9
+ if (code && !error.message.includes(code)) {
10
+ return `${code}: ${error.message}`;
11
+ }
12
+ return error.message;
13
+ }
14
+ return String(error);
15
+ }
5
16
  function runCommand(command, args, options) {
6
17
  const timeoutMs = options.timeoutMs ?? 10_000;
7
18
  return new Promise((resolve) => {
8
- const child = spawn(command, args, {
9
- cwd: options.cwd,
10
- env: options.env,
11
- stdio: ['ignore', 'pipe', 'pipe']
12
- });
19
+ let child;
20
+ try {
21
+ child = spawn(command, args, {
22
+ cwd: options.cwd,
23
+ env: options.env,
24
+ stdio: ['ignore', 'pipe', 'pipe']
25
+ });
26
+ }
27
+ catch (error) {
28
+ resolve({ exitCode: 1, stdout: '', stderr: formatCommandSpawnError(error) });
29
+ return;
30
+ }
13
31
  let stdout = '';
14
32
  let stderr = '';
15
33
  let settled = false;
@@ -39,7 +57,7 @@ function runCommand(command, args, options) {
39
57
  }
40
58
  settled = true;
41
59
  clearTimeout(timer);
42
- resolve({ exitCode: 1, stdout, stderr: `${stderr}\n${error.message}`.trim() });
60
+ resolve({ exitCode: 1, stdout, stderr: `${stderr}\n${formatCommandSpawnError(error)}`.trim() });
43
61
  });
44
62
  child.once('close', (code) => {
45
63
  if (settled) {
@@ -62,6 +80,169 @@ function normalizeCloudPreflightRequestValue(raw) {
62
80
  const trimmed = String(raw ?? '').trim();
63
81
  return trimmed.length > 0 ? trimmed : null;
64
82
  }
83
+ function readCloudPreflightCommandOutput(result) {
84
+ const output = [result.stderr, result.stdout]
85
+ .map((value) => value.trim())
86
+ .filter((value) => value.length > 0)
87
+ .join(' ')
88
+ .replace(/\s+/gu, ' ')
89
+ .trim();
90
+ return output || 'no output';
91
+ }
92
+ function readCloudPreflightErrorOutput(result) {
93
+ const output = result.stderr.trim().replace(/\s+/gu, ' ');
94
+ return output || 'no output';
95
+ }
96
+ function compactCloudPreflightOutput(output) {
97
+ return output.length > 500 ? `${output.slice(0, 497)}...` : output;
98
+ }
99
+ function redactCloudPreflightOutput(output) {
100
+ return output
101
+ .replace(/\b(?:authorization|bearer)\s*[:=]\s*bearer\s+[^\s,;]+/giu, 'authorization: Bearer <redacted>')
102
+ .replace(/\bbearer\s+[A-Za-z0-9._~+/-]{10,}/giu, 'Bearer <redacted>')
103
+ .replace(/\b(?:sk|sess|eyJ)[A-Za-z0-9._~+/-]{12,}/gu, '<redacted-token>')
104
+ .replace(/\b(?:CODEX|OPENAI|CHATGPT|GITHUB|GH)_[A-Z0-9_]*(?:API_KEY|AUTH_TOKEN|ACCESS_TOKEN|REFRESH_TOKEN|TOKEN|SECRET|PASSWORD)\s*=\s*[^\s,;]+/gu, (match) => `${match.split('=')[0] ?? 'secret'}=<redacted>`)
105
+ .replace(/\b(?:api[_ -]?key|auth[_ -]?token|access[_ -]?token|refresh[_ -]?token|bearer[_ -]?token|password|secret|credential)\s*[:=]\s*[^\s,;]+/giu, (match) => `${match.split(/[:=]/u)[0]?.trim() ?? 'secret'}=<redacted>`)
106
+ .replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/giu, '<redacted-email>');
107
+ }
108
+ function compactCloudPreflightCommandOutput(result) {
109
+ return compactCloudPreflightOutput(redactCloudPreflightOutput(readCloudPreflightCommandOutput(result)));
110
+ }
111
+ function escapeRegExpLiteral(value) {
112
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
113
+ }
114
+ function isEnvironmentNotFoundSignal(signal, environmentId) {
115
+ const normalized = signal.toLowerCase();
116
+ const normalizedEnvironmentId = environmentId.toLowerCase();
117
+ if (normalized.includes('environment_not_found')) {
118
+ return true;
119
+ }
120
+ const escapedEnvironmentId = escapeRegExpLiteral(normalizedEnvironmentId);
121
+ return new RegExp(`\\benvironment\\s+(?:(["'])${escapedEnvironmentId}\\1|${escapedEnvironmentId})\\s+not\\s+found\\b`, 'u').test(normalized);
122
+ }
123
+ function maskCloudPreflightEnvironmentIdentifierValues(signal) {
124
+ return signal
125
+ .toLowerCase()
126
+ .replace(/\benvironment\s+(?:['"][^'"]+['"]|[^\s'"]+)\s+not\s+found\b/gu, 'environment <env-id> not found')
127
+ .replace(/\bcodex_cloud_env_id\s+(?:['"][^'"]+['"]|[^\s'"]+)/gu, 'codex_cloud_env_id <env-id>')
128
+ .replace(/\bcodex cloud env id\s+(?:['"][^'"]+['"]|[^\s'"]+)/gu, 'codex cloud env id <env-id>');
129
+ }
130
+ function hasWrappedEnvironmentProbeUnavailableSignal(signal) {
131
+ const normalized = maskCloudPreflightEnvironmentIdentifierValues(signal);
132
+ return (/\b(?:missing[_ -]github[_ -]connector[_ -]link|github connection not found|github connector not found)\b/u.test(normalized) ||
133
+ /\b(?:cloud denial|cloud-denial|cloud_denial|cloud denied|not allowed in cloud|cloud access denied|cloud execution denied)\b/u.test(normalized) ||
134
+ /\b(?:forbidden|unauthorized|not logged in|login required|active account|active profile|account mismatch|profile mismatch|invalid token|expired token|token expired|missing token|token missing|api key|auth token|access token|refresh token|bearer token)\b/u.test(normalized) ||
135
+ /\b(?:rate limit|rate-limit|rate_limited|rate_limit_exceeded|quota|too many requests|usage limit|usage_limit_reached)\b/u.test(normalized) ||
136
+ /\b(?:enotfound|econn|network|timed out|timeout|502|503|504|bad gateway|service unavailable|gateway timeout)\b/u.test(normalized));
137
+ }
138
+ function buildEnvironmentProbeIssue(environmentId, result) {
139
+ const fullDetail = readCloudPreflightCommandOutput(result);
140
+ const detail = compactCloudPreflightCommandOutput(result);
141
+ if (isEnvironmentNotFoundSignal(fullDetail, environmentId) &&
142
+ !hasWrappedEnvironmentProbeUnavailableSignal(fullDetail)) {
143
+ return {
144
+ code: 'environment_not_found',
145
+ message: `Configured CODEX_CLOUD_ENV_ID '${environmentId}' is not visible to codex cloud before codex cloud exec: ${detail}`
146
+ };
147
+ }
148
+ return {
149
+ code: 'environment_unavailable',
150
+ message: `Configured CODEX_CLOUD_ENV_ID '${environmentId}' could not be verified by codex cloud before codex cloud exec: ${detail}`
151
+ };
152
+ }
153
+ function tryParseCloudListJson(stdout) {
154
+ const trimmed = stdout.trim();
155
+ if (!trimmed) {
156
+ return null;
157
+ }
158
+ try {
159
+ return JSON.parse(trimmed);
160
+ }
161
+ catch {
162
+ return null;
163
+ }
164
+ }
165
+ function readCloudListTasks(payload) {
166
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
167
+ return null;
168
+ }
169
+ const record = payload;
170
+ return Array.isArray(record.tasks) ? record.tasks : null;
171
+ }
172
+ function readCloudListTaskEnvironmentIdentities(task) {
173
+ if (!task || typeof task !== 'object') {
174
+ return [];
175
+ }
176
+ const record = task;
177
+ const identities = [];
178
+ for (const key of ['environment_id', 'environmentId', 'cloud_environment_id', 'cloudEnvId']) {
179
+ const value = normalizeCloudPreflightRequestValue(record[key]);
180
+ if (value) {
181
+ identities.push(value);
182
+ }
183
+ }
184
+ for (const key of ['environment_label', 'environmentLabel']) {
185
+ const value = normalizeCloudPreflightRequestValue(record[key]);
186
+ if (value) {
187
+ identities.push(value);
188
+ }
189
+ }
190
+ const environment = record.environment;
191
+ if (environment && typeof environment === 'object') {
192
+ const environmentRecord = environment;
193
+ for (const key of ['id', 'environment_id', 'environmentId', 'cloud_environment_id', 'cloudEnvId']) {
194
+ const value = normalizeCloudPreflightRequestValue(environmentRecord[key]);
195
+ if (value) {
196
+ identities.push(value);
197
+ }
198
+ }
199
+ for (const key of ['label', 'environment_label', 'environmentLabel']) {
200
+ const value = normalizeCloudPreflightRequestValue(environmentRecord[key]);
201
+ if (value) {
202
+ identities.push(value);
203
+ }
204
+ }
205
+ }
206
+ return [...new Set(identities)];
207
+ }
208
+ function normalizeCloudEnvironmentIdentity(value) {
209
+ return value.trim().toLowerCase();
210
+ }
211
+ function buildEnvironmentProbePayloadIssue(environmentId, detail) {
212
+ const safeDetail = compactCloudPreflightOutput(redactCloudPreflightOutput(detail));
213
+ return {
214
+ code: 'environment_unavailable',
215
+ message: `Configured CODEX_CLOUD_ENV_ID '${environmentId}' could not be verified by codex cloud before codex cloud exec: ${safeDetail}`
216
+ };
217
+ }
218
+ function inspectSuccessfulEnvironmentProbe(environmentId, result) {
219
+ const stderrDetail = readCloudPreflightErrorOutput(result);
220
+ if (isEnvironmentNotFoundSignal(stderrDetail, environmentId)) {
221
+ return buildEnvironmentProbeIssue(environmentId, {
222
+ ...result,
223
+ stdout: ''
224
+ });
225
+ }
226
+ const payload = tryParseCloudListJson(result.stdout);
227
+ const tasks = readCloudListTasks(payload);
228
+ if (!tasks) {
229
+ return buildEnvironmentProbePayloadIssue(environmentId, `unexpected codex cloud list JSON payload: ${compactCloudPreflightOutput(result.stdout.trim() || 'no output')}`);
230
+ }
231
+ const taskEnvironmentIdentities = tasks.map((task) => readCloudListTaskEnvironmentIdentities(task));
232
+ const normalizedEnvironmentId = normalizeCloudEnvironmentIdentity(environmentId);
233
+ if (taskEnvironmentIdentities.some((identities) => identities.length === 0)) {
234
+ return buildEnvironmentProbePayloadIssue(environmentId, 'codex cloud list returned task rows without an environment identity');
235
+ }
236
+ const mismatchedEnvironmentIds = taskEnvironmentIdentities
237
+ .filter((identities) => !identities.some((identity) => normalizeCloudEnvironmentIdentity(identity) === normalizedEnvironmentId))
238
+ .flat();
239
+ if (mismatchedEnvironmentIds.length > 0) {
240
+ return buildEnvironmentProbePayloadIssue(environmentId, `codex cloud list returned task rows for a different environment identity: ${[
241
+ ...new Set(mismatchedEnvironmentIds)
242
+ ].join(', ')}`);
243
+ }
244
+ return null;
245
+ }
65
246
  function readFirstCloudPreflightEnvValue(env, keys) {
66
247
  if (!env) {
67
248
  return null;
@@ -151,6 +332,21 @@ export async function runCloudPreflight(params) {
151
332
  message: `Codex CLI is unavailable (${params.codexBin} --version failed).`
152
333
  });
153
334
  }
335
+ if (params.environmentId && codexCheck.exitCode === 0) {
336
+ const environmentCheck = await runCommand(params.codexBin, ['cloud', 'list', '--env', params.environmentId, '--limit', '1', '--json'], {
337
+ cwd: params.repoRoot,
338
+ env: params.env
339
+ });
340
+ if (environmentCheck.exitCode !== 0) {
341
+ issues.push(buildEnvironmentProbeIssue(params.environmentId, environmentCheck));
342
+ }
343
+ else {
344
+ const environmentProbeIssue = inspectSuccessfulEnvironmentProbe(params.environmentId, environmentCheck);
345
+ if (environmentProbeIssue) {
346
+ issues.push(environmentProbeIssue);
347
+ }
348
+ }
349
+ }
154
350
  if (branch) {
155
351
  const gitCheck = await runCommand('git', ['--version'], { cwd: params.repoRoot, env: params.env });
156
352
  if (gitCheck.exitCode !== 0) {
@@ -0,0 +1,60 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ export function readCodexFeatureProbe(codexBin, env = process.env) {
3
+ const result = spawnSync(codexBin, ['features', 'list'], {
4
+ encoding: 'utf8',
5
+ env,
6
+ stdio: ['ignore', 'pipe', 'pipe'],
7
+ timeout: 5000
8
+ });
9
+ const stderr = String(result.stderr ?? '');
10
+ if (result.error || result.status !== 0) {
11
+ return {
12
+ flags: null,
13
+ stderr,
14
+ error: result.error
15
+ ? result.error.message
16
+ : `codex features list exited with status ${result.status ?? '<unknown>'}`,
17
+ status: result.status
18
+ };
19
+ }
20
+ const stdout = String(result.stdout ?? '');
21
+ return {
22
+ flags: parseFeatureFlagsFromText(stdout),
23
+ stderr,
24
+ error: null,
25
+ status: result.status
26
+ };
27
+ }
28
+ export function codexFeatureProbeRejectsAgentMaxThreads(probe) {
29
+ const text = `${probe.error ?? ''}\n${probe.stderr}`.toLowerCase();
30
+ const mentionsMaxThreads = /\bagents\.max_threads\b/u.test(text) || /\bmax[_\s-]?threads\b/u.test(text);
31
+ return mentionsMaxThreads && /\bmulti_agent_v2\b/u.test(text);
32
+ }
33
+ export function codexFeatureProbeDisablesMultiAgentV2(probe) {
34
+ return probe.flags?.multi_agent_v2 === false;
35
+ }
36
+ function parseFeatureFlagsFromText(stdout) {
37
+ const flags = {};
38
+ for (const line of stdout.split(/\r?\n/u)) {
39
+ const trimmed = line.trim();
40
+ if (!trimmed) {
41
+ continue;
42
+ }
43
+ const tokens = trimmed.split(/\s+/u);
44
+ if (tokens.length < 2) {
45
+ continue;
46
+ }
47
+ const name = tokens[0] ?? '';
48
+ const enabledToken = tokens[tokens.length - 1] ?? '';
49
+ if (!name) {
50
+ continue;
51
+ }
52
+ if (enabledToken === 'true') {
53
+ flags[name] = true;
54
+ }
55
+ else if (enabledToken === 'false') {
56
+ flags[name] = false;
57
+ }
58
+ }
59
+ return flags;
60
+ }
@@ -160,7 +160,7 @@ export class TaskManager {
160
160
  const build = await this.executeBuilder(task, plan, target, mode, runId);
161
161
  if (!build.success) {
162
162
  const skippedTest = this.createSkippedTestResult(build, runId);
163
- const skippedReview = this.createSkippedReviewResult('build-failed');
163
+ const skippedReview = this.createSkippedReviewResult('build-failed', build);
164
164
  const summary = this.createRunSummary(task, mode, plan, build, skippedTest, skippedReview, runId);
165
165
  return { target, mode, build, test: skippedTest, review: skippedReview, summary };
166
166
  }
@@ -239,13 +239,27 @@ export class TaskManager {
239
239
  runId
240
240
  };
241
241
  }
242
- createSkippedReviewResult(reason) {
242
+ createSkippedReviewResult(reason, build) {
243
243
  if (reason === 'build-failed') {
244
+ const prerequisiteStage = this.extractFailedPrerequisiteStage(build);
245
+ if (prerequisiteStage) {
246
+ const artifactPath = this.findFailedStageArtifactPath(build);
247
+ const artifactFeedback = artifactPath ? ` Error artifact: ${artifactPath}.` : '';
248
+ return {
249
+ summary: `Review skipped: prerequisite stage \`${prerequisiteStage}\` failed.`,
250
+ decision: {
251
+ approved: false,
252
+ feedback: `Prerequisite stage \`${prerequisiteStage}\` failed; review skipped.${artifactFeedback}`
253
+ }
254
+ };
255
+ }
256
+ const artifactPath = this.findFailedStageArtifactPath(build);
257
+ const artifactFeedback = artifactPath ? ` Error artifact: ${artifactPath}.` : '';
244
258
  return {
245
259
  summary: 'Review skipped: build stage failed.',
246
260
  decision: {
247
261
  approved: false,
248
- feedback: 'Build stage failed; review skipped.'
262
+ feedback: `Build stage failed; review skipped.${artifactFeedback}`
249
263
  }
250
264
  };
251
265
  }
@@ -257,6 +271,62 @@ export class TaskManager {
257
271
  }
258
272
  };
259
273
  }
274
+ extractFailedPrerequisiteStage(build) {
275
+ const stage = build?.failureStage?.trim();
276
+ const canonicalStage = stage?.toLowerCase();
277
+ if (!stage || (canonicalStage != null && ['build', 'test', 'review'].includes(canonicalStage))) {
278
+ return null;
279
+ }
280
+ return stage;
281
+ }
282
+ findFailedStageArtifactPath(build) {
283
+ if (build?.failureArtifactPath) {
284
+ return build.failureArtifactPath;
285
+ }
286
+ const failureStage = build?.failureStage?.trim().toLowerCase();
287
+ if (!failureStage) {
288
+ return null;
289
+ }
290
+ if (['build', 'test', 'review'].includes(failureStage)) {
291
+ return null;
292
+ }
293
+ const stagePathToken = failureStage.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
294
+ const stageArtifact = build?.artifacts.find((candidate) => {
295
+ const normalizedPath = candidate.path.replace(/\\/g, '/').toLowerCase();
296
+ const isErrorArtifact = normalizedPath.startsWith('errors/') ||
297
+ normalizedPath.includes('/errors/') ||
298
+ /\berror\b/i.test(candidate.description);
299
+ if (!isErrorArtifact) {
300
+ return false;
301
+ }
302
+ return this.artifactMatchesFailedStage(candidate, failureStage, stagePathToken);
303
+ });
304
+ return stageArtifact?.path ?? null;
305
+ }
306
+ artifactMatchesFailedStage(candidate, failureStage, stagePathToken) {
307
+ const normalizedDescription = candidate.description.toLowerCase();
308
+ const parentheticalStages = [...normalizedDescription.matchAll(/\(([^)]+)\)/g)]
309
+ .map((match) => match[1]?.trim() ?? '')
310
+ .filter(Boolean);
311
+ if (parentheticalStages.some((stage) => this.stageTokenMatches(stage, failureStage, stagePathToken))) {
312
+ return true;
313
+ }
314
+ const normalizedPath = candidate.path.replace(/\\/g, '/').toLowerCase();
315
+ return normalizedPath
316
+ .split('/')
317
+ .map((segment) => segment.replace(/\.[^.]+$/, '').replace(/^\d+-/, ''))
318
+ .some((segment) => this.stageTokenMatches(segment, failureStage, stagePathToken));
319
+ }
320
+ stageTokenMatches(candidate, failureStage, stagePathToken) {
321
+ if (candidate === failureStage) {
322
+ return true;
323
+ }
324
+ if (!stagePathToken) {
325
+ return false;
326
+ }
327
+ const candidatePathToken = candidate.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
328
+ return candidatePathToken === stagePathToken;
329
+ }
260
330
  async executePlanner(task, runId) {
261
331
  try {
262
332
  const plan = await this.options.planner.plan(task);
@@ -335,7 +405,7 @@ export class TaskManager {
335
405
  const cause = error ?? new Error('Planner stage failed without an error payload');
336
406
  const build = this.createBuilderErrorResult('planner-unavailable', mode, runId, cause);
337
407
  const skippedTest = this.createSkippedTestResult(build, runId);
338
- const skippedReview = this.createSkippedReviewResult('build-failed');
408
+ const skippedReview = this.createSkippedReviewResult('build-failed', build);
339
409
  const summary = this.createRunSummary(task, mode, plan, build, skippedTest, skippedReview, runId);
340
410
  this.eventBus.emit({ type: 'run:completed', payload: summary });
341
411
  return summary;
@@ -266,6 +266,7 @@ export async function readCurrentCodexPosture(repoRoot, policy = {}) {
266
266
  return {
267
267
  source_path: '',
268
268
  cli_version: null,
269
+ cli_compatibility_versions: [],
269
270
  model: null,
270
271
  default_runtime: null,
271
272
  explorer_fast_model: null,
@@ -278,15 +279,48 @@ export async function readCurrentCodexPosture(repoRoot, policy = {}) {
278
279
  /delegated subagent and review surfaces on [^;\n]*; `([^`]+)` is currently unsupported there/i.exec(content)?.[1] ??
279
280
  /delegated(?: subagent)?(?: and|\/) review surfaces on [^\n]* validates `([^`]+)`/i.exec(content)?.[1] ??
280
281
  null;
282
+ const cliVersion = /Codex CLI\s+\(?`?([0-9]+\.[0-9]+\.[0-9]+)`?\)?/.exec(content)?.[1] ?? null;
281
283
  return {
282
284
  source_path: sourcePath,
283
- cli_version: /Codex CLI\s+\(?`?([0-9]+\.[0-9]+\.[0-9]+)`?\)?/.exec(content)?.[1] ?? null,
285
+ cli_version: cliVersion,
286
+ cli_compatibility_versions: extractAllowedCliCompatibilityVersions(content, cliVersion),
284
287
  model: /Current model posture(?: is|:)\s*`([^`]+)`/i.exec(content)?.[1] ?? null,
285
288
  default_runtime: /Local ([A-Za-z0-9_-]+) remains the expected default runtime path/.exec(content)?.[1] ?? null,
286
289
  explorer_fast_model: /explorer_fast[^\n]*`(gpt-[^`]+)`/i.exec(content)?.[1] ?? null,
287
290
  unsupported_review_model: unsupportedReviewModel
288
291
  };
289
292
  }
293
+ function extractAllowedCliCompatibilityVersions(content, currentCliVersion) {
294
+ const versions = new Set();
295
+ const scanContent = extractCurrentPostureContent(content);
296
+ for (const line of scanContent.split(/\r?\n/)) {
297
+ if (!/compatibility/i.test(line) ||
298
+ !/(separately\s+rebaselined|downstream-smoke|release-facing)/i.test(line)) {
299
+ continue;
300
+ }
301
+ for (const version of extractCodexCliVersionMentions(line)) {
302
+ if (version !== currentCliVersion) {
303
+ versions.add(version);
304
+ }
305
+ }
306
+ }
307
+ return [...versions].sort();
308
+ }
309
+ function extractCurrentPostureContent(content) {
310
+ const lines = content.split(/\r?\n/);
311
+ const startIndex = lines.findIndex((line) => /^##\s+Current Posture\b/i.test(line.trim()));
312
+ if (startIndex === -1) {
313
+ return '';
314
+ }
315
+ const sectionLines = [];
316
+ for (const line of lines.slice(startIndex + 1)) {
317
+ if (/^##\s+/.test(line.trim())) {
318
+ break;
319
+ }
320
+ sectionLines.push(line);
321
+ }
322
+ return sectionLines.join('\n');
323
+ }
290
324
  export function extractCodexCliVersionMentions(content) {
291
325
  const results = new Set();
292
326
  const patterns = [