@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.
- package/README.md +43 -83
- package/dist/bin/codex-orchestrator.js +2 -0
- package/dist/orchestrator/src/cli/adapters/CommandBuilder.js +50 -0
- package/dist/orchestrator/src/cli/adapters/cloudFailureDiagnostics.js +117 -5
- package/dist/orchestrator/src/cli/coStatusAttachCliShell.js +2 -2
- package/dist/orchestrator/src/cli/coStatusCliShell.js +28 -6
- package/dist/orchestrator/src/cli/codexCliShell.js +48 -1
- package/dist/orchestrator/src/cli/codexDefaultsSetup.js +217 -26
- package/dist/orchestrator/src/cli/control/controlHostSupervision.js +28 -6
- package/dist/orchestrator/src/cli/control/controlRuntime.js +17 -6
- package/dist/orchestrator/src/cli/control/controlStatusDashboard.js +6 -1
- package/dist/orchestrator/src/cli/control/selectedRunProjection.js +49 -2
- package/dist/orchestrator/src/cli/doctor.js +142 -48
- package/dist/orchestrator/src/cli/init.js +94 -1
- package/dist/orchestrator/src/cli/providerLinearChildLaneRunner.js +64 -1
- package/dist/orchestrator/src/cli/providerLinearWorkerRunner.js +1165 -69
- package/dist/orchestrator/src/cli/rlm/alignment.js +3 -3
- package/dist/orchestrator/src/cli/services/commandRunner.js +31 -0
- package/dist/orchestrator/src/cli/utils/cloudPreflight.js +202 -6
- package/dist/orchestrator/src/cli/utils/codexFeatures.js +60 -0
- package/dist/orchestrator/src/manager.js +74 -4
- package/dist/scripts/lib/docs-catalog.js +35 -1
- package/docs/README.md +333 -0
- package/docs/book/README.md +19 -0
- package/docs/book/codex-cli-0124-adoption.md +68 -0
- package/docs/book/local-hook-impact.md +73 -0
- package/docs/book/operations.md +60 -0
- package/docs/book/public-posture.md +34 -0
- package/docs/book/setup.md +91 -0
- package/docs/book/skills.md +11 -0
- package/docs/guides/codex-version-policy.md +104 -0
- package/docs/public/downstream-setup.md +25 -18
- package/package.json +4 -1
- package/plugins/codex-orchestrator/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-orchestrator/launcher.mjs +6 -4
- package/schemas/manifest.json +17 -0
- package/skills/README.md +26 -0
- package/skills/collab-subagents-first/SKILL.md +1 -1
- package/skills/delegation-usage/DELEGATION_GUIDE.md +12 -7
- package/skills/delegation-usage/SKILL.md +13 -8
- 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
|
|
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
|
|
5
|
+
## Release Posture
|
|
6
6
|
|
|
7
|
-
This README tracks the current `main` branch.
|
|
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
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
30
|
+
The full version and model policy lives in [docs/guides/codex-version-policy.md](docs/guides/codex-version-policy.md).
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
## Quickstart
|
|
32
33
|
|
|
33
34
|
```bash
|
|
34
|
-
codex
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
## 2-minute quickstart (current `main`)
|
|
43
|
+
Use `codex login --device-auth` when browser login is not available.
|
|
40
44
|
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
+
```bash
|
|
50
|
+
# Codex 0.121.0 accepts either command.
|
|
51
|
+
codex marketplace add "$(npm root -g)/@kbediako/codex-orchestrator"
|
|
67
52
|
|
|
68
|
-
|
|
69
|
-
|
|
53
|
+
# Codex 0.122.0+ uses the plugin command.
|
|
54
|
+
codex plugin marketplace add "$(npm root -g)/@kbediako/codex-orchestrator"
|
|
55
|
+
```
|
|
70
56
|
|
|
71
|
-
`
|
|
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
|
|
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
|
|
81
|
-
codex-orchestrator
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
codex-orchestrator skills install
|
|
90
|
-
```
|
|
71
|
+
## Downstream Setup
|
|
91
72
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
-
|
|
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
|
|
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
|
|
169
|
+
function isEnvironmentNotFoundSignal(signal) {
|
|
156
170
|
const lowercase = signal.toLowerCase();
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
})
|
|
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(
|
|
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
|
|
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 `
|
|
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
|
-
|
|
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
|
|
99
|
+
function resolveLocalDegradedReadReason(error) {
|
|
95
100
|
const message = error?.message ?? String(error);
|
|
96
|
-
|
|
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
|
|
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
|
+
}
|