@kbediako/codex-orchestrator 0.1.21 → 0.1.23
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 +8 -2
- package/dist/bin/codex-orchestrator.js +96 -5
- 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/delegationSetup.js +239 -0
- package/dist/orchestrator/src/cli/doctor.js +211 -1
- package/dist/orchestrator/src/cli/doctorUsage.js +210 -0
- package/dist/orchestrator/src/cli/init.js +1 -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/utils/cloudPreflight.js +101 -0
- package/docs/README.md +3 -2
- package/package.json +1 -1
- package/skills/collab-subagents-first/SKILL.md +1 -0
- package/skills/delegation-usage/DELEGATION_GUIDE.md +2 -2
- package/skills/delegation-usage/SKILL.md +6 -4
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
|
|
@@ -152,11 +152,16 @@ Bundled skills (may vary by release):
|
|
|
152
152
|
|
|
153
153
|
## DevTools readiness
|
|
154
154
|
|
|
155
|
-
Check
|
|
155
|
+
Check readiness (deps + capability wiring):
|
|
156
156
|
```bash
|
|
157
157
|
codex-orchestrator doctor --format json
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
+
Usage snapshot (scans local `.runs/`):
|
|
161
|
+
```bash
|
|
162
|
+
codex-orchestrator doctor --usage
|
|
163
|
+
```
|
|
164
|
+
|
|
160
165
|
Print DevTools MCP setup guidance:
|
|
161
166
|
```bash
|
|
162
167
|
codex-orchestrator devtools setup
|
|
@@ -171,6 +176,7 @@ codex-orchestrator devtools setup
|
|
|
171
176
|
- `codex-orchestrator init codex --codex-cli --yes --codex-source <path>` — optionally provision a CO-managed Codex CLI binary (build-from-source default; set `CODEX_CLI_SOURCE` to avoid passing `--codex-source` every time).
|
|
172
177
|
- `codex-orchestrator init codex --codex-cli --yes --codex-download-url <url> --codex-download-sha256 <sha>` — opt-in to a prebuilt Codex CLI download.
|
|
173
178
|
- `codex-orchestrator codex setup` — plan/apply a CO-managed Codex CLI install (optional managed/pinned path; use `--download-url` + `--download-sha256` for prebuilts).
|
|
179
|
+
- `codex-orchestrator delegation setup --yes` — configure delegation MCP server wiring.
|
|
174
180
|
- `codex-orchestrator self-check --format json` — JSON health payload.
|
|
175
181
|
- `codex-orchestrator mcp serve` — Codex MCP stdio server.
|
|
176
182
|
|
|
@@ -14,8 +14,10 @@ import { evaluateInteractiveGate } from '../orchestrator/src/cli/utils/interacti
|
|
|
14
14
|
import { buildSelfCheckResult } from '../orchestrator/src/cli/selfCheck.js';
|
|
15
15
|
import { initCodexTemplates, formatInitSummary } from '../orchestrator/src/cli/init.js';
|
|
16
16
|
import { runDoctor, formatDoctorSummary } from '../orchestrator/src/cli/doctor.js';
|
|
17
|
+
import { formatDoctorUsageSummary, runDoctorUsage } from '../orchestrator/src/cli/doctorUsage.js';
|
|
17
18
|
import { formatDevtoolsSetupSummary, runDevtoolsSetup } from '../orchestrator/src/cli/devtoolsSetup.js';
|
|
18
19
|
import { formatCodexCliSetupSummary, runCodexCliSetup } from '../orchestrator/src/cli/codexCliSetup.js';
|
|
20
|
+
import { formatDelegationSetupSummary, runDelegationSetup } from '../orchestrator/src/cli/delegationSetup.js';
|
|
19
21
|
import { formatSkillsInstallSummary, installSkills } from '../orchestrator/src/cli/skills.js';
|
|
20
22
|
import { loadPackageInfo } from '../orchestrator/src/cli/utils/packageInfo.js';
|
|
21
23
|
import { slugify } from '../orchestrator/src/cli/utils/strings.js';
|
|
@@ -85,6 +87,9 @@ async function main() {
|
|
|
85
87
|
case 'delegation-server':
|
|
86
88
|
await handleDelegationServer(args);
|
|
87
89
|
break;
|
|
90
|
+
case 'delegation':
|
|
91
|
+
await handleDelegation(args);
|
|
92
|
+
break;
|
|
88
93
|
case 'version':
|
|
89
94
|
printVersion();
|
|
90
95
|
break;
|
|
@@ -167,6 +172,25 @@ function applyRlmEnvOverrides(flags, goal) {
|
|
|
167
172
|
if (goal) {
|
|
168
173
|
process.env.RLM_GOAL = goal;
|
|
169
174
|
}
|
|
175
|
+
const collabRaw = flags['collab'];
|
|
176
|
+
if (collabRaw !== undefined) {
|
|
177
|
+
const normalized = typeof collabRaw === 'string' ? collabRaw.trim().toLowerCase() : collabRaw === true ? 'true' : '';
|
|
178
|
+
const enabled = collabRaw === true || normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'auto';
|
|
179
|
+
const disabled = normalized === '0' || normalized === 'false' || normalized === 'no' || normalized === 'off';
|
|
180
|
+
if (enabled) {
|
|
181
|
+
process.env.RLM_SYMBOLIC_COLLAB = '1';
|
|
182
|
+
// Collab is only used in the symbolic loop; make the flag do what users expect.
|
|
183
|
+
if (!process.env.RLM_MODE) {
|
|
184
|
+
process.env.RLM_MODE = 'symbolic';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else if (disabled) {
|
|
188
|
+
process.env.RLM_SYMBOLIC_COLLAB = '0';
|
|
189
|
+
}
|
|
190
|
+
else if (typeof collabRaw === 'string') {
|
|
191
|
+
throw new Error('Invalid --collab value. Use --collab (or: --collab auto|true|false).');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
170
194
|
const validator = readStringFlag(flags, 'validator');
|
|
171
195
|
if (validator) {
|
|
172
196
|
process.env.RLM_VALIDATOR = validator;
|
|
@@ -313,6 +337,16 @@ async function handleRlm(orchestrator, rawArgs) {
|
|
|
313
337
|
process.env.MCP_RUNNER_TASK_ID = taskId;
|
|
314
338
|
applyRlmEnvOverrides(flags, goal);
|
|
315
339
|
console.log(`Task: ${taskId}`);
|
|
340
|
+
const collabUserChoice = flags['collab'] !== undefined || process.env.RLM_SYMBOLIC_COLLAB !== undefined;
|
|
341
|
+
if (!collabUserChoice) {
|
|
342
|
+
const doctor = runDoctor();
|
|
343
|
+
if (doctor.collab.status === 'ok') {
|
|
344
|
+
console.log('Tip: collab is enabled. Try: codex-orchestrator rlm --collab auto \"<goal>\"');
|
|
345
|
+
}
|
|
346
|
+
else if (doctor.collab.status === 'disabled') {
|
|
347
|
+
console.log('Tip: collab is available but disabled. Enable with: codex features enable collab');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
316
350
|
let startResult = null;
|
|
317
351
|
await withRunUi(flags, 'text', async (runEvents) => {
|
|
318
352
|
startResult = await orchestrator.start({
|
|
@@ -525,15 +559,38 @@ async function handleInit(rawArgs) {
|
|
|
525
559
|
async function handleDoctor(rawArgs) {
|
|
526
560
|
const { flags } = parseArgs(rawArgs);
|
|
527
561
|
const format = flags['format'] === 'json' ? 'json' : 'text';
|
|
528
|
-
const
|
|
562
|
+
const includeUsage = Boolean(flags['usage']);
|
|
563
|
+
const windowDaysRaw = readStringFlag(flags, 'window-days');
|
|
564
|
+
let windowDays = undefined;
|
|
565
|
+
if (windowDaysRaw) {
|
|
566
|
+
if (!/^\d+$/u.test(windowDaysRaw)) {
|
|
567
|
+
throw new Error(`Invalid --window-days value '${windowDaysRaw}'. Expected a positive integer.`);
|
|
568
|
+
}
|
|
569
|
+
const parsed = Number(windowDaysRaw);
|
|
570
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
571
|
+
throw new Error(`Invalid --window-days value '${windowDaysRaw}'. Expected a positive integer.`);
|
|
572
|
+
}
|
|
573
|
+
windowDays = parsed;
|
|
574
|
+
}
|
|
575
|
+
const taskFilter = readStringFlag(flags, 'task') ?? null;
|
|
576
|
+
const doctorResult = runDoctor();
|
|
577
|
+
const usageResult = includeUsage ? await runDoctorUsage({ windowDays, taskFilter }) : null;
|
|
529
578
|
if (format === 'json') {
|
|
530
|
-
|
|
579
|
+
if (usageResult) {
|
|
580
|
+
console.log(JSON.stringify({ ...doctorResult, usage: usageResult }, null, 2));
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
console.log(JSON.stringify(doctorResult, null, 2));
|
|
531
584
|
return;
|
|
532
585
|
}
|
|
533
|
-
const
|
|
534
|
-
for (const line of summary) {
|
|
586
|
+
for (const line of formatDoctorSummary(doctorResult)) {
|
|
535
587
|
console.log(line);
|
|
536
588
|
}
|
|
589
|
+
if (usageResult) {
|
|
590
|
+
for (const line of formatDoctorUsageSummary(usageResult)) {
|
|
591
|
+
console.log(line);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
537
594
|
}
|
|
538
595
|
async function handleDevtools(rawArgs) {
|
|
539
596
|
const { positionals, flags } = parseArgs(rawArgs);
|
|
@@ -559,6 +616,30 @@ async function handleDevtools(rawArgs) {
|
|
|
559
616
|
console.log(line);
|
|
560
617
|
}
|
|
561
618
|
}
|
|
619
|
+
async function handleDelegation(rawArgs) {
|
|
620
|
+
const { positionals, flags } = parseArgs(rawArgs);
|
|
621
|
+
const subcommand = positionals.shift();
|
|
622
|
+
if (!subcommand) {
|
|
623
|
+
throw new Error('delegation requires a subcommand (setup).');
|
|
624
|
+
}
|
|
625
|
+
if (subcommand !== 'setup') {
|
|
626
|
+
throw new Error(`Unknown delegation subcommand: ${subcommand}`);
|
|
627
|
+
}
|
|
628
|
+
const format = flags['format'] === 'json' ? 'json' : 'text';
|
|
629
|
+
const apply = Boolean(flags['yes']);
|
|
630
|
+
if (format === 'json' && apply) {
|
|
631
|
+
throw new Error('delegation setup does not support --format json with --yes.');
|
|
632
|
+
}
|
|
633
|
+
const repoRoot = readStringFlag(flags, 'repo') ?? process.cwd();
|
|
634
|
+
const result = await runDelegationSetup({ apply, repoRoot });
|
|
635
|
+
if (format === 'json') {
|
|
636
|
+
console.log(JSON.stringify(result, null, 2));
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
for (const line of formatDelegationSetupSummary(result)) {
|
|
640
|
+
console.log(line);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
562
643
|
async function handleCodex(rawArgs) {
|
|
563
644
|
const { positionals, flags } = parseArgs(rawArgs);
|
|
564
645
|
const subcommand = positionals.shift();
|
|
@@ -879,6 +960,7 @@ Commands:
|
|
|
879
960
|
--cloud Shortcut for --execution-mode cloud.
|
|
880
961
|
--target <stage-id> Focus plan/build metadata on a specific stage (alias: --target-stage).
|
|
881
962
|
--goal "<goal>" When pipeline is rlm, set the RLM goal.
|
|
963
|
+
--collab [auto|true|false] When pipeline is rlm, enable collab subagents (implies symbolic mode).
|
|
882
964
|
--validator <cmd|none> When pipeline is rlm, set the validator command.
|
|
883
965
|
--max-iterations <n> When pipeline is rlm, override max iterations.
|
|
884
966
|
--max-minutes <n> When pipeline is rlm, override max minutes.
|
|
@@ -888,6 +970,7 @@ Commands:
|
|
|
888
970
|
|
|
889
971
|
rlm "<goal>" Run RLM loop until validator passes.
|
|
890
972
|
--task <id> Override task identifier.
|
|
973
|
+
--collab [auto|true|false] Enable collab subagents (implies symbolic mode).
|
|
891
974
|
--validator <cmd|none> Set validator command or disable validation.
|
|
892
975
|
--max-iterations <n> Override max iterations (0 = unlimited with validator).
|
|
893
976
|
--max-minutes <n> Optional time-based guardrail in minutes.
|
|
@@ -940,7 +1023,11 @@ Commands:
|
|
|
940
1023
|
--codex-download-sha256 <sha> Expected SHA256 for the prebuilt download.
|
|
941
1024
|
--codex-force Overwrite existing CO-managed codex binary.
|
|
942
1025
|
--yes Apply codex CLI setup (otherwise plan only).
|
|
943
|
-
doctor [--format json]
|
|
1026
|
+
doctor [--format json] [--usage] [--window-days <n>] [--task <id>]
|
|
1027
|
+
--usage Include a local usage snapshot (scans .runs/).
|
|
1028
|
+
--window-days <n> Window for --usage (default 30).
|
|
1029
|
+
--task <id> Limit --usage scan to a specific task directory.
|
|
1030
|
+
--format json Emit machine-readable output.
|
|
944
1031
|
codex setup
|
|
945
1032
|
--source <path> Build from local Codex repo (or git URL).
|
|
946
1033
|
--ref <ref> Git ref (branch/tag/sha) when building from repo.
|
|
@@ -952,6 +1039,10 @@ Commands:
|
|
|
952
1039
|
devtools setup Print DevTools MCP setup instructions.
|
|
953
1040
|
--yes Apply setup by running "codex mcp add ...".
|
|
954
1041
|
--format json Emit machine-readable output (dry-run only).
|
|
1042
|
+
delegation setup Configure delegation MCP server wiring.
|
|
1043
|
+
--repo <path> Repo root for delegation server (default cwd).
|
|
1044
|
+
--yes Apply setup by running "codex mcp add ...".
|
|
1045
|
+
--format json Emit machine-readable output (dry-run only).
|
|
955
1046
|
skills install Install bundled skills into $CODEX_HOME/skills.
|
|
956
1047
|
--force Overwrite existing skill files.
|
|
957
1048
|
--only <skills> Install only selected skills (comma-separated).
|
|
@@ -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({
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { resolveCodexCliBin } from './utils/codexCli.js';
|
|
6
|
+
import { resolveCodexHome } from './utils/codexPaths.js';
|
|
7
|
+
export async function runDelegationSetup(options = {}) {
|
|
8
|
+
const env = options.env ?? process.env;
|
|
9
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
10
|
+
const codexBin = resolveCodexCliBin(env);
|
|
11
|
+
const codexHome = resolveCodexHome(env);
|
|
12
|
+
const configPath = join(codexHome, 'config.toml');
|
|
13
|
+
const plan = {
|
|
14
|
+
codexBin,
|
|
15
|
+
codexHome,
|
|
16
|
+
repoRoot,
|
|
17
|
+
commandLine: `"${codexBin}" mcp add delegation -- codex-orchestrator delegate-server`
|
|
18
|
+
};
|
|
19
|
+
const probe = inspectDelegationReadiness({ codexBin, configPath, repoRoot, env });
|
|
20
|
+
const readiness = { configured: probe.configured, configPath };
|
|
21
|
+
if (!options.apply) {
|
|
22
|
+
return { status: 'planned', plan, readiness };
|
|
23
|
+
}
|
|
24
|
+
if (probe.configured) {
|
|
25
|
+
return { status: 'skipped', reason: probe.reason ?? 'Delegation MCP is already configured.', plan, readiness };
|
|
26
|
+
}
|
|
27
|
+
await applyDelegationSetup({ codexBin, removeExisting: probe.removeExisting, envVars: probe.envVars }, env);
|
|
28
|
+
const configuredAfter = inspectDelegationReadiness({ codexBin, configPath, repoRoot, env }).configured;
|
|
29
|
+
return {
|
|
30
|
+
status: 'applied',
|
|
31
|
+
reason: probe.reason,
|
|
32
|
+
plan,
|
|
33
|
+
readiness: { ...readiness, configured: configuredAfter }
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function formatDelegationSetupSummary(result) {
|
|
37
|
+
const lines = [];
|
|
38
|
+
lines.push(`Delegation setup: ${result.status}`);
|
|
39
|
+
if (result.reason) {
|
|
40
|
+
lines.push(`Note: ${result.reason}`);
|
|
41
|
+
}
|
|
42
|
+
lines.push(`- Codex home: ${result.plan.codexHome}`);
|
|
43
|
+
lines.push(`- Config: ${result.readiness.configured ? 'ok' : 'missing'} (${result.readiness.configPath})`);
|
|
44
|
+
lines.push(`- Command: ${result.plan.commandLine}`);
|
|
45
|
+
if (result.status === 'planned') {
|
|
46
|
+
lines.push('Run with --yes to apply this setup.');
|
|
47
|
+
}
|
|
48
|
+
return lines;
|
|
49
|
+
}
|
|
50
|
+
function inspectDelegationReadiness(options) {
|
|
51
|
+
const requestedRepo = resolve(options.repoRoot);
|
|
52
|
+
const existing = readDelegationMcpServer(options.codexBin, options.env);
|
|
53
|
+
if (existing) {
|
|
54
|
+
const envVars = existing.envVars;
|
|
55
|
+
const isDelegationServer = existing.args.includes('delegate-server') || existing.args.includes('delegation-server');
|
|
56
|
+
if (!isDelegationServer) {
|
|
57
|
+
return {
|
|
58
|
+
configured: false,
|
|
59
|
+
removeExisting: true,
|
|
60
|
+
envVars,
|
|
61
|
+
reason: 'Existing delegation MCP entry does not point to codex-orchestrator delegate-server; reconfiguring.'
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (existing.pinnedRepo) {
|
|
65
|
+
const pinnedRepo = resolve(existing.pinnedRepo);
|
|
66
|
+
if (pinnedRepo !== requestedRepo) {
|
|
67
|
+
return {
|
|
68
|
+
configured: false,
|
|
69
|
+
removeExisting: true,
|
|
70
|
+
envVars,
|
|
71
|
+
reason: `Existing delegation MCP entry is pinned to ${existing.pinnedRepo}; reconfiguring.`
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
configured: true,
|
|
76
|
+
removeExisting: false,
|
|
77
|
+
envVars,
|
|
78
|
+
reason: `Delegation MCP is already configured (pinned to ${existing.pinnedRepo}).`
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
configured: true,
|
|
83
|
+
removeExisting: false,
|
|
84
|
+
envVars,
|
|
85
|
+
reason: 'Delegation MCP is already configured.'
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// Fall back to directly scanning config.toml when the Codex CLI probe is unavailable.
|
|
89
|
+
const configured = isDelegationConfiguredFallback(options.configPath);
|
|
90
|
+
return { configured, removeExisting: false, envVars: {} };
|
|
91
|
+
}
|
|
92
|
+
function applyDelegationSetup(plan, env) {
|
|
93
|
+
const envFlags = [];
|
|
94
|
+
for (const [key, value] of Object.entries(plan.envVars ?? {})) {
|
|
95
|
+
envFlags.push('--env', `${key}=${value}`);
|
|
96
|
+
}
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const runAdd = () => {
|
|
99
|
+
const child = spawn(plan.codexBin, ['mcp', 'add', 'delegation', ...envFlags, '--', 'codex-orchestrator', 'delegate-server'], { stdio: 'inherit', env });
|
|
100
|
+
child.once('error', (error) => reject(error instanceof Error ? error : new Error(String(error))));
|
|
101
|
+
child.once('exit', (code) => {
|
|
102
|
+
if (code === 0) {
|
|
103
|
+
resolve();
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
reject(new Error(`codex mcp add exited with code ${code ?? 'unknown'}`));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
if (!plan.removeExisting) {
|
|
111
|
+
runAdd();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const child = spawn(plan.codexBin, ['mcp', 'remove', 'delegation'], { stdio: 'inherit', env });
|
|
115
|
+
child.once('error', (error) => reject(error instanceof Error ? error : new Error(String(error))));
|
|
116
|
+
child.once('exit', (code) => {
|
|
117
|
+
if (code === 0) {
|
|
118
|
+
runAdd();
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
reject(new Error(`codex mcp remove exited with code ${code ?? 'unknown'}`));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function readDelegationMcpServer(codexBin, env) {
|
|
127
|
+
const result = spawnSync(codexBin, ['mcp', 'get', 'delegation', '--json'], {
|
|
128
|
+
encoding: 'utf8',
|
|
129
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
130
|
+
timeout: 5000,
|
|
131
|
+
env
|
|
132
|
+
});
|
|
133
|
+
if (result.error || result.status !== 0) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const stdout = String(result.stdout ?? '').trim();
|
|
137
|
+
if (!stdout) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(stdout);
|
|
142
|
+
const transport = parsed.transport;
|
|
143
|
+
const args = Array.isArray(transport?.args)
|
|
144
|
+
? transport.args.filter((value) => typeof value === 'string')
|
|
145
|
+
: [];
|
|
146
|
+
const envVars = {};
|
|
147
|
+
const envRecord = transport?.env;
|
|
148
|
+
if (envRecord && typeof envRecord === 'object' && !Array.isArray(envRecord)) {
|
|
149
|
+
for (const [key, value] of Object.entries(envRecord)) {
|
|
150
|
+
if (typeof value === 'string') {
|
|
151
|
+
envVars[key] = value;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const pinnedRepo = readPinnedRepo(args);
|
|
156
|
+
return { args, pinnedRepo, envVars };
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function readPinnedRepo(args) {
|
|
163
|
+
const index = args.indexOf('--repo');
|
|
164
|
+
if (index === -1) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const candidate = args[index + 1];
|
|
168
|
+
return typeof candidate === 'string' && candidate.trim().length > 0 ? candidate.trim() : null;
|
|
169
|
+
}
|
|
170
|
+
function isDelegationConfiguredFallback(configPath) {
|
|
171
|
+
if (!existsSync(configPath)) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
// Keep parsing loose; we only need to know whether a delegation entry exists.
|
|
176
|
+
const raw = readFileSync(configPath, 'utf8');
|
|
177
|
+
return hasMcpServerEntry(raw, 'delegation');
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function hasMcpServerEntry(raw, serverName) {
|
|
184
|
+
const lines = raw.split('\n');
|
|
185
|
+
let currentTable = null;
|
|
186
|
+
for (const line of lines) {
|
|
187
|
+
const trimmed = stripTomlComment(line).trim();
|
|
188
|
+
if (!trimmed) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const tableMatch = trimmed.match(/^\[(.+)\]$/u);
|
|
192
|
+
if (tableMatch) {
|
|
193
|
+
currentTable = tableMatch[1]?.trim() ?? null;
|
|
194
|
+
if (currentTable === `mcp_servers.${serverName}` ||
|
|
195
|
+
currentTable === `mcp_servers."${serverName}"` ||
|
|
196
|
+
currentTable === `mcp_servers.'${serverName}'`) {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (trimmed.startsWith('mcp_servers.')) {
|
|
202
|
+
if (trimmed.startsWith(`mcp_servers."${serverName}".`)) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
if (trimmed.startsWith(`mcp_servers.'${serverName}'.`)) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
if (trimmed.startsWith(`mcp_servers.${serverName}.`)) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
if (trimmed.startsWith(`mcp_servers."${serverName}"=`)) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
if (trimmed.startsWith(`mcp_servers.'${serverName}'=`)) {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
if (trimmed.startsWith(`mcp_servers.${serverName}=`)) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (currentTable === 'mcp_servers') {
|
|
222
|
+
const entryPattern = new RegExp(`^"?${escapeRegExp(serverName)}"?\\s*=`, 'u');
|
|
223
|
+
if (entryPattern.test(trimmed)) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
function stripTomlComment(line) {
|
|
231
|
+
const index = line.indexOf('#');
|
|
232
|
+
if (index === -1) {
|
|
233
|
+
return line;
|
|
234
|
+
}
|
|
235
|
+
return line.slice(0, index);
|
|
236
|
+
}
|
|
237
|
+
function escapeRegExp(value) {
|
|
238
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
239
|
+
}
|
|
@@ -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,57 @@ 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-orchestrator delegation setup --yes',
|
|
119
|
+
'Or manually: codex mcp add delegation -- codex-orchestrator delegate-server',
|
|
120
|
+
"Enable for a run with: codex -c 'mcp_servers.delegation.enabled=true' ...",
|
|
121
|
+
'See: codex-orchestrator init codex'
|
|
122
|
+
]
|
|
123
|
+
}
|
|
73
124
|
};
|
|
74
125
|
}
|
|
75
126
|
export function formatDoctorSummary(result) {
|
|
@@ -118,5 +169,164 @@ export function formatDoctorSummary(result) {
|
|
|
118
169
|
for (const line of result.devtools.enablement) {
|
|
119
170
|
lines.push(` - ${line}`);
|
|
120
171
|
}
|
|
172
|
+
lines.push(`Codex CLI: ${result.codex_cli.active.command}`);
|
|
173
|
+
lines.push(` - managed: ${result.codex_cli.managed.status} (${result.codex_cli.managed.config.path})`);
|
|
174
|
+
if (result.codex_cli.managed.status === 'invalid' && result.codex_cli.managed.config.error) {
|
|
175
|
+
lines.push(` error: ${result.codex_cli.managed.config.error}`);
|
|
176
|
+
}
|
|
177
|
+
if (result.codex_cli.managed.status === 'ok') {
|
|
178
|
+
lines.push(` - binary: ${result.codex_cli.managed.binary.status} (${result.codex_cli.managed.binary.path})`);
|
|
179
|
+
if (result.codex_cli.managed.install?.version) {
|
|
180
|
+
lines.push(` - version: ${result.codex_cli.managed.install.version}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
lines.push(`Collab: ${result.collab.status}`);
|
|
184
|
+
if (result.collab.enabled !== null) {
|
|
185
|
+
lines.push(` - enabled: ${result.collab.enabled}`);
|
|
186
|
+
}
|
|
187
|
+
for (const line of result.collab.enablement) {
|
|
188
|
+
lines.push(` - ${line}`);
|
|
189
|
+
}
|
|
190
|
+
lines.push(`Cloud: ${result.cloud.status}`);
|
|
191
|
+
lines.push(` - CODEX_CLOUD_ENV_ID: ${result.cloud.env_id_configured ? 'set' : 'missing'}`);
|
|
192
|
+
lines.push(` - CODEX_CLOUD_BRANCH: ${result.cloud.branch ?? '<unset>'}`);
|
|
193
|
+
for (const line of result.cloud.enablement) {
|
|
194
|
+
lines.push(` - ${line}`);
|
|
195
|
+
}
|
|
196
|
+
lines.push(`Delegation: ${result.delegation.status}`);
|
|
197
|
+
const delegationConfigLabel = result.delegation.config.status === 'ok'
|
|
198
|
+
? `ok (${result.delegation.config.path})`
|
|
199
|
+
: `missing (${result.delegation.config.path})`;
|
|
200
|
+
lines.push(` - config.toml: ${delegationConfigLabel}`);
|
|
201
|
+
if (result.delegation.config.detail) {
|
|
202
|
+
lines.push(` detail: ${result.delegation.config.detail}`);
|
|
203
|
+
}
|
|
204
|
+
for (const line of result.delegation.enablement) {
|
|
205
|
+
lines.push(` - ${line}`);
|
|
206
|
+
}
|
|
121
207
|
return lines;
|
|
122
208
|
}
|
|
209
|
+
function readCodexFeatureFlags(codexBin) {
|
|
210
|
+
const result = spawnSync(codexBin, ['features', 'list'], {
|
|
211
|
+
encoding: 'utf8',
|
|
212
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
213
|
+
timeout: 5000
|
|
214
|
+
});
|
|
215
|
+
if (result.error || result.status !== 0) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const stdout = String(result.stdout ?? '');
|
|
219
|
+
const flags = {};
|
|
220
|
+
for (const line of stdout.split(/\r?\n/u)) {
|
|
221
|
+
const trimmed = line.trim();
|
|
222
|
+
if (!trimmed) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const tokens = trimmed.split(/\s+/u);
|
|
226
|
+
if (tokens.length < 2) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const name = tokens[0] ?? '';
|
|
230
|
+
const enabledToken = tokens[tokens.length - 1] ?? '';
|
|
231
|
+
if (!name) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (enabledToken === 'true') {
|
|
235
|
+
flags[name] = true;
|
|
236
|
+
}
|
|
237
|
+
else if (enabledToken === 'false') {
|
|
238
|
+
flags[name] = false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return flags;
|
|
242
|
+
}
|
|
243
|
+
function canRunCommand(command, args) {
|
|
244
|
+
const result = spawnSync(command, args, {
|
|
245
|
+
encoding: 'utf8',
|
|
246
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
247
|
+
timeout: 5000
|
|
248
|
+
});
|
|
249
|
+
if (result.error) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
return result.status === 0;
|
|
253
|
+
}
|
|
254
|
+
function inspectDelegationConfig(env = process.env) {
|
|
255
|
+
const codexHome = resolveCodexHome(env);
|
|
256
|
+
const configPath = join(codexHome, 'config.toml');
|
|
257
|
+
if (!existsSync(configPath)) {
|
|
258
|
+
return { status: 'missing', path: configPath, detail: 'config.toml not found' };
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
const raw = readFileSync(configPath, 'utf8');
|
|
262
|
+
const hasEntry = hasMcpServerEntry(raw, 'delegation');
|
|
263
|
+
if (hasEntry) {
|
|
264
|
+
return { status: 'ok', path: configPath };
|
|
265
|
+
}
|
|
266
|
+
return { status: 'missing', path: configPath, detail: 'mcp_servers.delegation entry not found' };
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
return {
|
|
270
|
+
status: 'missing',
|
|
271
|
+
path: configPath,
|
|
272
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function hasMcpServerEntry(raw, serverName) {
|
|
277
|
+
const lines = raw.split('\n');
|
|
278
|
+
let currentTable = null;
|
|
279
|
+
for (const line of lines) {
|
|
280
|
+
const trimmed = stripTomlComment(line).trim();
|
|
281
|
+
if (!trimmed) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const tableMatch = trimmed.match(/^\[(.+)\]$/u);
|
|
285
|
+
if (tableMatch) {
|
|
286
|
+
currentTable = tableMatch[1]?.trim() ?? null;
|
|
287
|
+
if (currentTable === `mcp_servers.${serverName}` ||
|
|
288
|
+
currentTable === `mcp_servers."${serverName}"` ||
|
|
289
|
+
currentTable === `mcp_servers.'${serverName}'`) {
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (trimmed.startsWith('mcp_servers.')) {
|
|
295
|
+
if (trimmed.startsWith(`mcp_servers."${serverName}".`)) {
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
if (trimmed.startsWith(`mcp_servers.'${serverName}'.`)) {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
if (trimmed.startsWith(`mcp_servers.${serverName}.`)) {
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
if (trimmed.startsWith(`mcp_servers."${serverName}"=`)) {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
if (trimmed.startsWith(`mcp_servers.'${serverName}'=`)) {
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
if (trimmed.startsWith(`mcp_servers.${serverName}=`)) {
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (currentTable === 'mcp_servers') {
|
|
315
|
+
const entryPattern = new RegExp(`^"?${escapeRegExp(serverName)}"?\\s*=`, 'u');
|
|
316
|
+
if (entryPattern.test(trimmed)) {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
function stripTomlComment(line) {
|
|
324
|
+
const index = line.indexOf('#');
|
|
325
|
+
if (index === -1) {
|
|
326
|
+
return line;
|
|
327
|
+
}
|
|
328
|
+
return line.slice(0, index);
|
|
329
|
+
}
|
|
330
|
+
function escapeRegExp(value) {
|
|
331
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
332
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, join } from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { collectManifests, findSubagentManifests, parseRunIdTimestamp, resolveEnvironmentPaths } from '../../../scripts/lib/run-manifests.js';
|
|
6
|
+
import { normalizeTaskKey as normalizeTaskKeyAny } from '../../../scripts/lib/docs-helpers.js';
|
|
7
|
+
const normalizeTaskKey = normalizeTaskKeyAny;
|
|
8
|
+
export async function runDoctorUsage(options = {}) {
|
|
9
|
+
const windowDays = clampInt(options.windowDays ?? 30, 1, 3650);
|
|
10
|
+
const cutoffMs = Date.now() - windowDays * 24 * 60 * 60 * 1000;
|
|
11
|
+
const cutoffIso = new Date(cutoffMs).toISOString();
|
|
12
|
+
const env = resolveEnvironmentPaths();
|
|
13
|
+
const manifestPaths = await collectManifests(env.runsRoot, options.taskFilter ?? undefined);
|
|
14
|
+
const seenRunIds = new Set();
|
|
15
|
+
const pipelines = new Map();
|
|
16
|
+
const cloudByStatus = {};
|
|
17
|
+
const statusCounts = { total: 0, succeeded: 0, failed: 0, cancelled: 0, other: 0 };
|
|
18
|
+
let cloudRuns = 0;
|
|
19
|
+
let rlmRuns = 0;
|
|
20
|
+
let collabRunsWithToolCalls = 0;
|
|
21
|
+
let collabTotalToolCalls = 0;
|
|
22
|
+
const collabCaptureDisabled = String(process.env.CODEX_ORCHESTRATOR_COLLAB_MAX_EVENTS ?? '').trim() === '0';
|
|
23
|
+
const activeIndexTasks = new Set();
|
|
24
|
+
const taskKeys = readTaskIndexKeys(env.repoRoot);
|
|
25
|
+
for (const manifestPath of manifestPaths) {
|
|
26
|
+
const runIdFromPath = extractRunIdFromManifestPath(manifestPath);
|
|
27
|
+
if (!runIdFromPath) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (seenRunIds.has(runIdFromPath)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const timestamp = parseRunIdTimestamp(runIdFromPath);
|
|
34
|
+
if (timestamp && timestamp.getTime() < cutoffMs) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
let manifest;
|
|
38
|
+
try {
|
|
39
|
+
const raw = await readFile(manifestPath, 'utf8');
|
|
40
|
+
manifest = JSON.parse(raw);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const runId = typeof manifest.run_id === 'string' && manifest.run_id ? manifest.run_id : runIdFromPath;
|
|
46
|
+
if (seenRunIds.has(runId)) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
seenRunIds.add(runId);
|
|
50
|
+
const startedAtMs = Date.parse(manifest.started_at ?? '') || timestamp?.getTime() || 0;
|
|
51
|
+
if (!startedAtMs || startedAtMs < cutoffMs) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
statusCounts.total += 1;
|
|
55
|
+
if (manifest.status === 'succeeded') {
|
|
56
|
+
statusCounts.succeeded += 1;
|
|
57
|
+
}
|
|
58
|
+
else if (manifest.status === 'failed') {
|
|
59
|
+
statusCounts.failed += 1;
|
|
60
|
+
}
|
|
61
|
+
else if (manifest.status === 'cancelled') {
|
|
62
|
+
statusCounts.cancelled += 1;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
statusCounts.other += 1;
|
|
66
|
+
}
|
|
67
|
+
const pipelineId = typeof manifest.pipeline_id === 'string' && manifest.pipeline_id ? manifest.pipeline_id : 'unknown';
|
|
68
|
+
pipelines.set(pipelineId, (pipelines.get(pipelineId) ?? 0) + 1);
|
|
69
|
+
if (pipelineId === 'rlm') {
|
|
70
|
+
rlmRuns += 1;
|
|
71
|
+
}
|
|
72
|
+
if (manifest.cloud_execution) {
|
|
73
|
+
cloudRuns += 1;
|
|
74
|
+
const status = (manifest.cloud_execution.status ?? 'unknown').trim() || 'unknown';
|
|
75
|
+
cloudByStatus[status] = (cloudByStatus[status] ?? 0) + 1;
|
|
76
|
+
}
|
|
77
|
+
if (Array.isArray(manifest.collab_tool_calls) && manifest.collab_tool_calls.length > 0) {
|
|
78
|
+
collabRunsWithToolCalls += 1;
|
|
79
|
+
collabTotalToolCalls += manifest.collab_tool_calls.length;
|
|
80
|
+
}
|
|
81
|
+
if (taskKeys.has(manifest.task_id)) {
|
|
82
|
+
activeIndexTasks.add(manifest.task_id);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const pipelineTop = [...pipelines.entries()]
|
|
86
|
+
.sort((a, b) => b[1] - a[1])
|
|
87
|
+
.slice(0, 10)
|
|
88
|
+
.map(([id, runs]) => ({ id, runs }));
|
|
89
|
+
const delegationErrors = [];
|
|
90
|
+
let activeWithSubagents = 0;
|
|
91
|
+
let totalSubagentManifests = 0;
|
|
92
|
+
const activeTasks = [...activeIndexTasks];
|
|
93
|
+
const subagentResults = await Promise.all(activeTasks.map(async (taskId) => {
|
|
94
|
+
const result = await findSubagentManifests(env.runsRoot, taskId);
|
|
95
|
+
if (result.error) {
|
|
96
|
+
delegationErrors.push(result.error);
|
|
97
|
+
}
|
|
98
|
+
return { taskId, found: result.found };
|
|
99
|
+
}));
|
|
100
|
+
for (const item of subagentResults) {
|
|
101
|
+
totalSubagentManifests += item.found.length;
|
|
102
|
+
if (item.found.length > 0) {
|
|
103
|
+
activeWithSubagents += 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
window_days: windowDays,
|
|
108
|
+
cutoff_iso: cutoffIso,
|
|
109
|
+
runs: statusCounts,
|
|
110
|
+
cloud: {
|
|
111
|
+
runs: cloudRuns,
|
|
112
|
+
by_status: cloudByStatus
|
|
113
|
+
},
|
|
114
|
+
rlm: {
|
|
115
|
+
runs: rlmRuns
|
|
116
|
+
},
|
|
117
|
+
collab: {
|
|
118
|
+
runs_with_tool_calls: collabRunsWithToolCalls,
|
|
119
|
+
total_tool_calls: collabTotalToolCalls,
|
|
120
|
+
capture_disabled: collabCaptureDisabled
|
|
121
|
+
},
|
|
122
|
+
delegation: {
|
|
123
|
+
active_top_level_tasks: activeTasks.length,
|
|
124
|
+
active_with_subagents: activeWithSubagents,
|
|
125
|
+
total_subagent_manifests: totalSubagentManifests,
|
|
126
|
+
errors: delegationErrors
|
|
127
|
+
},
|
|
128
|
+
pipelines: {
|
|
129
|
+
total: pipelines.size,
|
|
130
|
+
top: pipelineTop
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export function formatDoctorUsageSummary(result) {
|
|
135
|
+
const lines = [];
|
|
136
|
+
lines.push(`Usage (last ${result.window_days}d, cutoff ${result.cutoff_iso})`);
|
|
137
|
+
lines.push(` - runs: ${result.runs.total} (ok=${result.runs.succeeded}, failed=${result.runs.failed}, cancelled=${result.runs.cancelled}, other=${result.runs.other})`);
|
|
138
|
+
lines.push(` - cloud: ${result.cloud.runs} (${formatPercent(result.cloud.runs, result.runs.total)})${formatCloudStatuses(result.cloud.by_status)}`);
|
|
139
|
+
lines.push(` - rlm: ${result.rlm.runs} (${formatPercent(result.rlm.runs, result.runs.total)})`);
|
|
140
|
+
const collabSuffix = result.collab.capture_disabled ? ' (capture disabled)' : '';
|
|
141
|
+
lines.push(` - collab: ${result.collab.runs_with_tool_calls} (${formatPercent(result.collab.runs_with_tool_calls, result.runs.total)})${collabSuffix}`);
|
|
142
|
+
if (result.delegation.active_top_level_tasks > 0) {
|
|
143
|
+
lines.push(` - delegation: ${result.delegation.active_with_subagents}/${result.delegation.active_top_level_tasks} top-level tasks have subagent manifests`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
lines.push(' - delegation: no top-level tasks detected in tasks/index.json for this window');
|
|
147
|
+
}
|
|
148
|
+
if (result.pipelines.top.length > 0) {
|
|
149
|
+
lines.push('Top pipelines:');
|
|
150
|
+
for (const entry of result.pipelines.top) {
|
|
151
|
+
lines.push(` - ${entry.id}: ${entry.runs}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (result.delegation.errors.length > 0) {
|
|
155
|
+
lines.push('Delegation scan warnings:');
|
|
156
|
+
for (const warning of result.delegation.errors.slice(0, 3)) {
|
|
157
|
+
lines.push(` - ${warning}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return lines;
|
|
161
|
+
}
|
|
162
|
+
function extractRunIdFromManifestPath(manifestPath) {
|
|
163
|
+
if (!manifestPath) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
// .../<run-id>/manifest.json
|
|
167
|
+
const dir = manifestPath.endsWith('manifest.json') ? basename(dirname(manifestPath)) : null;
|
|
168
|
+
return dir && dir !== '..' ? dir : null;
|
|
169
|
+
}
|
|
170
|
+
function clampInt(value, min, max) {
|
|
171
|
+
const rounded = Math.floor(value);
|
|
172
|
+
if (!Number.isFinite(rounded)) {
|
|
173
|
+
return min;
|
|
174
|
+
}
|
|
175
|
+
return Math.max(min, Math.min(max, rounded));
|
|
176
|
+
}
|
|
177
|
+
function formatPercent(numerator, denominator) {
|
|
178
|
+
if (!denominator) {
|
|
179
|
+
return '0%';
|
|
180
|
+
}
|
|
181
|
+
const pct = (numerator / denominator) * 100;
|
|
182
|
+
return `${Math.round(pct * 10) / 10}%`;
|
|
183
|
+
}
|
|
184
|
+
function formatCloudStatuses(byStatus) {
|
|
185
|
+
const entries = Object.entries(byStatus);
|
|
186
|
+
if (entries.length === 0) {
|
|
187
|
+
return '';
|
|
188
|
+
}
|
|
189
|
+
entries.sort((a, b) => b[1] - a[1]);
|
|
190
|
+
const top = entries
|
|
191
|
+
.slice(0, 3)
|
|
192
|
+
.map(([status, count]) => `${status}=${count}`)
|
|
193
|
+
.join(', ');
|
|
194
|
+
return ` [${top}]`;
|
|
195
|
+
}
|
|
196
|
+
function readTaskIndexKeys(repoRoot) {
|
|
197
|
+
const indexPath = join(repoRoot, 'tasks', 'index.json');
|
|
198
|
+
try {
|
|
199
|
+
const raw = readFileSync(indexPath, 'utf8');
|
|
200
|
+
const parsed = JSON.parse(raw);
|
|
201
|
+
const items = Array.isArray(parsed?.items) ? parsed.items : [];
|
|
202
|
+
const keys = items
|
|
203
|
+
.map((item) => normalizeTaskKey(item))
|
|
204
|
+
.filter((key) => typeof key === 'string' && key.length > 0);
|
|
205
|
+
return new Set(keys);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return new Set();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -61,7 +61,7 @@ export function formatInitSummary(result, cwd) {
|
|
|
61
61
|
lines.push('No files written.');
|
|
62
62
|
}
|
|
63
63
|
lines.push('Next steps (recommended):');
|
|
64
|
-
lines.push(
|
|
64
|
+
lines.push(' - codex-orchestrator delegation setup --yes');
|
|
65
65
|
lines.push(' - codex-orchestrator codex setup # optional: managed/pinned Codex CLI (stock CLI works by default)');
|
|
66
66
|
return lines;
|
|
67
67
|
}
|
|
@@ -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)}`);
|
|
@@ -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,8 +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] [--usage]`: check optional tooling dependencies plus collab/cloud/delegation readiness and print enablement commands. `--usage` appends a local usage snapshot (scans `.runs/`).
|
|
105
105
|
- `codex-orchestrator devtools setup [--yes]`: print DevTools MCP setup instructions (`--yes` applies `codex mcp add ...`).
|
|
106
|
+
- `codex-orchestrator delegation setup [--yes]`: configure delegation MCP wiring (`--yes` applies `codex mcp add ...`).
|
|
106
107
|
- `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
108
|
- `codex-orchestrator self-check --format json`: emit a safe JSON health payload for smoke tests.
|
|
108
109
|
- `codex-orchestrator --version`: print the package version.
|
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.
|
|
@@ -63,7 +63,7 @@ Fix by re-registering the server with a TOML-quoted override:
|
|
|
63
63
|
codex mcp remove delegation
|
|
64
64
|
codex mcp add delegation \
|
|
65
65
|
--env 'CODEX_MCP_CONFIG_OVERRIDES=delegate.mode="full"' \
|
|
66
|
-
-- codex-orchestrator delegate-server
|
|
66
|
+
-- codex-orchestrator delegate-server
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
## Server mode vs child mode (don’t mix them up)
|
|
@@ -98,7 +98,7 @@ If you want deeper recursion or longer wall-clock time for delegated runs, set R
|
|
|
98
98
|
```bash
|
|
99
99
|
codex mcp add delegation \
|
|
100
100
|
--env 'CODEX_MCP_CONFIG_OVERRIDES=rlm.max_subcall_depth=8;rlm.wall_clock_timeout_ms=14400000' \
|
|
101
|
-
-- codex-orchestrator delegate-server
|
|
101
|
+
-- codex-orchestrator delegate-server
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
For the `rlm` pipeline specifically, use:
|
|
@@ -35,16 +35,16 @@ codex exec \
|
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Optional (only if you need it):
|
|
38
|
-
- Add `--repo /path/to/repo`
|
|
38
|
+
- Add `--repo /path/to/repo` only when you want to pin the server to a repo even if Codex is launched outside that repo (default uses cwd).
|
|
39
39
|
- Add `-c 'features.skills=false'` for a minimal, deterministic background run.
|
|
40
40
|
- Add `-c 'delegate.mode=question_only'` when the child only needs `delegate.question.*` (and optional `delegate.status`).
|
|
41
41
|
- Add `-c 'delegate.mode=full'` when the child needs `delegate.spawn/pause/cancel` (nested delegation / run control).
|
|
42
42
|
- If the task needs external docs or APIs, enable only the relevant MCP server for that environment.
|
|
43
43
|
- If `delegate.spawn` is missing, re-register the MCP server with full mode (server config controls tool surface):
|
|
44
44
|
- `codex mcp remove delegation`
|
|
45
|
-
- `codex mcp add delegation --env 'CODEX_MCP_CONFIG_OVERRIDES=delegate.mode="full"' -- codex-orchestrator delegate-server
|
|
45
|
+
- `codex mcp add delegation --env 'CODEX_MCP_CONFIG_OVERRIDES=delegate.mode="full"' -- codex-orchestrator delegate-server`
|
|
46
46
|
- To raise RLM budgets for delegated runs, re-register with an override (TOML-quoted):
|
|
47
|
-
- `codex mcp add delegation --env 'CODEX_MCP_CONFIG_OVERRIDES=rlm.max_subcall_depth=8;rlm.wall_clock_timeout_ms=14400000' -- codex-orchestrator delegate-server
|
|
47
|
+
- `codex mcp add delegation --env 'CODEX_MCP_CONFIG_OVERRIDES=rlm.max_subcall_depth=8;rlm.wall_clock_timeout_ms=14400000' -- codex-orchestrator delegate-server`
|
|
48
48
|
|
|
49
49
|
For deeper background patterns and troubleshooting, see `DELEGATION_GUIDE.md`.
|
|
50
50
|
For runner + delegation coordination (short `--task` flow), see `docs/delegation-runner-workflow.md`.
|
|
@@ -64,8 +64,10 @@ For runner + delegation coordination (short `--task` flow), see `docs/delegation
|
|
|
64
64
|
### 0) One-time setup (register the MCP server)
|
|
65
65
|
|
|
66
66
|
- Register the delegation server once:
|
|
67
|
+
- Preferred: `codex-orchestrator delegation setup --yes`
|
|
68
|
+
- This wraps `codex mcp add delegation ...` and keeps wiring discoverable via `codex-orchestrator doctor`.
|
|
67
69
|
- `codex mcp add delegation -- codex-orchestrator delegate-server`
|
|
68
|
-
- Optional
|
|
70
|
+
- Optional: append `--repo /path/to/repo` to pin the server to one repo (not recommended if you work across repos).
|
|
69
71
|
- `delegate-server` is the canonical name; `delegation-server` is supported as an alias.
|
|
70
72
|
- Per-run `-c 'mcp_servers.delegation.enabled=true'` only works **after** registration.
|
|
71
73
|
- If `delegate.*` tools are missing mid-task, start a new run with:
|