@kbediako/codex-orchestrator 0.1.20 → 0.1.22
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 +4 -1
- package/dist/bin/codex-orchestrator.js +32 -1
- package/dist/orchestrator/src/cli/adapters/CommandReviewer.js +16 -0
- package/dist/orchestrator/src/cli/adapters/CommandTester.js +25 -0
- package/dist/orchestrator/src/cli/doctor.js +210 -1
- package/dist/orchestrator/src/cli/orchestrator.js +29 -1
- package/dist/orchestrator/src/cli/services/pipelineExperience.js +1 -0
- package/dist/orchestrator/src/cli/skills.js +30 -4
- package/dist/orchestrator/src/cli/utils/cloudPreflight.js +101 -0
- package/docs/README.md +3 -3
- package/package.json +1 -1
- package/skills/collab-subagents-first/SKILL.md +1 -0
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ Delegation guard profile:
|
|
|
89
89
|
## Delegation + RLM flow
|
|
90
90
|
|
|
91
91
|
RLM (Recursive Language Model) is the long-horizon loop used by the `rlm` pipeline (`codex-orchestrator rlm "<goal>"` or `codex-orchestrator start rlm --goal "<goal>"`). Delegated runs only enter RLM when the child is launched with the `rlm` pipeline (or the rlm runner directly). In auto mode it resolves to symbolic when delegated, when `RLM_CONTEXT_PATH` is set, or when the context exceeds `RLM_SYMBOLIC_MIN_BYTES`; otherwise it stays iterative. The runner writes state to `.runs/<task-id>/cli/<run-id>/rlm/state.json` and stops when the validator passes or budgets are exhausted.
|
|
92
|
-
Symbolic subcalls can optionally use collab tools
|
|
92
|
+
Symbolic subcalls can optionally use collab tools. Fast path: `codex-orchestrator rlm --collab "<goal>"` (sets `RLM_SYMBOLIC_COLLAB=1` and implies symbolic mode). Collab requires `collab=true` in `codex features list`. Collab tool calls parsed from `codex exec --json --enable collab` are stored in `manifest.collab_tool_calls` (bounded by `CODEX_ORCHESTRATOR_COLLAB_MAX_EVENTS`, set to `0` to disable). `codex-orchestrator codex setup` remains available when you want a managed/pinned CLI path.
|
|
93
93
|
|
|
94
94
|
### Delegation flow
|
|
95
95
|
```mermaid
|
|
@@ -137,14 +137,17 @@ codex-orchestrator skills install
|
|
|
137
137
|
|
|
138
138
|
Options:
|
|
139
139
|
- `--force` overwrites existing files.
|
|
140
|
+
- `--only <skills>` installs only selected skills (comma-separated). Combine with `--force` to overwrite only those.
|
|
140
141
|
- `--codex-home <path>` targets a different Codex home directory.
|
|
141
142
|
|
|
142
143
|
Bundled skills (may vary by release):
|
|
144
|
+
- `collab-subagents-first`
|
|
143
145
|
- `delegation-usage`
|
|
144
146
|
- `standalone-review`
|
|
145
147
|
- `docs-first`
|
|
146
148
|
- `collab-evals`
|
|
147
149
|
- `collab-deliberation`
|
|
150
|
+
- `release`
|
|
148
151
|
- `delegate-early` (compatibility alias; use `delegation-usage`)
|
|
149
152
|
|
|
150
153
|
## DevTools readiness
|
|
@@ -167,6 +167,25 @@ function applyRlmEnvOverrides(flags, goal) {
|
|
|
167
167
|
if (goal) {
|
|
168
168
|
process.env.RLM_GOAL = goal;
|
|
169
169
|
}
|
|
170
|
+
const collabRaw = flags['collab'];
|
|
171
|
+
if (collabRaw !== undefined) {
|
|
172
|
+
const normalized = typeof collabRaw === 'string' ? collabRaw.trim().toLowerCase() : collabRaw === true ? 'true' : '';
|
|
173
|
+
const enabled = collabRaw === true || normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'auto';
|
|
174
|
+
const disabled = normalized === '0' || normalized === 'false' || normalized === 'no' || normalized === 'off';
|
|
175
|
+
if (enabled) {
|
|
176
|
+
process.env.RLM_SYMBOLIC_COLLAB = '1';
|
|
177
|
+
// Collab is only used in the symbolic loop; make the flag do what users expect.
|
|
178
|
+
if (!process.env.RLM_MODE) {
|
|
179
|
+
process.env.RLM_MODE = 'symbolic';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else if (disabled) {
|
|
183
|
+
process.env.RLM_SYMBOLIC_COLLAB = '0';
|
|
184
|
+
}
|
|
185
|
+
else if (typeof collabRaw === 'string') {
|
|
186
|
+
throw new Error('Invalid --collab value. Use --collab (or: --collab auto|true|false).');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
170
189
|
const validator = readStringFlag(flags, 'validator');
|
|
171
190
|
if (validator) {
|
|
172
191
|
process.env.RLM_VALIDATOR = validator;
|
|
@@ -605,7 +624,15 @@ async function handleSkills(rawArgs) {
|
|
|
605
624
|
const format = flags['format'] === 'json' ? 'json' : 'text';
|
|
606
625
|
const force = flags['force'] === true;
|
|
607
626
|
const codexHome = readStringFlag(flags, 'codex-home');
|
|
608
|
-
const
|
|
627
|
+
const onlyRaw = flags['only'];
|
|
628
|
+
let only;
|
|
629
|
+
if (onlyRaw !== undefined) {
|
|
630
|
+
if (typeof onlyRaw !== 'string') {
|
|
631
|
+
throw new Error('--only requires a comma-separated list of skill names.');
|
|
632
|
+
}
|
|
633
|
+
only = onlyRaw.split(',').map((entry) => entry.trim()).filter(Boolean);
|
|
634
|
+
}
|
|
635
|
+
const result = await installSkills({ force, codexHome, only });
|
|
609
636
|
if (format === 'json') {
|
|
610
637
|
console.log(JSON.stringify(result, null, 2));
|
|
611
638
|
}
|
|
@@ -871,6 +898,7 @@ Commands:
|
|
|
871
898
|
--cloud Shortcut for --execution-mode cloud.
|
|
872
899
|
--target <stage-id> Focus plan/build metadata on a specific stage (alias: --target-stage).
|
|
873
900
|
--goal "<goal>" When pipeline is rlm, set the RLM goal.
|
|
901
|
+
--collab [auto|true|false] When pipeline is rlm, enable collab subagents (implies symbolic mode).
|
|
874
902
|
--validator <cmd|none> When pipeline is rlm, set the validator command.
|
|
875
903
|
--max-iterations <n> When pipeline is rlm, override max iterations.
|
|
876
904
|
--max-minutes <n> When pipeline is rlm, override max minutes.
|
|
@@ -880,6 +908,7 @@ Commands:
|
|
|
880
908
|
|
|
881
909
|
rlm "<goal>" Run RLM loop until validator passes.
|
|
882
910
|
--task <id> Override task identifier.
|
|
911
|
+
--collab [auto|true|false] Enable collab subagents (implies symbolic mode).
|
|
883
912
|
--validator <cmd|none> Set validator command or disable validation.
|
|
884
913
|
--max-iterations <n> Override max iterations (0 = unlimited with validator).
|
|
885
914
|
--max-minutes <n> Optional time-based guardrail in minutes.
|
|
@@ -946,6 +975,7 @@ Commands:
|
|
|
946
975
|
--format json Emit machine-readable output (dry-run only).
|
|
947
976
|
skills install Install bundled skills into $CODEX_HOME/skills.
|
|
948
977
|
--force Overwrite existing skill files.
|
|
978
|
+
--only <skills> Install only selected skills (comma-separated).
|
|
949
979
|
--codex-home <path> Override the target Codex home directory.
|
|
950
980
|
--format json Emit machine-readable output.
|
|
951
981
|
mcp serve [--repo <path>] [--dry-run] [-- <extra args>]
|
|
@@ -972,6 +1002,7 @@ function printSkillsHelp() {
|
|
|
972
1002
|
Commands:
|
|
973
1003
|
install Install bundled skills into $CODEX_HOME/skills.
|
|
974
1004
|
--force Overwrite existing skill files.
|
|
1005
|
+
--only <skills> Install only selected skills (comma-separated).
|
|
975
1006
|
--codex-home <path> Override the target Codex home directory.
|
|
976
1007
|
--format json Emit machine-readable output.
|
|
977
1008
|
`);
|
|
@@ -8,6 +8,22 @@ export class CommandReviewer {
|
|
|
8
8
|
const result = this.requireResult();
|
|
9
9
|
if (input.mode === 'cloud') {
|
|
10
10
|
const cloudExecution = result.manifest.cloud_execution;
|
|
11
|
+
if (!cloudExecution) {
|
|
12
|
+
const summaryLines = [
|
|
13
|
+
result.success
|
|
14
|
+
? 'Cloud mode requested but preflight failed; fell back to MCP mode successfully.'
|
|
15
|
+
: 'Cloud mode requested but preflight failed; fell back to MCP mode and the run failed.',
|
|
16
|
+
`Manifest: ${result.manifestPath}`,
|
|
17
|
+
`Runner log: ${result.logPath}`
|
|
18
|
+
];
|
|
19
|
+
return {
|
|
20
|
+
summary: summaryLines.join('\n'),
|
|
21
|
+
decision: {
|
|
22
|
+
approved: result.success,
|
|
23
|
+
feedback: result.notes.join('\n') || result.manifest.summary || undefined
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
11
27
|
const status = cloudExecution?.status ?? 'unknown';
|
|
12
28
|
const cloudTask = cloudExecution?.task_id ?? '<unknown>';
|
|
13
29
|
const approved = status === 'ready' && result.success;
|
|
@@ -9,6 +9,31 @@ export class CommandTester {
|
|
|
9
9
|
const result = this.requireResult();
|
|
10
10
|
if (input.mode === 'cloud') {
|
|
11
11
|
const cloudExecution = result.manifest.cloud_execution;
|
|
12
|
+
if (!cloudExecution) {
|
|
13
|
+
// Cloud mode can fall back to MCP when preflight fails; treat that as a normal guardrail run.
|
|
14
|
+
const guardrailStatus = ensureGuardrailStatus(result.manifest);
|
|
15
|
+
const reports = [
|
|
16
|
+
{
|
|
17
|
+
name: 'cloud-preflight',
|
|
18
|
+
status: 'passed',
|
|
19
|
+
details: result.manifest.summary?.trim() ||
|
|
20
|
+
'Cloud execution was skipped due to preflight failure; fell back to MCP mode.'
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'guardrails',
|
|
24
|
+
status: guardrailStatus.present ? 'passed' : 'failed',
|
|
25
|
+
details: guardrailStatus.present
|
|
26
|
+
? guardrailStatus.summary
|
|
27
|
+
: guardrailStatus.recommendation ?? guardrailStatus.summary
|
|
28
|
+
}
|
|
29
|
+
];
|
|
30
|
+
return {
|
|
31
|
+
subtaskId: input.build.subtaskId,
|
|
32
|
+
success: guardrailStatus.present && result.success,
|
|
33
|
+
reports,
|
|
34
|
+
runId: input.runId
|
|
35
|
+
};
|
|
36
|
+
}
|
|
12
37
|
const status = cloudExecution?.status ?? 'unknown';
|
|
13
38
|
const passed = status === 'ready' && result.success;
|
|
14
39
|
const diagnosis = diagnoseCloudFailure({
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
2
5
|
import { buildDevtoolsSetupPlan, DEVTOOLS_SKILL_NAME, resolveDevtoolsReadiness } from './utils/devtools.js';
|
|
6
|
+
import { resolveCodexCliBin, resolveCodexCliReadiness } from './utils/codexCli.js';
|
|
7
|
+
import { resolveCodexHome } from './utils/codexPaths.js';
|
|
3
8
|
import { resolveOptionalDependency } from './utils/optionalDeps.js';
|
|
4
9
|
const OPTIONAL_DEPENDENCIES = [
|
|
5
10
|
{
|
|
@@ -65,11 +70,56 @@ export function runDoctor(cwd = process.cwd()) {
|
|
|
65
70
|
if (readiness.config.status !== 'ok') {
|
|
66
71
|
missing.push(`${DEVTOOLS_SKILL_NAME}-config`);
|
|
67
72
|
}
|
|
73
|
+
const codexBin = resolveCodexCliBin(process.env);
|
|
74
|
+
const managedCodex = resolveCodexCliReadiness(process.env);
|
|
75
|
+
const features = readCodexFeatureFlags(codexBin);
|
|
76
|
+
const collabEnabled = features?.collab ?? null;
|
|
77
|
+
const collabStatus = features === null ? 'unavailable' : collabEnabled ? 'ok' : 'disabled';
|
|
78
|
+
const cloudCmdAvailable = canRunCommand(codexBin, ['cloud', '--help']);
|
|
79
|
+
const cloudEnvIdConfigured = typeof process.env.CODEX_CLOUD_ENV_ID === 'string' && process.env.CODEX_CLOUD_ENV_ID.trim().length > 0;
|
|
80
|
+
const cloudBranch = typeof process.env.CODEX_CLOUD_BRANCH === 'string' && process.env.CODEX_CLOUD_BRANCH.trim().length > 0
|
|
81
|
+
? process.env.CODEX_CLOUD_BRANCH.trim().replace(/^refs\/heads\//u, '')
|
|
82
|
+
: null;
|
|
83
|
+
const cloudStatus = !cloudCmdAvailable ? 'unavailable' : cloudEnvIdConfigured ? 'ok' : 'not_configured';
|
|
84
|
+
const delegationConfig = inspectDelegationConfig();
|
|
85
|
+
const delegationStatus = delegationConfig.status === 'ok' ? 'ok' : 'missing-config';
|
|
68
86
|
return {
|
|
69
87
|
status: missing.length === 0 ? 'ok' : 'warning',
|
|
70
88
|
missing,
|
|
71
89
|
dependencies,
|
|
72
|
-
devtools
|
|
90
|
+
devtools,
|
|
91
|
+
codex_cli: {
|
|
92
|
+
active: { command: codexBin },
|
|
93
|
+
managed: managedCodex
|
|
94
|
+
},
|
|
95
|
+
collab: {
|
|
96
|
+
status: collabStatus,
|
|
97
|
+
enabled: collabEnabled,
|
|
98
|
+
enablement: [
|
|
99
|
+
'Enable collab for symbolic RLM runs with: codex-orchestrator rlm --collab "<goal>"',
|
|
100
|
+
'Or set: RLM_SYMBOLIC_COLLAB=1 (implies symbolic mode when using --collab).',
|
|
101
|
+
'If collab is disabled in codex features: codex features enable collab'
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
cloud: {
|
|
105
|
+
status: cloudStatus,
|
|
106
|
+
env_id_configured: cloudEnvIdConfigured,
|
|
107
|
+
branch: cloudBranch,
|
|
108
|
+
enablement: [
|
|
109
|
+
'Set CODEX_CLOUD_ENV_ID to a valid Codex Cloud environment id.',
|
|
110
|
+
'Optional: set CODEX_CLOUD_BRANCH (must exist on origin).',
|
|
111
|
+
'Then run a pipeline stage in cloud mode with: codex-orchestrator start <pipeline> --cloud --target <stage-id>'
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
delegation: {
|
|
115
|
+
status: delegationStatus,
|
|
116
|
+
config: delegationConfig,
|
|
117
|
+
enablement: [
|
|
118
|
+
`Run: codex mcp add delegation -- codex-orchestrator delegate-server --repo ${cwd}`,
|
|
119
|
+
"Enable for a run with: codex -c 'mcp_servers.delegation.enabled=true' ...",
|
|
120
|
+
'See: codex-orchestrator init codex'
|
|
121
|
+
]
|
|
122
|
+
}
|
|
73
123
|
};
|
|
74
124
|
}
|
|
75
125
|
export function formatDoctorSummary(result) {
|
|
@@ -118,5 +168,164 @@ export function formatDoctorSummary(result) {
|
|
|
118
168
|
for (const line of result.devtools.enablement) {
|
|
119
169
|
lines.push(` - ${line}`);
|
|
120
170
|
}
|
|
171
|
+
lines.push(`Codex CLI: ${result.codex_cli.active.command}`);
|
|
172
|
+
lines.push(` - managed: ${result.codex_cli.managed.status} (${result.codex_cli.managed.config.path})`);
|
|
173
|
+
if (result.codex_cli.managed.status === 'invalid' && result.codex_cli.managed.config.error) {
|
|
174
|
+
lines.push(` error: ${result.codex_cli.managed.config.error}`);
|
|
175
|
+
}
|
|
176
|
+
if (result.codex_cli.managed.status === 'ok') {
|
|
177
|
+
lines.push(` - binary: ${result.codex_cli.managed.binary.status} (${result.codex_cli.managed.binary.path})`);
|
|
178
|
+
if (result.codex_cli.managed.install?.version) {
|
|
179
|
+
lines.push(` - version: ${result.codex_cli.managed.install.version}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
lines.push(`Collab: ${result.collab.status}`);
|
|
183
|
+
if (result.collab.enabled !== null) {
|
|
184
|
+
lines.push(` - enabled: ${result.collab.enabled}`);
|
|
185
|
+
}
|
|
186
|
+
for (const line of result.collab.enablement) {
|
|
187
|
+
lines.push(` - ${line}`);
|
|
188
|
+
}
|
|
189
|
+
lines.push(`Cloud: ${result.cloud.status}`);
|
|
190
|
+
lines.push(` - CODEX_CLOUD_ENV_ID: ${result.cloud.env_id_configured ? 'set' : 'missing'}`);
|
|
191
|
+
lines.push(` - CODEX_CLOUD_BRANCH: ${result.cloud.branch ?? '<unset>'}`);
|
|
192
|
+
for (const line of result.cloud.enablement) {
|
|
193
|
+
lines.push(` - ${line}`);
|
|
194
|
+
}
|
|
195
|
+
lines.push(`Delegation: ${result.delegation.status}`);
|
|
196
|
+
const delegationConfigLabel = result.delegation.config.status === 'ok'
|
|
197
|
+
? `ok (${result.delegation.config.path})`
|
|
198
|
+
: `missing (${result.delegation.config.path})`;
|
|
199
|
+
lines.push(` - config.toml: ${delegationConfigLabel}`);
|
|
200
|
+
if (result.delegation.config.detail) {
|
|
201
|
+
lines.push(` detail: ${result.delegation.config.detail}`);
|
|
202
|
+
}
|
|
203
|
+
for (const line of result.delegation.enablement) {
|
|
204
|
+
lines.push(` - ${line}`);
|
|
205
|
+
}
|
|
121
206
|
return lines;
|
|
122
207
|
}
|
|
208
|
+
function readCodexFeatureFlags(codexBin) {
|
|
209
|
+
const result = spawnSync(codexBin, ['features', 'list'], {
|
|
210
|
+
encoding: 'utf8',
|
|
211
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
212
|
+
timeout: 5000
|
|
213
|
+
});
|
|
214
|
+
if (result.error || result.status !== 0) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
const stdout = String(result.stdout ?? '');
|
|
218
|
+
const flags = {};
|
|
219
|
+
for (const line of stdout.split(/\r?\n/u)) {
|
|
220
|
+
const trimmed = line.trim();
|
|
221
|
+
if (!trimmed) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const tokens = trimmed.split(/\s+/u);
|
|
225
|
+
if (tokens.length < 2) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const name = tokens[0] ?? '';
|
|
229
|
+
const enabledToken = tokens[tokens.length - 1] ?? '';
|
|
230
|
+
if (!name) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (enabledToken === 'true') {
|
|
234
|
+
flags[name] = true;
|
|
235
|
+
}
|
|
236
|
+
else if (enabledToken === 'false') {
|
|
237
|
+
flags[name] = false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return flags;
|
|
241
|
+
}
|
|
242
|
+
function canRunCommand(command, args) {
|
|
243
|
+
const result = spawnSync(command, args, {
|
|
244
|
+
encoding: 'utf8',
|
|
245
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
246
|
+
timeout: 5000
|
|
247
|
+
});
|
|
248
|
+
if (result.error) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
return result.status === 0;
|
|
252
|
+
}
|
|
253
|
+
function inspectDelegationConfig(env = process.env) {
|
|
254
|
+
const codexHome = resolveCodexHome(env);
|
|
255
|
+
const configPath = join(codexHome, 'config.toml');
|
|
256
|
+
if (!existsSync(configPath)) {
|
|
257
|
+
return { status: 'missing', path: configPath, detail: 'config.toml not found' };
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const raw = readFileSync(configPath, 'utf8');
|
|
261
|
+
const hasEntry = hasMcpServerEntry(raw, 'delegation');
|
|
262
|
+
if (hasEntry) {
|
|
263
|
+
return { status: 'ok', path: configPath };
|
|
264
|
+
}
|
|
265
|
+
return { status: 'missing', path: configPath, detail: 'mcp_servers.delegation entry not found' };
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
return {
|
|
269
|
+
status: 'missing',
|
|
270
|
+
path: configPath,
|
|
271
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function hasMcpServerEntry(raw, serverName) {
|
|
276
|
+
const lines = raw.split('\n');
|
|
277
|
+
let currentTable = null;
|
|
278
|
+
for (const line of lines) {
|
|
279
|
+
const trimmed = stripTomlComment(line).trim();
|
|
280
|
+
if (!trimmed) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const tableMatch = trimmed.match(/^\[(.+)\]$/u);
|
|
284
|
+
if (tableMatch) {
|
|
285
|
+
currentTable = tableMatch[1]?.trim() ?? null;
|
|
286
|
+
if (currentTable === `mcp_servers.${serverName}` ||
|
|
287
|
+
currentTable === `mcp_servers."${serverName}"` ||
|
|
288
|
+
currentTable === `mcp_servers.'${serverName}'`) {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (trimmed.startsWith('mcp_servers.')) {
|
|
294
|
+
if (trimmed.startsWith(`mcp_servers."${serverName}".`)) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
if (trimmed.startsWith(`mcp_servers.'${serverName}'.`)) {
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
if (trimmed.startsWith(`mcp_servers.${serverName}.`)) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
if (trimmed.startsWith(`mcp_servers."${serverName}"=`)) {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
if (trimmed.startsWith(`mcp_servers.'${serverName}'=`)) {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
if (trimmed.startsWith(`mcp_servers.${serverName}=`)) {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (currentTable === 'mcp_servers') {
|
|
314
|
+
const entryPattern = new RegExp(`^"?${escapeRegExp(serverName)}"?\\s*=`, 'u');
|
|
315
|
+
if (entryPattern.test(trimmed)) {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
function stripTomlComment(line) {
|
|
323
|
+
const index = line.indexOf('#');
|
|
324
|
+
if (index === -1) {
|
|
325
|
+
return line;
|
|
326
|
+
}
|
|
327
|
+
return line.slice(0, index);
|
|
328
|
+
}
|
|
329
|
+
function escapeRegExp(value) {
|
|
330
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
331
|
+
}
|
|
@@ -31,6 +31,7 @@ import { CLI_EXECUTION_MODE_PARSER, resolveRequiresCloudPolicy } from '../utils/
|
|
|
31
31
|
import { resolveCodexCliBin } from './utils/codexCli.js';
|
|
32
32
|
import { CodexCloudTaskExecutor } from '../cloud/CodexCloudTaskExecutor.js';
|
|
33
33
|
import { persistPipelineExperience } from './services/pipelineExperience.js';
|
|
34
|
+
import { runCloudPreflight } from './utils/cloudPreflight.js';
|
|
34
35
|
const resolveBaseEnvironment = () => normalizeEnvironmentPaths(resolveEnvironmentPaths());
|
|
35
36
|
const CONFIG_OVERRIDE_ENV_KEYS = ['CODEX_CONFIG_OVERRIDES', 'CODEX_MCP_CONFIG_OVERRIDES'];
|
|
36
37
|
const DEFAULT_CLOUD_POLL_INTERVAL_SECONDS = 10;
|
|
@@ -552,6 +553,27 @@ export class CodexOrchestrator {
|
|
|
552
553
|
}
|
|
553
554
|
async executePipeline(options) {
|
|
554
555
|
if (options.mode === 'cloud') {
|
|
556
|
+
const environmentId = resolveCloudEnvironmentId(options.task, options.target, options.envOverrides);
|
|
557
|
+
const branch = readCloudString(options.envOverrides?.CODEX_CLOUD_BRANCH) ??
|
|
558
|
+
readCloudString(process.env.CODEX_CLOUD_BRANCH);
|
|
559
|
+
const mergedEnv = { ...process.env, ...(options.envOverrides ?? {}) };
|
|
560
|
+
const codexBin = resolveCodexCliBin(mergedEnv);
|
|
561
|
+
const preflight = await runCloudPreflight({
|
|
562
|
+
repoRoot: options.env.repoRoot,
|
|
563
|
+
codexBin,
|
|
564
|
+
environmentId,
|
|
565
|
+
branch,
|
|
566
|
+
env: mergedEnv
|
|
567
|
+
});
|
|
568
|
+
if (!preflight.ok) {
|
|
569
|
+
const detail = `Cloud preflight failed; falling back to mcp. ` +
|
|
570
|
+
preflight.issues.map((issue) => issue.message).join(' ');
|
|
571
|
+
appendSummary(options.manifest, detail);
|
|
572
|
+
logger.warn(detail);
|
|
573
|
+
const fallback = await this.executePipeline({ ...options, mode: 'mcp', executionModeOverride: 'mcp' });
|
|
574
|
+
fallback.notes.unshift(detail);
|
|
575
|
+
return fallback;
|
|
576
|
+
}
|
|
555
577
|
return await this.executeCloudPipeline(options);
|
|
556
578
|
}
|
|
557
579
|
const { env, pipeline, manifest, paths, runEvents, envOverrides } = options;
|
|
@@ -883,6 +905,12 @@ export class CodexOrchestrator {
|
|
|
883
905
|
const disableFeatures = readCloudFeatureList(readCloudString(envOverrides?.CODEX_CLOUD_DISABLE_FEATURES) ??
|
|
884
906
|
readCloudString(process.env.CODEX_CLOUD_DISABLE_FEATURES));
|
|
885
907
|
const codexBin = resolveCodexCliBin({ ...process.env, ...(envOverrides ?? {}) });
|
|
908
|
+
const cloudEnvOverrides = {
|
|
909
|
+
...(envOverrides ?? {}),
|
|
910
|
+
CODEX_NON_INTERACTIVE: envOverrides?.CODEX_NON_INTERACTIVE ?? process.env.CODEX_NON_INTERACTIVE ?? '1',
|
|
911
|
+
CODEX_NO_INTERACTIVE: envOverrides?.CODEX_NO_INTERACTIVE ?? process.env.CODEX_NO_INTERACTIVE ?? '1',
|
|
912
|
+
CODEX_INTERACTIVE: envOverrides?.CODEX_INTERACTIVE ?? process.env.CODEX_INTERACTIVE ?? '0'
|
|
913
|
+
};
|
|
886
914
|
const cloudResult = await executor.execute({
|
|
887
915
|
codexBin,
|
|
888
916
|
prompt,
|
|
@@ -895,7 +923,7 @@ export class CodexOrchestrator {
|
|
|
895
923
|
branch,
|
|
896
924
|
enableFeatures,
|
|
897
925
|
disableFeatures,
|
|
898
|
-
env:
|
|
926
|
+
env: cloudEnvOverrides
|
|
899
927
|
});
|
|
900
928
|
success = cloudResult.success;
|
|
901
929
|
notes.push(...cloudResult.notes);
|
|
@@ -53,6 +53,7 @@ export async function persistPipelineExperience(params) {
|
|
|
53
53
|
maxSummaryWords: instructions.experienceMaxWords
|
|
54
54
|
});
|
|
55
55
|
await store.recordBatch([record], relativeToRepo(env, paths.manifestPath));
|
|
56
|
+
logger.info(`[experience] Recorded pipeline experience (domain=${domain}, words=${tokenCount}) for ${pipeline.id} run ${manifest.run_id}.`);
|
|
56
57
|
}
|
|
57
58
|
catch (error) {
|
|
58
59
|
logger.warn(`Failed to persist pipeline experience for run ${manifest.run_id}: ${error?.message ?? String(error)}`);
|
|
@@ -12,18 +12,28 @@ export async function installSkills(options = {}) {
|
|
|
12
12
|
const targetRoot = join(codexHome, 'skills');
|
|
13
13
|
const written = [];
|
|
14
14
|
const skipped = [];
|
|
15
|
-
const
|
|
16
|
-
|
|
15
|
+
const availableSkills = await listSkillNames(sourceRoot);
|
|
16
|
+
const selectedSkills = resolveSelectedSkills(availableSkills, options.only);
|
|
17
|
+
const copyOptions = {
|
|
17
18
|
force: options.force ?? false,
|
|
18
19
|
written,
|
|
19
20
|
skipped
|
|
20
|
-
}
|
|
21
|
+
};
|
|
22
|
+
if (selectedSkills.length === availableSkills.length) {
|
|
23
|
+
await copyDir(sourceRoot, targetRoot, copyOptions);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
await mkdir(targetRoot, { recursive: true });
|
|
27
|
+
for (const skill of selectedSkills) {
|
|
28
|
+
await copyDir(join(sourceRoot, skill), join(targetRoot, skill), copyOptions);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
21
31
|
return {
|
|
22
32
|
written,
|
|
23
33
|
skipped,
|
|
24
34
|
sourceRoot,
|
|
25
35
|
targetRoot,
|
|
26
|
-
skills:
|
|
36
|
+
skills: selectedSkills
|
|
27
37
|
};
|
|
28
38
|
}
|
|
29
39
|
export function formatSkillsInstallSummary(result, cwd = process.cwd()) {
|
|
@@ -61,6 +71,22 @@ async function listSkillNames(sourceRoot) {
|
|
|
61
71
|
const entries = await readdir(sourceRoot, { withFileTypes: true });
|
|
62
72
|
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
63
73
|
}
|
|
74
|
+
function resolveSelectedSkills(availableSkills, only) {
|
|
75
|
+
if (!only) {
|
|
76
|
+
return availableSkills;
|
|
77
|
+
}
|
|
78
|
+
const trimmed = only.map((entry) => entry.trim()).filter(Boolean);
|
|
79
|
+
if (trimmed.length === 0) {
|
|
80
|
+
throw new Error('No skills specified for --only.');
|
|
81
|
+
}
|
|
82
|
+
const requested = Array.from(new Set(trimmed));
|
|
83
|
+
const available = new Set(availableSkills);
|
|
84
|
+
const unknown = requested.filter((skill) => !available.has(skill));
|
|
85
|
+
if (unknown.length > 0) {
|
|
86
|
+
throw new Error(`Unknown skill(s): ${unknown.join(', ')}. Available skills: ${availableSkills.join(', ')}`);
|
|
87
|
+
}
|
|
88
|
+
return requested;
|
|
89
|
+
}
|
|
64
90
|
async function assertDirectory(path) {
|
|
65
91
|
const info = await stat(path).catch(() => null);
|
|
66
92
|
if (!info || !info.isDirectory()) {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
function runCommand(command, args, options) {
|
|
3
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
const child = spawn(command, args, {
|
|
6
|
+
cwd: options.cwd,
|
|
7
|
+
env: options.env,
|
|
8
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
9
|
+
});
|
|
10
|
+
let stdout = '';
|
|
11
|
+
let stderr = '';
|
|
12
|
+
let settled = false;
|
|
13
|
+
const timer = setTimeout(() => {
|
|
14
|
+
if (settled) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
settled = true;
|
|
18
|
+
child.kill('SIGTERM');
|
|
19
|
+
setTimeout(() => child.kill('SIGKILL'), 4000).unref();
|
|
20
|
+
resolve({
|
|
21
|
+
exitCode: 124,
|
|
22
|
+
stdout,
|
|
23
|
+
stderr: `${stderr}\nTimed out after ${Math.round(timeoutMs / 1000)}s.`.trim()
|
|
24
|
+
});
|
|
25
|
+
}, timeoutMs);
|
|
26
|
+
timer.unref();
|
|
27
|
+
child.stdout?.on('data', (chunk) => {
|
|
28
|
+
stdout += chunk.toString();
|
|
29
|
+
});
|
|
30
|
+
child.stderr?.on('data', (chunk) => {
|
|
31
|
+
stderr += chunk.toString();
|
|
32
|
+
});
|
|
33
|
+
child.once('error', (error) => {
|
|
34
|
+
if (settled) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
settled = true;
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
resolve({ exitCode: 1, stdout, stderr: `${stderr}\n${error.message}`.trim() });
|
|
40
|
+
});
|
|
41
|
+
child.once('close', (code) => {
|
|
42
|
+
if (settled) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
settled = true;
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
resolve({ exitCode: typeof code === 'number' ? code : 1, stdout, stderr });
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function normalizeBranch(raw) {
|
|
52
|
+
const trimmed = String(raw ?? '').trim();
|
|
53
|
+
if (!trimmed) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return trimmed.replace(/^refs\/heads\//u, '');
|
|
57
|
+
}
|
|
58
|
+
export async function runCloudPreflight(params) {
|
|
59
|
+
const issues = [];
|
|
60
|
+
const branch = normalizeBranch(params.branch);
|
|
61
|
+
if (!params.environmentId) {
|
|
62
|
+
issues.push({
|
|
63
|
+
code: 'missing_environment',
|
|
64
|
+
message: 'Missing CODEX_CLOUD_ENV_ID (or target metadata.cloudEnvId).'
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const codexCheck = await runCommand(params.codexBin, ['--version'], {
|
|
68
|
+
cwd: params.repoRoot,
|
|
69
|
+
env: params.env
|
|
70
|
+
});
|
|
71
|
+
if (codexCheck.exitCode !== 0) {
|
|
72
|
+
issues.push({
|
|
73
|
+
code: 'codex_unavailable',
|
|
74
|
+
message: `Codex CLI is unavailable (${params.codexBin} --version failed).`
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (branch) {
|
|
78
|
+
const gitCheck = await runCommand('git', ['--version'], { cwd: params.repoRoot, env: params.env });
|
|
79
|
+
if (gitCheck.exitCode !== 0) {
|
|
80
|
+
issues.push({ code: 'git_unavailable', message: 'git is unavailable (required for cloud preflight).' });
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
const branchCheck = await runCommand('git', ['ls-remote', '--exit-code', '--heads', 'origin', branch], { cwd: params.repoRoot, env: params.env });
|
|
84
|
+
if (branchCheck.exitCode !== 0) {
|
|
85
|
+
issues.push({
|
|
86
|
+
code: 'branch_missing',
|
|
87
|
+
message: `Cloud branch '${branch}' was not found on origin. Push it first or set CODEX_CLOUD_BRANCH to an existing remote branch.`
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
ok: issues.length === 0,
|
|
94
|
+
issues,
|
|
95
|
+
details: {
|
|
96
|
+
codexBin: params.codexBin,
|
|
97
|
+
environmentId: params.environmentId,
|
|
98
|
+
branch
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
package/docs/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Codex Orchestrator is the coordination layer that glues together Codex-driven ag
|
|
|
25
25
|
|
|
26
26
|
## How It Works
|
|
27
27
|
- **Planner → Builder → Tester → Reviewer:** The core `TaskManager` (see `orchestrator/src/manager.ts`) wires together agent interfaces that decide *what* to run (planner), execute the selected pipeline stage (builder), verify results (tester), and give a final decision (reviewer).
|
|
28
|
-
- **Execution modes:** Each plan item can flag `requires_cloud` and task metadata can set `execution.parallel`; the mode policy picks `mcp` (local MCP runtime) or `cloud` execution accordingly.
|
|
28
|
+
- **Execution modes:** Each plan item can flag `requires_cloud` and task metadata can set `execution.parallel`; the mode policy picks `mcp` (local MCP runtime) or `cloud` execution accordingly. Cloud runs perform a quick preflight (env id, codex availability, optional remote branch) and fall back to `mcp` with a recorded summary when preflight fails.
|
|
29
29
|
- **Event-driven persistence:** Milestones emit typed events on `EventBus`. `PersistenceCoordinator` captures run summaries in the task state store and writes manifests so nothing is lost if the process crashes.
|
|
30
30
|
- **CLI lifecycle:** `CodexOrchestrator` (in `orchestrator/src/cli/orchestrator.ts`) resolves instruction sources (`AGENTS.md`, `docs/AGENTS.md`, `.agent/AGENTS.md`), loads the chosen pipeline, executes each command stage via `runCommandStage`, and keeps heartbeats plus command status current inside the manifest (approval evidence will surface once prompt wiring lands).
|
|
31
31
|
- **Control-plane & scheduler integrations:** Optional validation (`control-plane/`) and scheduling (`scheduler/`) modules enrich manifests with drift checks, plan assignments, and remote run metadata.
|
|
@@ -101,9 +101,9 @@ Use `npx @kbediako/codex-orchestrator resume --run <run-id>` to continue interru
|
|
|
101
101
|
## Companion Package Commands
|
|
102
102
|
- `codex-orchestrator mcp serve [--repo <path>] [--dry-run] [-- <extra args>]`: launch the MCP stdio server (delegates to `codex mcp-server`; stdout guard keeps protocol-only output, logs to stderr).
|
|
103
103
|
- `codex-orchestrator init codex [--cwd <path>] [--force]`: copy starter templates into a repo (includes `mcp-client.json` and `AGENTS.md`; no overwrite unless `--force`).
|
|
104
|
-
- `codex-orchestrator doctor [--format json]`: check optional tooling dependencies and print
|
|
104
|
+
- `codex-orchestrator doctor [--format json]`: check optional tooling dependencies plus collab/cloud/delegation readiness and print enablement commands.
|
|
105
105
|
- `codex-orchestrator devtools setup [--yes]`: print DevTools MCP setup instructions (`--yes` applies `codex mcp add ...`).
|
|
106
|
-
- `codex-orchestrator skills install [--force] [--codex-home <path>]`: install bundled skills into `$CODEX_HOME/skills` (global skills remain the primary reference when installed).
|
|
106
|
+
- `codex-orchestrator skills install [--force] [--only <skills>] [--codex-home <path>]`: install bundled skills into `$CODEX_HOME/skills` (global skills remain the primary reference when installed).
|
|
107
107
|
- `codex-orchestrator self-check --format json`: emit a safe JSON health payload for smoke tests.
|
|
108
108
|
- `codex-orchestrator --version`: print the package version.
|
|
109
109
|
|
package/package.json
CHANGED
|
@@ -137,6 +137,7 @@ Do not treat wrapper handoff-only output as a completed review.
|
|
|
137
137
|
|
|
138
138
|
- Symptoms: missing collab/delegate tool-call evidence, framing/parsing errors, or unstable collab behavior after CLI upgrades.
|
|
139
139
|
- Check versions first: `codex --version` and `codex-orchestrator --version`.
|
|
140
|
+
- Confirm feature readiness: `codex-orchestrator doctor` (checks collab/cloud/delegation readiness and prints enablement commands).
|
|
140
141
|
- CO repo refresh path (safe default): `scripts/codex-cli-refresh.sh --repo <codex-repo> --no-push`.
|
|
141
142
|
- Rebuild managed CLI only: `codex-orchestrator codex setup --source <codex-repo> --yes --force`.
|
|
142
143
|
- If local codex is materially behind upstream, sync before diagnosing collab behavior differences.
|