@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
package/README.md CHANGED
@@ -1,121 +1,81 @@
1
1
  # Codex Orchestrator
2
2
 
3
- Codex Orchestrator is a CLI and runtime for Codex-driven pipelines, auditable manifests, delegation MCP workflows, and downstream repo bootstrapping.
3
+ Codex Orchestrator (CO) is a CLI and runtime for Codex-driven pipelines, auditable manifests, delegation workflows, and downstream repo bootstrapping.
4
4
 
5
- ## Release posture
5
+ ## Release Posture
6
6
 
7
- This README tracks the current `main` branch. When `main` moves ahead of the latest published package, published-package users should follow the README and docs that match the tag or release they actually installed instead of assuming every source-head guide below is already shipped.
7
+ This README tracks the current `main` branch. Published-package users should follow the README and docs for the tag or release they installed.
8
8
 
9
- - Release-safe docs for the current published package: [latest GitHub release](https://github.com/Kbediako/CO/releases/latest)
10
- - If you are pinned to the older `v0.1.38` package, use the matching [README for `v0.1.38`](https://github.com/Kbediako/CO/blob/v0.1.38/README.md)
11
- - Source-head guidance in this checkout includes the marketplace/plugin flow below plus `docs/public/downstream-setup.md` and `docs/public/provider-onboarding.md`
9
+ - Latest package docs: [GitHub releases](https://github.com/Kbediako/CO/releases/latest)
10
+ - Older `v0.2.0` package docs: [README for `v0.2.0`](https://github.com/Kbediako/CO/blob/v0.2.0/README.md)
11
+ - Detailed source-head docs: [docs/book/README.md](docs/book/README.md)
12
12
 
13
13
  ## Install
14
14
 
15
- npm remains the supported baseline because it is the simplest way to install the CO CLI.
16
-
17
15
  ```bash
18
16
  npm i -g @kbediako/codex-orchestrator
19
17
  codex-orchestrator --version
20
18
  ```
21
19
 
22
- Node.js `>=20` is required.
20
+ Node.js `>=20` is required. npm remains the supported baseline install path.
23
21
 
24
- CO currently targets Codex CLI `0.123.0`; newer candidates stay evidence-gated in `docs/guides/codex-version-policy.md`.
25
- The source-head marketplace/plugin guidance keeps the CO-196 packaging boundary: npm remains the release-safe baseline, while Codex plugin marketplace registration is an additive path for newer Codex CLI command surfaces.
22
+ ## Current Posture
26
23
 
27
- ### Source-head marketplace/plugin setup
24
+ - Current CO-local Codex CLI `0.125.0` ChatGPT-auth/appserver posture
25
+ - Current model posture: `gpt-5.5` / `xhigh` when available in ChatGPT-auth Codex sessions
26
+ - Portable packaged/generated defaults keep `gpt-5.4` / `xhigh` as fallback values when `gpt-5.5`, API, or cloud portability is unavailable
27
+ - Local default runtime: `appserver`
28
+ - Unsupported combination: `executionMode=cloud` with explicit `runtimeMode=appserver`
28
29
 
29
- The marketplace/plugin flow below reflects the current source tree. Published-package users, especially anyone pinned to older tags such as `v0.1.38`, should keep following the matching tagged README or release docs instead of assuming the latest source-head marketplace surfaces are already shipped. Only use the marketplace/plugin steps below when you are on `main` or on a tag or release that already includes `plugins/codex-orchestrator` and the related public docs.
30
+ The full version and model policy lives in [docs/guides/codex-version-policy.md](docs/guides/codex-version-policy.md).
30
31
 
31
- For newer Codex releases that expose `codex plugin marketplace`, CO also ships a repo marketplace entry plus plugin manifests under `plugins/codex-orchestrator`. You can add the packaged or repo-root marketplace source and install the plugin from Codex:
32
+ ## Quickstart
32
33
 
33
34
  ```bash
34
- codex plugin marketplace add "$(npm root -g)/@kbediako/codex-orchestrator"
35
+ codex-orchestrator init codex --cwd /path/to/repo
36
+ cd /path/to/repo
37
+ codex-orchestrator setup --yes --repo /path/to/repo
38
+ codex login
39
+ codex-orchestrator flow --task <task-id>
40
+ codex-orchestrator doctor --format json
35
41
  ```
36
42
 
37
- For a local checkout, add the repository root instead of the npm install directory. For a Git-backed install, pass a Git identifier or URL such as `owner/repo[@ref]`, an HTTPS Git URL, or an SSH Git URL. Those source-driven installs can include unreleased changes; pinning an older tag such as `v0.1.38` keeps you on the pre-marketplace behavior, so use the steps below only on `main` or on a newer ref that already contains the plugin manifests and public docs mentioned here. The marketplace entry points at the packaged `plugins/codex-orchestrator` directory, and the installed plugin uses a small `node` launcher to resolve the marketplace runtime root from `${CODEX_HOME:-~/.codex}/config.toml`: local-directory sources run from the recorded source path, while Git-backed sources run from Codex's installed checkout under `${CODEX_HOME:-~/.codex}/.tmp/marketplaces/codex-orchestrator`. That keeps the MCP registration path independent of a second `codex-orchestrator` PATH entry. If you move or replace a local-directory source, or remove Codex's installed marketplace checkout, re-run `codex plugin marketplace add ...` before using the plugin again. Then open `/plugins` in Codex, install `Codex Orchestrator`, and restart Codex if it does not pick up the plugin immediately. Use the plugin browser's uninstall action to remove the plugin, `codex plugin marketplace remove codex-orchestrator` to remove the marketplace registration, or set the plugin entry in `${CODEX_HOME:-~/.codex}/config.toml` to `enabled = false` to turn it off without uninstalling.
38
-
39
- ## 2-minute quickstart (current `main`)
43
+ Use `codex login --device-auth` when browser login is not available.
40
44
 
41
- 1. Install the downstream repo templates:
42
- ```bash
43
- codex-orchestrator init codex --cwd /path/to/repo
44
- ```
45
- 2. Configure bundled skills plus delegation and DevTools wiring once per machine:
46
- ```bash
47
- codex-orchestrator setup --yes
48
- ```
49
- 3. Log in to Codex. If browser login is not available, use device auth:
50
- ```bash
51
- codex login
52
- # Fallback
53
- codex login --device-auth
54
- ```
55
- 4. Run the default docs-first flow inside your repo:
56
- ```bash
57
- codex-orchestrator flow --task <task-id>
58
- ```
59
- 5. Check local readiness:
60
- ```bash
61
- codex-orchestrator doctor --format json
62
- ```
45
+ ## Plugin Install
63
46
 
64
- ## Downstream setup
47
+ The npm CLI install is the baseline. Codex plugin marketplace setup is additive for Codex releases that expose plugin flows. Current Codex CLI `0.125.0` keeps marketplace management under `codex plugin marketplace ...`:
65
48
 
66
- These guides are current source-head docs in this checkout. Readers pinned to older releases such as `v0.1.38` should treat them as source-head-only unless they are reading a matching newer tag or source ref that already includes these files.
49
+ ```bash
50
+ # Codex 0.121.0 accepts either command.
51
+ codex marketplace add "$(npm root -g)/@kbediako/codex-orchestrator"
67
52
 
68
- - [docs/public/downstream-setup.md](docs/public/downstream-setup.md): install, repo bootstrap, machine setup, and first-run flow
69
- - [docs/public/provider-onboarding.md](docs/public/provider-onboarding.md): Linear and Telegram onboarding, env vars, policy examples, readiness, and smoke flow
53
+ # Codex 0.122.0+ uses the plugin command.
54
+ codex plugin marketplace add "$(npm root -g)/@kbediako/codex-orchestrator"
55
+ ```
70
56
 
71
- `init codex` also seeds provider examples under `.codex/providers/` so fresh repos do not need to hand-author the first env and policy files from scratch.
57
+ For local checkout installs, pass the repository root instead of the npm install directory. For Git-backed installs, pass `owner/repo[@ref]`, an HTTPS Git URL, or an SSH Git URL. Use `codex plugin marketplace upgrade codex-orchestrator` to refresh a Git-backed marketplace checkout and `codex plugin marketplace remove codex-orchestrator` to remove the marketplace registration. Then open `/plugins` in Codex, install `Codex Orchestrator`, and restart Codex if the plugin is not picked up immediately. More local checkout, Git-backed, and rollback details are in [docs/book/setup.md](docs/book/setup.md).
72
58
 
73
- ## Common commands
59
+ ## Common Commands
74
60
 
75
61
  ```bash
76
62
  codex-orchestrator flow --task <task-id>
77
- codex-orchestrator review --task <task-id>
78
- codex-orchestrator doctor --usage --window-days 30
79
63
  codex-orchestrator start diagnostics --task <task-id> --format json
80
- codex-orchestrator co-status
81
- codex-orchestrator control-host supervise status --format json
64
+ codex-orchestrator status --run <run-id> --watch --interval 10
65
+ codex-orchestrator review
66
+ codex-orchestrator linear issue-context --issue-id <linear-uuid>
82
67
  ```
83
68
 
84
- ## Skills (bundled)
85
-
86
- Install bundled skills into `$CODEX_HOME/skills`:
69
+ Run artifacts live under `.runs/<task-id>/` and summaries under `out/<task-id>/`.
87
70
 
88
- ```bash
89
- codex-orchestrator skills install
90
- ```
71
+ ## Downstream Setup
91
72
 
92
- Bundled skills:
93
-
94
- - `agent-first-adoption-steering`
95
- - `chrome-devtools`
96
- - `codex-orchestrator`
97
- - `collab-deliberation`
98
- - `collab-evals`
99
- - `collab-subagents-first`
100
- - `delegate-early`
101
- - `delegation-usage`
102
- - `docs-first`
103
- - `elegance-review`
104
- - `land`
105
- - `linear`
106
- - `long-poll-wait`
107
- - `release`
108
- - `standalone-review`
109
-
110
- ## Public posture
111
-
112
- - Current Codex CLI target: `0.123.0`
113
- - Current model posture: `gpt-5.4`
114
- - `explorer_fast` remains the explicit `gpt-5.3-codex-spark` file/codebase search-only exception
115
- - Local default runtime: `appserver`
116
- - `executionMode=cloud` with explicit `runtimeMode=appserver` remains unsupported
73
+ - [Book index](docs/book/README.md): setup, operations, skills, public posture, and CO-345 evidence notes
74
+ - [Bundled skills](skills/README.md): shipped skill roster and install behavior
75
+ - [Downstream setup](docs/public/downstream-setup.md): install, repo bootstrap, machine setup, and first run
76
+ - [Provider onboarding](docs/public/provider-onboarding.md): Linear and provider-worker setup
77
+ - [Docs index](docs/README.md): repo-local documentation map
117
78
 
118
79
  ## Contributing
119
80
 
120
- Contributor and repo-internal guidance lives in the source repository:
121
- [docs/README.md](https://github.com/Kbediako/CO/blob/main/docs/README.md).
81
+ Contributor and repo-internal guidance lives in [docs/README.md](docs/README.md).
@@ -1331,6 +1331,7 @@ Commands:
1331
1331
  codex defaults
1332
1332
  --yes Apply setup (otherwise dry-run plan only).
1333
1333
  --force Allow overwriting existing role files in ~/.codex/agents.
1334
+ --auth-scope <portable|chatgpt> Select portable defaults or validated ChatGPT-auth gpt-5.5 defaults.
1334
1335
  --format json Emit machine-readable output.
1335
1336
  devtools setup Print DevTools MCP setup instructions.
1336
1337
  --yes Apply setup by running "codex mcp add ...".
@@ -1558,6 +1559,7 @@ Subcommands:
1558
1559
  defaults Plan/apply additive global Codex defaults in ~/.codex/config.toml.
1559
1560
  --yes Apply setup (otherwise dry-run plan only).
1560
1561
  --force Overwrite existing role files in ~/.codex/agents.
1562
+ --auth-scope <portable|chatgpt> Select portable defaults or validated ChatGPT-auth gpt-5.5 defaults.
1561
1563
  --format json Emit machine-readable output.
1562
1564
  `);
1563
1565
  }
@@ -5,11 +5,13 @@ export class CommandBuilder {
5
5
  }
6
6
  async build(input) {
7
7
  const result = await this.executePipeline(input);
8
+ const failure = resolveManifestFailure(result.manifest);
8
9
  return {
9
10
  subtaskId: input.target.id,
10
11
  artifacts: [
11
12
  { path: result.manifestPath, description: 'CLI run manifest' },
12
13
  { path: result.logPath, description: 'Runner log (ndjson)' },
14
+ ...collectCommandErrorArtifacts(result.manifest.commands),
13
15
  ...(result.manifest.cloud_execution?.diff_path
14
16
  ? [{ path: result.manifest.cloud_execution.diff_path, description: 'Cloud diff artifact' }]
15
17
  : [])
@@ -18,6 +20,8 @@ export class CommandBuilder {
18
20
  runId: input.runId,
19
21
  success: result.success,
20
22
  notes: result.notes.join('\n') || undefined,
23
+ failureStage: failure.stage,
24
+ failureArtifactPath: failure.artifactPath,
21
25
  cloudExecution: result.manifest.cloud_execution
22
26
  ? {
23
27
  taskId: result.manifest.cloud_execution.task_id,
@@ -42,3 +46,49 @@ export class CommandBuilder {
42
46
  };
43
47
  }
44
48
  }
49
+ function resolveManifestFailure(manifest) {
50
+ const statusDetailStage = extractStageFromStatusDetail(manifest.status_detail);
51
+ const cloudStage = extractCloudStageFromStatusDetail(manifest.status_detail);
52
+ const fallbackFailedCommand = statusDetailStage || hasStatusDetail(manifest.status_detail) ? null : firstFailedCommand(manifest.commands);
53
+ const cloudFailedStage = cloudStage && hasFailedCommand(manifest.commands, cloudStage) ? cloudStage : null;
54
+ const stage = statusDetailStage ?? cloudFailedStage ?? fallbackFailedCommand?.id ?? null;
55
+ const artifactPath = stage ? findFailedCommandErrorArtifact(manifest.commands, stage) : null;
56
+ return { stage, artifactPath };
57
+ }
58
+ function hasStatusDetail(statusDetail) {
59
+ return statusDetail != null && statusDetail.trim().length > 0;
60
+ }
61
+ function extractStageFromStatusDetail(statusDetail) {
62
+ const match = /^(?:stage|subpipeline):(.+):(?:failed|error)$/.exec(statusDetail ?? '');
63
+ return match?.[1] ?? null;
64
+ }
65
+ function extractCloudStageFromStatusDetail(statusDetail) {
66
+ const match = /^cloud:(.+):(?:failed|error)$/.exec(statusDetail ?? '');
67
+ return match?.[1] ?? null;
68
+ }
69
+ function firstFailedCommand(commands) {
70
+ return commands.find((command) => command.status === 'failed') ?? null;
71
+ }
72
+ function hasFailedCommand(commands, stage) {
73
+ return commands.some((command) => command.status === 'failed' && command.id === stage);
74
+ }
75
+ function findFailedCommandErrorArtifact(commands, stage) {
76
+ const matching = commands.find((command) => command.status === 'failed' && command.id === stage && command.error_file);
77
+ return matching?.error_file ?? null;
78
+ }
79
+ function collectCommandErrorArtifacts(commands) {
80
+ const seen = new Set();
81
+ const artifacts = [];
82
+ for (const command of commands) {
83
+ const errorFile = command.error_file;
84
+ if (!errorFile || seen.has(errorFile)) {
85
+ continue;
86
+ }
87
+ seen.add(errorFile);
88
+ artifacts.push({
89
+ path: errorFile,
90
+ description: `Command error artifact (${command.id})`
91
+ });
92
+ }
93
+ return artifacts;
94
+ }
@@ -60,6 +60,18 @@ const CLOUD_FAILURE_RULES = [
60
60
  ],
61
61
  guidance: 'Codex Cloud rejected this run; verify the configured cloud environment, branch, and account permission for cloud execution.'
62
62
  },
63
+ {
64
+ category: 'configuration',
65
+ diagnostic_category: 'environment_not_found',
66
+ patterns: ['environment_not_found', 'environment not found', 'is not visible to codex cloud'],
67
+ guidance: 'CODEX_CLOUD_ENV_ID is configured, but Codex Cloud could not resolve it. Fix the env id or choose an environment visible to this account.'
68
+ },
69
+ {
70
+ category: 'configuration',
71
+ diagnostic_category: 'environment_unavailable',
72
+ patterns: ['environment_unavailable', 'could not be verified by codex cloud'],
73
+ guidance: 'CODEX_CLOUD_ENV_ID is configured, but this account cannot use that environment right now. Verify visibility/permissions and retry.'
74
+ },
63
75
  {
64
76
  category: 'configuration',
65
77
  diagnostic_category: 'env_config',
@@ -135,6 +147,8 @@ const MACHINE_READABLE_CLOUD_DETAILS = new Set([
135
147
  'cloud_denial',
136
148
  'cloud_denied',
137
149
  'cloud_env_missing',
150
+ 'environment_not_found',
151
+ 'environment_unavailable',
138
152
  'cloud_execution_denied',
139
153
  'cloud_connector_auth_drift',
140
154
  'codex_cloud_env_id',
@@ -152,14 +166,100 @@ const MACHINE_READABLE_CLOUD_DETAILS = new Set([
152
166
  'rate_limited',
153
167
  'usage_limit_reached'
154
168
  ]);
155
- function matchCloudFailureRule(signal) {
169
+ function isEnvironmentNotFoundSignal(signal) {
156
170
  const lowercase = signal.toLowerCase();
157
- const normalized = normalizeCloudFailureSignal(signal);
158
- return CLOUD_FAILURE_RULES.find((rule) => rule.patterns.some((pattern) => {
171
+ if (/\benvironment_not_found\b/u.test(lowercase)) {
172
+ return true;
173
+ }
174
+ if (!/\benvironment\s+(?:['"][^'"]+['"]|[^\s'"]+)\s+not\s+found\b/u.test(lowercase)) {
175
+ return false;
176
+ }
177
+ return (lowercase.includes('codex_cloud_env_id') ||
178
+ lowercase.includes('codex cloud env id') ||
179
+ lowercase.includes('is not visible to codex cloud') ||
180
+ lowercase.includes('could not be verified by codex cloud'));
181
+ }
182
+ function matchesCloudFailureRule(rule, lowercase, normalized) {
183
+ return rule.patterns.some((pattern) => {
159
184
  const normalizedPattern = normalizeCloudFailureSignal(pattern);
160
185
  return lowercase.includes(pattern) ||
161
186
  (normalizedPattern.length >= 4 && normalized.includes(normalizedPattern));
162
- })) ?? null;
187
+ });
188
+ }
189
+ function findCloudFailureRule(diagnosticCategory) {
190
+ return CLOUD_FAILURE_RULES.find((rule) => rule.diagnostic_category === diagnosticCategory) ?? null;
191
+ }
192
+ function findMatchedCloudFailureRule(diagnosticCategory, signal) {
193
+ const rule = findCloudFailureRule(diagnosticCategory);
194
+ if (!rule) {
195
+ return null;
196
+ }
197
+ const lowercase = signal.toLowerCase();
198
+ const normalized = normalizeCloudFailureSignal(signal);
199
+ return matchesCloudFailureRule(rule, lowercase, normalized) ? rule : null;
200
+ }
201
+ function maskEnvConfigIdentifierValues(normalizedSignal) {
202
+ return normalizedSignal
203
+ .replace(/\bcodex_cloud_env_id\s+(?:['"][^'"]+['"]|[^\s'"]+)/gu, 'codex_cloud_env_id <env-id>')
204
+ .replace(/\bcodex cloud env id\s+(?:['"][^'"]+['"]|[^\s'"]+)/gu, 'codex cloud env id <env-id>')
205
+ .replace(/\benvironment\s+(?:['"][^'"]+['"]|[^\s'"]+)\s+not\s+found\b/gu, 'environment <env-id> not found');
206
+ }
207
+ function hasStrongConnectivityContext(signal) {
208
+ const normalized = maskEnvConfigIdentifierValues(signal.toLowerCase());
209
+ return (normalized.includes('enotfound') ||
210
+ normalized.includes('econn') ||
211
+ normalized.includes('bad gateway') ||
212
+ normalized.includes('service unavailable') ||
213
+ normalized.includes('gateway timeout') ||
214
+ /\bnetwork\W{0,24}(?:error|failure|unreachable|unavailable|timeout|timed out|connection|connectivity)\b/u.test(normalized) ||
215
+ /\b(?:request|connection|endpoint|gateway|upstream|service)\W{0,24}(?:timed out|timeout)\b/u.test(normalized) ||
216
+ /\b(?:timed out|timeout)\W{0,24}(?:(?:while\s+)?(?:contacting|connecting|reaching|calling|waiting)|request|connection|endpoint|gateway|upstream|service|after)\b/u.test(normalized) ||
217
+ /\b(?:http(?:\s+(?:status|response))?|status|response|upstream|gateway|error|failed|returned)\D{0,16}(?:502|503|504)\b/u.test(normalized) ||
218
+ /\b(?:502|503|504)\D{0,16}(?:bad gateway|service unavailable|gateway timeout)\b/u.test(normalized));
219
+ }
220
+ function matchCloudFailureRule(signal) {
221
+ const lowercase = signal.toLowerCase();
222
+ const normalized = normalizeCloudFailureSignal(signal);
223
+ const envConfigRule = findCloudFailureRule('env_config');
224
+ if (isEnvironmentNotFoundSignal(signal)) {
225
+ const specificRule = matchSpecificWrappedFailureRule(signal, lowercase, normalized);
226
+ if (specificRule) {
227
+ return specificRule;
228
+ }
229
+ return findCloudFailureRule('environment_not_found');
230
+ }
231
+ if (envConfigRule && matchesCloudFailureRule(envConfigRule, lowercase, normalized)) {
232
+ const specificRule = matchSpecificWrappedFailureRule(signal, lowercase, normalized);
233
+ if (specificRule) {
234
+ return specificRule;
235
+ }
236
+ const envUnavailableRule = findCloudFailureRule('environment_unavailable');
237
+ if (envUnavailableRule && matchesCloudFailureRule(envUnavailableRule, lowercase, normalized)) {
238
+ return envUnavailableRule;
239
+ }
240
+ }
241
+ return CLOUD_FAILURE_RULES.find((rule) => matchesCloudFailureRule(rule, lowercase, normalized)) ?? null;
242
+ }
243
+ function matchSpecificWrappedFailureRule(signal, lowercase, normalized) {
244
+ const maskedSignal = maskEnvConfigIdentifierValues(lowercase);
245
+ for (const diagnosticCategory of [
246
+ 'cloud_connector_auth_drift',
247
+ 'cloud_denial',
248
+ 'auth_mismatch',
249
+ 'quota_rate_limit'
250
+ ]) {
251
+ const rule = findMatchedCloudFailureRule(diagnosticCategory, maskedSignal);
252
+ if (rule) {
253
+ return rule;
254
+ }
255
+ }
256
+ const connectivityRule = findCloudFailureRule('network_connectivity');
257
+ if (connectivityRule &&
258
+ matchesCloudFailureRule(connectivityRule, lowercase, normalized) &&
259
+ hasStrongConnectivityContext(signal)) {
260
+ return connectivityRule;
261
+ }
262
+ return null;
163
263
  }
164
264
  function normalizeCloudFailureSignal(signal) {
165
265
  return signal
@@ -169,8 +269,11 @@ function normalizeCloudFailureSignal(signal) {
169
269
  .replace(/\s+/gu, ' ')
170
270
  .trim();
171
271
  }
272
+ function normalizeMachineReadableCloudDetail(signal) {
273
+ return normalizeCloudFailureSignal(signal).replace(/\s+/gu, '_');
274
+ }
172
275
  function isMachineReadableCloudDetail(signal) {
173
- return MACHINE_READABLE_CLOUD_DETAILS.has(normalizeCloudFailureSignal(signal).replace(/\s+/gu, '_'));
276
+ return MACHINE_READABLE_CLOUD_DETAILS.has(normalizeMachineReadableCloudDetail(signal));
174
277
  }
175
278
  function buildCloudFailureDiagnosis(rule, signal) {
176
279
  return {
@@ -187,6 +290,15 @@ export function diagnoseCloudFailure(options) {
187
290
  if (options.statusDetail && isMachineReadableCloudDetail(options.statusDetail)) {
188
291
  const statusDetailRule = matchCloudFailureRule(options.statusDetail);
189
292
  if (statusDetailRule) {
293
+ if (normalizeMachineReadableCloudDetail(options.statusDetail) === 'environment_unavailable' &&
294
+ options.error) {
295
+ const errorRule = matchCloudFailureRule(options.error);
296
+ if (errorRule &&
297
+ errorRule.diagnostic_category !== 'environment_unavailable' &&
298
+ errorRule.diagnostic_category !== 'env_config') {
299
+ return buildCloudFailureDiagnosis(errorRule, signal);
300
+ }
301
+ }
190
302
  return buildCloudFailureDiagnosis(statusDetailRule, signal);
191
303
  }
192
304
  }
@@ -275,9 +275,9 @@ function formatAttachRequestFailure(error, target, options = {}) {
275
275
  if (error instanceof CoStatusAttachRequestError) {
276
276
  if (error.kind === 'network') {
277
277
  if (options.endpointAlreadyRotated) {
278
- return `${error.message}. The refreshed control-host endpoint is still unreachable; wait for the new host to come up or rerun co-status attach.`;
278
+ return `current-host-unhealthy: ${error.message}. The refreshed control-host endpoint is still unreachable; wait for the new host to come up or rerun co-status attach.`;
279
279
  }
280
- return `stale endpoint after control-host restart; control-host unavailable; control_endpoint.json has not rotated to a reachable host. ${error.message}. Waiting for ${resolve(target.runDir, 'control_endpoint.json')} to rotate or rerun co-status attach.`;
280
+ return `current-host-unhealthy: control_endpoint.json; control-host unavailable; stale endpoint after control-host restart. control_endpoint.json has not rotated to a reachable host. ${error.message}. Waiting for ${resolve(target.runDir, 'control_endpoint.json')} to rotate or rerun co-status attach.`;
281
281
  }
282
282
  if (error.kind === 'timeout') {
283
283
  return `${error.message}. The control-host did not answer before the attach timeout; if restart is in progress, wait for endpoint rotation or rerun co-status attach.`;
@@ -14,6 +14,10 @@ const LOCAL_DEGRADED_FALLBACK_ALLOWED_VERDICTS = new Set([
14
14
  'degraded'
15
15
  ]);
16
16
  const LOCAL_DEGRADED_FALLBACK_ALLOWED_FINDING_CODES = new Set(['active_worker_proof_missing']);
17
+ const CURRENT_HOST_UNHEALTHY_MARKER = 'current-host-unhealthy';
18
+ const CURRENT_HOST_UNHEALTHY_STALE_ENDPOINT_FALLBACK = 'control-host unavailable; stale endpoint after control-host restart';
19
+ const LEGACY_CURRENT_HOST_UNHEALTHY_STALE_ENDPOINT_FALLBACK = 'control-host unavailable; control_endpoint.json has not rotated to a reachable host';
20
+ const CURRENT_HOST_UNHEALTHY_ROTATED_ENDPOINT_FALLBACK = 'refreshed control-host endpoint is still unreachable';
17
21
  export async function runCoStatusCliShell(params) {
18
22
  if (params.flags.help !== undefined) {
19
23
  params.printHelp();
@@ -69,7 +73,8 @@ function assertAttachCompatibleFlags(flags) {
69
73
  throw new Error(`co-status attaches to an existing control host and does not accept launch-only flags: ${renderedFlags}. Use \`control-host\` to start a control host with launch settings.`);
70
74
  }
71
75
  async function tryReadLocalDegradedUiDataset(input) {
72
- if (!isUiRequestTimeoutError(input.error)) {
76
+ const degradedReason = resolveLocalDegradedReadReason(input.error);
77
+ if (!degradedReason) {
73
78
  return null;
74
79
  }
75
80
  const freshnessReport = await evaluateProviderControlHostFreshnessGauge({
@@ -88,12 +93,29 @@ async function tryReadLocalDegradedUiDataset(input) {
88
93
  }
89
94
  return {
90
95
  ...dataset,
91
- degraded_read: buildDegradedReadPayload(input.target, freshnessReport)
96
+ degraded_read: buildDegradedReadPayload(input.target, freshnessReport, degradedReason)
92
97
  };
93
98
  }
94
- function isUiRequestTimeoutError(error) {
99
+ function resolveLocalDegradedReadReason(error) {
95
100
  const message = error?.message ?? String(error);
96
- return message.includes('control-host ui request timeout after');
101
+ if (message.includes('Re-resolving control_endpoint.json failed')) {
102
+ return null;
103
+ }
104
+ if (isCurrentHostUnhealthyErrorMessage(message)) {
105
+ return 'current_host_unhealthy';
106
+ }
107
+ if (message.includes('control-host ui request timeout after')) {
108
+ return 'ui_request_timeout';
109
+ }
110
+ return null;
111
+ }
112
+ function isCurrentHostUnhealthyErrorMessage(message) {
113
+ return [
114
+ CURRENT_HOST_UNHEALTHY_MARKER,
115
+ CURRENT_HOST_UNHEALTHY_STALE_ENDPOINT_FALLBACK,
116
+ LEGACY_CURRENT_HOST_UNHEALTHY_STALE_ENDPOINT_FALLBACK,
117
+ CURRENT_HOST_UNHEALTHY_ROTATED_ENDPOINT_FALLBACK
118
+ ].some((fragment) => message.includes(fragment));
97
119
  }
98
120
  function isEligibleLocalDegradedFallbackFreshnessReport(report) {
99
121
  if (LOCAL_DEGRADED_FALLBACK_ALLOWED_VERDICTS.has(report.verdict)) {
@@ -105,9 +127,9 @@ function isEligibleLocalDegradedFallbackFreshnessReport(report) {
105
127
  return (report.findings.length > 0 &&
106
128
  report.findings.every((finding) => LOCAL_DEGRADED_FALLBACK_ALLOWED_FINDING_CODES.has(finding.code)));
107
129
  }
108
- function buildDegradedReadPayload(target, report) {
130
+ function buildDegradedReadPayload(target, report, reason) {
109
131
  return {
110
- reason: 'ui_request_timeout',
132
+ reason,
111
133
  source: 'local_seeded_runtime',
112
134
  freshness_verdict: report.verdict,
113
135
  artifact_root: target.runDir,
@@ -1,6 +1,8 @@
1
1
  /* eslint-disable patterns/prefer-logger-over-console */
2
2
  import { formatCodexCliSetupSummary, runCodexCliSetup } from './codexCliSetup.js';
3
3
  import { formatCodexDefaultsSetupSummary, runCodexDefaultsSetup } from './codexDefaultsSetup.js';
4
+ const LEGACY_CHATGPT_AUTH_TRUE_VALUES = new Set(['true', '1', 'yes', 'on', 'enabled']);
5
+ const LEGACY_CHATGPT_AUTH_FALSE_VALUES = new Set(['false', '0', 'no', 'off', 'disabled']);
4
6
  const DEFAULT_DEPENDENCIES = {
5
7
  runCodexCliSetup,
6
8
  runCodexDefaultsSetup,
@@ -48,9 +50,11 @@ export async function runCodexCliShell(params, overrides = {}) {
48
50
  const format = resolveOutputFormat(params.flags);
49
51
  const apply = Boolean(params.flags['yes']);
50
52
  const force = Boolean(params.flags['force']);
53
+ const authScope = readAuthScopeFlag(params.flags);
51
54
  const result = await dependencies.runCodexDefaultsSetup({
52
55
  apply,
53
- force
56
+ force,
57
+ authScope
54
58
  });
55
59
  if (format === 'json') {
56
60
  dependencies.log(JSON.stringify(result, null, 2));
@@ -70,3 +74,46 @@ function readStringFlag(flags, key) {
70
74
  const value = flags[key];
71
75
  return typeof value === 'string' ? value : undefined;
72
76
  }
77
+ function readAuthScopeFlag(flags) {
78
+ const legacyChatGptAuth = readLegacyChatGptAuthFlag(flags);
79
+ if (!Object.prototype.hasOwnProperty.call(flags, 'auth-scope')) {
80
+ if (legacyChatGptAuth === true) {
81
+ return 'chatgpt';
82
+ }
83
+ if (legacyChatGptAuth === false) {
84
+ return 'portable';
85
+ }
86
+ return undefined;
87
+ }
88
+ const value = readStringFlag(flags, 'auth-scope');
89
+ if (value === undefined) {
90
+ throw new Error('Missing value for codex defaults auth scope: expected portable or chatgpt.');
91
+ }
92
+ if (legacyChatGptAuth === true && value !== 'chatgpt') {
93
+ throw new Error('Conflicting codex defaults auth scope: --chatgpt-auth requires --auth-scope chatgpt.');
94
+ }
95
+ if (legacyChatGptAuth === false && value !== 'portable') {
96
+ throw new Error('Conflicting codex defaults auth scope: --chatgpt-auth=false requires --auth-scope portable.');
97
+ }
98
+ if (value === 'portable' || value === 'chatgpt') {
99
+ return value;
100
+ }
101
+ throw new Error(`Invalid codex defaults auth scope: ${value}`);
102
+ }
103
+ function readLegacyChatGptAuthFlag(flags) {
104
+ if (!Object.prototype.hasOwnProperty.call(flags, 'chatgpt-auth')) {
105
+ return undefined;
106
+ }
107
+ const value = flags['chatgpt-auth'];
108
+ if (typeof value === 'boolean') {
109
+ return value;
110
+ }
111
+ const normalized = value.trim().toLowerCase();
112
+ if (LEGACY_CHATGPT_AUTH_TRUE_VALUES.has(normalized)) {
113
+ return true;
114
+ }
115
+ if (LEGACY_CHATGPT_AUTH_FALSE_VALUES.has(normalized)) {
116
+ return false;
117
+ }
118
+ throw new Error(`Invalid codex defaults ChatGPT auth flag: --chatgpt-auth expected a boolean-like value, got ${value}.`);
119
+ }