@kbediako/codex-orchestrator 0.1.35 → 0.1.37
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 +48 -25
- package/codex.orchestrator.json +39 -0
- package/dist/bin/codex-orchestrator.js +257 -29
- package/dist/orchestrator/src/cli/codexDefaultsSetup.js +274 -0
- package/dist/orchestrator/src/cli/config/userConfig.js +17 -1
- package/dist/orchestrator/src/cli/doctor.js +132 -1
- package/dist/orchestrator/src/cli/doctorIssueLog.js +42 -16
- package/dist/orchestrator/src/cli/frontendTestingRunner.js +24 -6
- package/dist/orchestrator/src/cli/orchestrator.js +119 -16
- package/dist/orchestrator/src/cli/rlmRunner.js +27 -3
- package/dist/orchestrator/src/cli/run/manifest.js +19 -0
- package/dist/orchestrator/src/cli/runtime/codexCommand.js +39 -0
- package/dist/orchestrator/src/cli/runtime/index.js +3 -0
- package/dist/orchestrator/src/cli/runtime/mode.js +53 -0
- package/dist/orchestrator/src/cli/runtime/provider.js +205 -0
- package/dist/orchestrator/src/cli/runtime/types.js +1 -0
- package/dist/orchestrator/src/cli/services/commandRunner.js +19 -5
- package/dist/orchestrator/src/cli/services/runPreparation.js +2 -0
- package/dist/orchestrator/src/cli/services/runSummaryWriter.js +12 -0
- package/dist/scripts/lib/pr-watch-merge.js +170 -9
- package/dist/scripts/run-review.js +2029 -0
- package/docs/README.md +12 -10
- package/package.json +4 -1
- package/schemas/manifest.json +20 -0
- package/skills/agent-first-adoption-steering/SKILL.md +116 -0
- package/skills/chrome-devtools/SKILL.md +6 -0
- package/skills/collab-deliberation/SKILL.md +6 -0
- package/skills/collab-evals/SKILL.md +15 -0
- package/skills/collab-subagents-first/SKILL.md +7 -1
- package/skills/delegate-early/SKILL.md +6 -0
- package/skills/delegation-usage/DELEGATION_GUIDE.md +7 -4
- package/skills/delegation-usage/SKILL.md +14 -4
- package/skills/docs-first/SKILL.md +6 -0
- package/skills/elegance-review/SKILL.md +4 -0
- package/skills/long-poll-wait/SKILL.md +82 -0
- package/skills/release/SKILL.md +6 -2
- package/skills/standalone-review/SKILL.md +9 -3
- package/templates/README.md +5 -0
- package/templates/codex/.codex/agents/awaiter-high.toml +38 -0
- package/templates/codex/.codex/agents/explorer-fast.toml +2 -0
- package/templates/codex/.codex/agents/worker-complex.toml +2 -0
- package/templates/codex/.codex/config.toml +19 -0
- package/templates/codex/AGENTS.md +10 -4
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
import { resolveCodexHome } from './utils/codexPaths.js';
|
|
7
|
+
import { findPackageRoot } from './utils/packageInfo.js';
|
|
8
|
+
import { writeAtomicFile } from '../utils/atomicWrite.js';
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const toml = require('@iarna/toml');
|
|
11
|
+
const canonicalize = require('canonicalize');
|
|
12
|
+
export const BASELINE_MODEL = 'gpt-5.3-codex';
|
|
13
|
+
export const BASELINE_REASONING = 'xhigh';
|
|
14
|
+
export const BASELINE_REASONING_MINIMUM = 'high';
|
|
15
|
+
export const BASELINE_AGENTS = {
|
|
16
|
+
max_threads: 12,
|
|
17
|
+
max_depth: 4,
|
|
18
|
+
max_spawn_depth: 4
|
|
19
|
+
};
|
|
20
|
+
const ROLE_DEFINITIONS = [
|
|
21
|
+
{
|
|
22
|
+
key: 'explorer_fast',
|
|
23
|
+
description: 'Fast explorer (spark text-only).',
|
|
24
|
+
fileName: 'explorer-fast.toml',
|
|
25
|
+
configFile: './agents/explorer-fast.toml',
|
|
26
|
+
templatePath: join('templates', 'codex', '.codex', 'agents', 'explorer-fast.toml')
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: 'worker_complex',
|
|
30
|
+
description: 'Complex implementation role.',
|
|
31
|
+
fileName: 'worker-complex.toml',
|
|
32
|
+
configFile: './agents/worker-complex.toml',
|
|
33
|
+
templatePath: join('templates', 'codex', '.codex', 'agents', 'worker-complex.toml')
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
key: 'awaiter',
|
|
37
|
+
description: 'Awaiter override (keeps awaiter behavior with latest codex/high reasoning).',
|
|
38
|
+
fileName: 'awaiter-high.toml',
|
|
39
|
+
configFile: './agents/awaiter-high.toml',
|
|
40
|
+
templatePath: join('templates', 'codex', '.codex', 'agents', 'awaiter-high.toml')
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
export async function runCodexDefaultsSetup(options = {}) {
|
|
44
|
+
const env = options.env ?? process.env;
|
|
45
|
+
const force = Boolean(options.force);
|
|
46
|
+
const apply = Boolean(options.apply);
|
|
47
|
+
const plan = buildPlan(env, force);
|
|
48
|
+
const roleDefinitions = await loadRoleDefinitions();
|
|
49
|
+
const configState = await loadConfigState(plan.configPath);
|
|
50
|
+
const nextConfig = mergeBaselineDefaults(configState.parsed, roleDefinitions);
|
|
51
|
+
const configChanged = canonicalize(configState.parsed) !== canonicalize(nextConfig);
|
|
52
|
+
const roleChanges = await planRoleChanges(plan.agentsDir, force, roleDefinitions);
|
|
53
|
+
if (!apply) {
|
|
54
|
+
const changes = buildPlannedChanges({
|
|
55
|
+
configPath: plan.configPath,
|
|
56
|
+
configExists: configState.exists,
|
|
57
|
+
configChanged,
|
|
58
|
+
roleChanges
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
status: 'planned',
|
|
62
|
+
plan,
|
|
63
|
+
changes
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const changes = [];
|
|
67
|
+
if (configChanged || !configState.exists) {
|
|
68
|
+
const rendered = `${toml.stringify(nextConfig)}\n`;
|
|
69
|
+
await writeAtomicFile(plan.configPath, rendered, { ensureDir: true, encoding: 'utf8' });
|
|
70
|
+
changes.push({
|
|
71
|
+
target: 'config',
|
|
72
|
+
name: 'config.toml',
|
|
73
|
+
path: plan.configPath,
|
|
74
|
+
status: configState.exists ? 'updated' : 'created',
|
|
75
|
+
detail: configState.exists
|
|
76
|
+
? 'Updated CO baseline defaults additively and preserved unrelated keys.'
|
|
77
|
+
: 'Created config.toml with CO baseline defaults.'
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
changes.push({
|
|
82
|
+
target: 'config',
|
|
83
|
+
name: 'config.toml',
|
|
84
|
+
path: plan.configPath,
|
|
85
|
+
status: 'unchanged',
|
|
86
|
+
detail: 'CO baseline defaults already present.'
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
await mkdir(plan.agentsDir, { recursive: true });
|
|
90
|
+
for (const roleChange of roleChanges) {
|
|
91
|
+
const shouldWrite = roleChange.existingContent === null
|
|
92
|
+
|| (force && roleChange.existingContent !== roleChange.definition.content);
|
|
93
|
+
if (shouldWrite) {
|
|
94
|
+
await writeAtomicFile(roleChange.path, roleChange.definition.content, {
|
|
95
|
+
ensureDir: true,
|
|
96
|
+
encoding: 'utf8'
|
|
97
|
+
});
|
|
98
|
+
changes.push({
|
|
99
|
+
target: 'role_file',
|
|
100
|
+
name: roleChange.definition.key,
|
|
101
|
+
path: roleChange.path,
|
|
102
|
+
status: roleChange.existingContent === null ? 'created' : 'updated',
|
|
103
|
+
detail: roleChange.existingContent === null
|
|
104
|
+
? `Created ${roleChange.definition.fileName}.`
|
|
105
|
+
: `Overwrote ${roleChange.definition.fileName} because --force was set.`
|
|
106
|
+
});
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
changes.push({
|
|
110
|
+
target: 'role_file',
|
|
111
|
+
name: roleChange.definition.key,
|
|
112
|
+
path: roleChange.path,
|
|
113
|
+
status: roleChange.currentStatus,
|
|
114
|
+
detail: roleChange.detail
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
status: 'applied',
|
|
119
|
+
plan,
|
|
120
|
+
changes
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function formatCodexDefaultsSetupSummary(result) {
|
|
124
|
+
const lines = [];
|
|
125
|
+
lines.push(`Codex defaults setup: ${result.status}`);
|
|
126
|
+
lines.push(`- Codex home: ${result.plan.codexHome}`);
|
|
127
|
+
lines.push(`- Config: ${result.plan.configPath}`);
|
|
128
|
+
lines.push(`- Agents dir: ${result.plan.agentsDir}`);
|
|
129
|
+
lines.push(`- Force overwrite: ${result.plan.force ? 'yes' : 'no'}`);
|
|
130
|
+
lines.push('- Changes:');
|
|
131
|
+
for (const change of result.changes) {
|
|
132
|
+
lines.push(` - ${change.target}:${change.name} -> ${change.status} (${change.path})`);
|
|
133
|
+
lines.push(` ${change.detail}`);
|
|
134
|
+
}
|
|
135
|
+
if (result.status === 'planned') {
|
|
136
|
+
lines.push('Run with --yes to apply this setup.');
|
|
137
|
+
}
|
|
138
|
+
return lines;
|
|
139
|
+
}
|
|
140
|
+
function buildPlan(env, force) {
|
|
141
|
+
const codexHome = resolveCodexHome(env);
|
|
142
|
+
return {
|
|
143
|
+
codexHome,
|
|
144
|
+
configPath: join(codexHome, 'config.toml'),
|
|
145
|
+
agentsDir: join(codexHome, 'agents'),
|
|
146
|
+
force
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async function loadConfigState(configPath) {
|
|
150
|
+
if (!existsSync(configPath)) {
|
|
151
|
+
return { exists: false, parsed: {} };
|
|
152
|
+
}
|
|
153
|
+
const raw = await readFile(configPath, 'utf8');
|
|
154
|
+
try {
|
|
155
|
+
const parsed = toml.parse(raw);
|
|
156
|
+
if (!isRecord(parsed)) {
|
|
157
|
+
throw new Error('top-level TOML document must be a table.');
|
|
158
|
+
}
|
|
159
|
+
return { exists: true, parsed: parsed };
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
const reason = error?.message ?? String(error);
|
|
163
|
+
throw new Error(`Failed to parse Codex config TOML at ${configPath}: ${reason}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function mergeBaselineDefaults(existing, roleDefinitions) {
|
|
167
|
+
const next = structuredClone(existing);
|
|
168
|
+
next.model = BASELINE_MODEL;
|
|
169
|
+
next.model_reasoning_effort = BASELINE_REASONING;
|
|
170
|
+
const agents = isRecord(next.agents) ? structuredClone(next.agents) : {};
|
|
171
|
+
agents.max_threads = BASELINE_AGENTS.max_threads;
|
|
172
|
+
agents.max_depth = BASELINE_AGENTS.max_depth;
|
|
173
|
+
agents.max_spawn_depth = BASELINE_AGENTS.max_spawn_depth;
|
|
174
|
+
for (const role of roleDefinitions) {
|
|
175
|
+
const existingRole = isRecord(agents[role.key])
|
|
176
|
+
? structuredClone(agents[role.key])
|
|
177
|
+
: {};
|
|
178
|
+
existingRole.description = role.description;
|
|
179
|
+
existingRole.config_file = role.configFile;
|
|
180
|
+
agents[role.key] = existingRole;
|
|
181
|
+
}
|
|
182
|
+
next.agents = agents;
|
|
183
|
+
return next;
|
|
184
|
+
}
|
|
185
|
+
async function planRoleChanges(agentsDir, force, roleDefinitions) {
|
|
186
|
+
const changes = [];
|
|
187
|
+
for (const definition of roleDefinitions) {
|
|
188
|
+
const path = join(agentsDir, definition.fileName);
|
|
189
|
+
if (!existsSync(path)) {
|
|
190
|
+
changes.push({
|
|
191
|
+
definition,
|
|
192
|
+
path,
|
|
193
|
+
existingContent: null,
|
|
194
|
+
currentStatus: 'pending',
|
|
195
|
+
detail: `Will create ${definition.fileName}.`
|
|
196
|
+
});
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const current = await readFile(path, 'utf8');
|
|
200
|
+
if (current === definition.content) {
|
|
201
|
+
changes.push({
|
|
202
|
+
definition,
|
|
203
|
+
path,
|
|
204
|
+
existingContent: current,
|
|
205
|
+
currentStatus: 'unchanged',
|
|
206
|
+
detail: `${definition.fileName} already matches CO baseline defaults.`
|
|
207
|
+
});
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (force) {
|
|
211
|
+
changes.push({
|
|
212
|
+
definition,
|
|
213
|
+
path,
|
|
214
|
+
existingContent: current,
|
|
215
|
+
currentStatus: 'pending',
|
|
216
|
+
detail: `Will overwrite ${definition.fileName} because --force is set.`
|
|
217
|
+
});
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
changes.push({
|
|
221
|
+
definition,
|
|
222
|
+
path,
|
|
223
|
+
existingContent: current,
|
|
224
|
+
currentStatus: 'preserved',
|
|
225
|
+
detail: `${definition.fileName} already exists; preserving without --force.`
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return changes;
|
|
229
|
+
}
|
|
230
|
+
async function loadRoleDefinitions() {
|
|
231
|
+
const packageRoot = findPackageRoot();
|
|
232
|
+
const loaded = [];
|
|
233
|
+
for (const definition of ROLE_DEFINITIONS) {
|
|
234
|
+
const templateFile = join(packageRoot, definition.templatePath);
|
|
235
|
+
let content;
|
|
236
|
+
try {
|
|
237
|
+
content = await readFile(templateFile, 'utf8');
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
const reason = error?.message ?? String(error);
|
|
241
|
+
throw new Error(`Unable to read role template ${templateFile}: ${reason}`);
|
|
242
|
+
}
|
|
243
|
+
loaded.push({ ...definition, content });
|
|
244
|
+
}
|
|
245
|
+
return loaded;
|
|
246
|
+
}
|
|
247
|
+
function buildPlannedChanges(params) {
|
|
248
|
+
const changes = [];
|
|
249
|
+
const configStatus = params.configChanged || !params.configExists ? 'pending' : 'unchanged';
|
|
250
|
+
changes.push({
|
|
251
|
+
target: 'config',
|
|
252
|
+
name: 'config.toml',
|
|
253
|
+
path: params.configPath,
|
|
254
|
+
status: configStatus,
|
|
255
|
+
detail: configStatus === 'pending'
|
|
256
|
+
? params.configExists
|
|
257
|
+
? 'Will update CO baseline defaults additively while preserving unrelated keys.'
|
|
258
|
+
: 'Will create config.toml with CO baseline defaults.'
|
|
259
|
+
: 'CO baseline defaults already present.'
|
|
260
|
+
});
|
|
261
|
+
for (const roleChange of params.roleChanges) {
|
|
262
|
+
changes.push({
|
|
263
|
+
target: 'role_file',
|
|
264
|
+
name: roleChange.definition.key,
|
|
265
|
+
path: roleChange.path,
|
|
266
|
+
status: roleChange.currentStatus,
|
|
267
|
+
detail: roleChange.detail
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return changes;
|
|
271
|
+
}
|
|
272
|
+
function isRecord(value) {
|
|
273
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
274
|
+
}
|
|
@@ -55,11 +55,17 @@ function normalizeUserConfig(config, source) {
|
|
|
55
55
|
if (!config) {
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
58
|
+
const runtimeMode = normalizeRuntimeMode(config.runtimeMode);
|
|
58
59
|
const stageSets = normalizeStageSets(config.stageSets);
|
|
59
60
|
const pipelines = Array.isArray(config.pipelines)
|
|
60
61
|
? config.pipelines.map((pipeline) => expandPipelineStages(pipeline, stageSets))
|
|
61
62
|
: config.pipelines;
|
|
62
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
pipelines,
|
|
65
|
+
defaultPipeline: config.defaultPipeline,
|
|
66
|
+
runtimeMode,
|
|
67
|
+
source
|
|
68
|
+
};
|
|
63
69
|
}
|
|
64
70
|
async function readConfig(configPath) {
|
|
65
71
|
try {
|
|
@@ -111,3 +117,13 @@ function expandPipelineStages(pipeline, stageSets) {
|
|
|
111
117
|
function isStageSetRef(stage) {
|
|
112
118
|
return stage.kind === 'stage-set';
|
|
113
119
|
}
|
|
120
|
+
function normalizeRuntimeMode(value) {
|
|
121
|
+
if (typeof value !== 'string') {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
const normalized = value.trim().toLowerCase();
|
|
125
|
+
if (normalized === 'cli' || normalized === 'appserver') {
|
|
126
|
+
return normalized;
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`Invalid codex.orchestrator.json runtimeMode "${value}". Expected one of: cli, appserver.`);
|
|
129
|
+
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
4
5
|
import { dirname, join, resolve } from 'node:path';
|
|
5
6
|
import { buildDevtoolsSetupPlan, DEVTOOLS_SKILL_NAME, resolveDevtoolsReadiness } from './utils/devtools.js';
|
|
6
7
|
import { isManagedCodexCliEnabled, resolveCodexCliBin, resolveCodexCliReadiness } from './utils/codexCli.js';
|
|
7
8
|
import { resolveCodexHome } from './utils/codexPaths.js';
|
|
8
9
|
import { resolveOptionalDependency } from './utils/optionalDeps.js';
|
|
9
10
|
import { runCloudPreflight } from './utils/cloudPreflight.js';
|
|
11
|
+
import { BASELINE_AGENTS, BASELINE_MODEL, BASELINE_REASONING_MINIMUM } from './codexDefaultsSetup.js';
|
|
10
12
|
import { CommandPlanner } from './adapters/CommandPlanner.js';
|
|
11
13
|
import { PipelineResolver } from './services/pipelineResolver.js';
|
|
12
14
|
import { isRepoConfigRequired } from './config/repoConfigPolicy.js';
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const toml = require('@iarna/toml');
|
|
13
17
|
const OPTIONAL_DEPENDENCIES = [
|
|
14
18
|
{
|
|
15
19
|
name: 'playwright',
|
|
@@ -75,6 +79,7 @@ export function runDoctor(cwd = process.cwd()) {
|
|
|
75
79
|
if (readiness.config.status !== 'ok') {
|
|
76
80
|
missing.push(`${DEVTOOLS_SKILL_NAME}-config`);
|
|
77
81
|
}
|
|
82
|
+
const codexDefaults = inspectCodexDefaultsAdvisory(process.env);
|
|
78
83
|
const codexBin = resolveCodexCliBin(process.env);
|
|
79
84
|
const managedOptIn = isManagedCodexCliEnabled(process.env);
|
|
80
85
|
const managedCodex = resolveCodexCliReadiness(process.env);
|
|
@@ -102,7 +107,7 @@ export function runDoctor(cwd = process.cwd()) {
|
|
|
102
107
|
const delegationConfig = inspectDelegationConfig();
|
|
103
108
|
const delegationStatus = delegationConfig.status === 'ok' ? 'ok' : 'missing-config';
|
|
104
109
|
return {
|
|
105
|
-
status: missing.length === 0 ? 'ok' : 'warning',
|
|
110
|
+
status: missing.length === 0 && codexDefaults.status === 'ok' ? 'ok' : 'warning',
|
|
106
111
|
missing,
|
|
107
112
|
dependencies,
|
|
108
113
|
devtools,
|
|
@@ -110,6 +115,7 @@ export function runDoctor(cwd = process.cwd()) {
|
|
|
110
115
|
active: { command: codexBin, managed_opt_in: managedOptIn },
|
|
111
116
|
managed: managedCodex
|
|
112
117
|
},
|
|
118
|
+
codex_defaults: codexDefaults,
|
|
113
119
|
collab: {
|
|
114
120
|
status: collabStatus,
|
|
115
121
|
enabled: collabEnabled,
|
|
@@ -296,6 +302,19 @@ export function formatDoctorSummary(result) {
|
|
|
296
302
|
lines.push(` - version: ${result.codex_cli.managed.install.version}`);
|
|
297
303
|
}
|
|
298
304
|
}
|
|
305
|
+
lines.push(`Codex defaults advisory: ${result.codex_defaults.status}`);
|
|
306
|
+
lines.push(` - config.toml: ${result.codex_defaults.config.status} (${result.codex_defaults.config.path})`);
|
|
307
|
+
if (result.codex_defaults.config.detail) {
|
|
308
|
+
lines.push(` detail: ${result.codex_defaults.config.detail}`);
|
|
309
|
+
}
|
|
310
|
+
lines.push(` - model: ${result.codex_defaults.checks.model.status} (actual: ${result.codex_defaults.checks.model.actual ?? '<unset>'}, expected: ${result.codex_defaults.checks.model.expected})`);
|
|
311
|
+
lines.push(` - model_reasoning_effort: ${result.codex_defaults.checks.model_reasoning_effort.status} (actual: ${result.codex_defaults.checks.model_reasoning_effort.actual ?? '<unset>'}, expected >= ${result.codex_defaults.checks.model_reasoning_effort.expected_minimum})`);
|
|
312
|
+
lines.push(` - agents.max_threads: ${result.codex_defaults.checks.max_threads.status} (actual: ${result.codex_defaults.checks.max_threads.actual ?? '<unset>'}, expected >= ${result.codex_defaults.checks.max_threads.expected_minimum})`);
|
|
313
|
+
lines.push(` - agents.max_depth: ${result.codex_defaults.checks.max_depth.status} (actual: ${result.codex_defaults.checks.max_depth.actual ?? '<unset>'}, expected >= ${result.codex_defaults.checks.max_depth.expected_minimum})`);
|
|
314
|
+
lines.push(` - agents.max_spawn_depth: ${result.codex_defaults.checks.max_spawn_depth.status} (actual: ${result.codex_defaults.checks.max_spawn_depth.actual ?? '<unset>'}, expected >= ${result.codex_defaults.checks.max_spawn_depth.expected_minimum})`);
|
|
315
|
+
for (const line of result.codex_defaults.guidance) {
|
|
316
|
+
lines.push(` - ${line}`);
|
|
317
|
+
}
|
|
299
318
|
lines.push(`Collab: ${result.collab.status}`);
|
|
300
319
|
if (result.collab.enabled !== null) {
|
|
301
320
|
lines.push(` - enabled: ${result.collab.enabled}`);
|
|
@@ -326,6 +345,118 @@ export function formatDoctorSummary(result) {
|
|
|
326
345
|
}
|
|
327
346
|
return lines;
|
|
328
347
|
}
|
|
348
|
+
function inspectCodexDefaultsAdvisory(env = process.env) {
|
|
349
|
+
const configPath = join(resolveCodexHome(env), 'config.toml');
|
|
350
|
+
const checks = {
|
|
351
|
+
model: { status: 'advisory', expected: BASELINE_MODEL, actual: null },
|
|
352
|
+
model_reasoning_effort: { status: 'advisory', expected_minimum: BASELINE_REASONING_MINIMUM, actual: null },
|
|
353
|
+
max_threads: { status: 'advisory', expected_minimum: BASELINE_AGENTS.max_threads, actual: null },
|
|
354
|
+
max_depth: { status: 'advisory', expected_minimum: BASELINE_AGENTS.max_depth, actual: null },
|
|
355
|
+
max_spawn_depth: { status: 'advisory', expected_minimum: BASELINE_AGENTS.max_spawn_depth, actual: null }
|
|
356
|
+
};
|
|
357
|
+
const guidance = [
|
|
358
|
+
'Run `codex-orchestrator codex defaults --yes` to apply additive baseline defaults.',
|
|
359
|
+
'Additive policy: unrelated config keys are preserved; existing role files stay untouched unless `--force` is set.'
|
|
360
|
+
];
|
|
361
|
+
if (!existsSync(configPath)) {
|
|
362
|
+
return {
|
|
363
|
+
status: 'advisory',
|
|
364
|
+
config: { path: configPath, status: 'missing', detail: 'config.toml not found' },
|
|
365
|
+
checks,
|
|
366
|
+
guidance
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
let parsed;
|
|
370
|
+
try {
|
|
371
|
+
const raw = readFileSync(configPath, 'utf8');
|
|
372
|
+
const value = toml.parse(raw);
|
|
373
|
+
if (!isRecord(value)) {
|
|
374
|
+
throw new Error('top-level TOML document must be a table.');
|
|
375
|
+
}
|
|
376
|
+
parsed = value;
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
return {
|
|
380
|
+
status: 'advisory',
|
|
381
|
+
config: {
|
|
382
|
+
path: configPath,
|
|
383
|
+
status: 'invalid',
|
|
384
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
385
|
+
},
|
|
386
|
+
checks,
|
|
387
|
+
guidance: [
|
|
388
|
+
`Fix TOML syntax in ${configPath} first, then rerun \`codex-orchestrator codex defaults --yes\`.`,
|
|
389
|
+
...guidance
|
|
390
|
+
]
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const model = normalizeOptionalString(readStringValue(parsed.model));
|
|
394
|
+
checks.model.actual = model;
|
|
395
|
+
checks.model.status = model === BASELINE_MODEL ? 'ok' : 'advisory';
|
|
396
|
+
const reasoning = normalizeOptionalString(readStringValue(parsed.model_reasoning_effort));
|
|
397
|
+
checks.model_reasoning_effort.actual = reasoning;
|
|
398
|
+
checks.model_reasoning_effort.status = isReasoningAtLeastMinimum(reasoning, BASELINE_REASONING_MINIMUM)
|
|
399
|
+
? 'ok'
|
|
400
|
+
: 'advisory';
|
|
401
|
+
const agents = isRecord(parsed.agents) ? parsed.agents : {};
|
|
402
|
+
const maxThreads = readNumberValue(agents.max_threads);
|
|
403
|
+
const maxDepth = readNumberValue(agents.max_depth);
|
|
404
|
+
const maxSpawnDepth = readNumberValue(agents.max_spawn_depth);
|
|
405
|
+
checks.max_threads.actual = maxThreads;
|
|
406
|
+
checks.max_threads.status =
|
|
407
|
+
typeof maxThreads === 'number' && maxThreads >= BASELINE_AGENTS.max_threads ? 'ok' : 'advisory';
|
|
408
|
+
checks.max_depth.actual = maxDepth;
|
|
409
|
+
checks.max_depth.status = typeof maxDepth === 'number' && maxDepth >= BASELINE_AGENTS.max_depth ? 'ok' : 'advisory';
|
|
410
|
+
checks.max_spawn_depth.actual = maxSpawnDepth;
|
|
411
|
+
checks.max_spawn_depth.status =
|
|
412
|
+
typeof maxSpawnDepth === 'number' && maxSpawnDepth >= BASELINE_AGENTS.max_spawn_depth ? 'ok' : 'advisory';
|
|
413
|
+
const allChecksOk = Object.values(checks).every((check) => check.status === 'ok');
|
|
414
|
+
return {
|
|
415
|
+
status: allChecksOk ? 'ok' : 'advisory',
|
|
416
|
+
config: { path: configPath, status: 'ok' },
|
|
417
|
+
checks,
|
|
418
|
+
guidance
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function isRecord(value) {
|
|
422
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
423
|
+
}
|
|
424
|
+
function readStringValue(value) {
|
|
425
|
+
return typeof value === 'string' ? value : null;
|
|
426
|
+
}
|
|
427
|
+
function readNumberValue(value) {
|
|
428
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
429
|
+
}
|
|
430
|
+
function isReasoningAtLeastMinimum(value, minimum) {
|
|
431
|
+
const rank = resolveReasoningRank(value);
|
|
432
|
+
const minimumRank = resolveReasoningRank(minimum);
|
|
433
|
+
if (rank === null || minimumRank === null) {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
return rank >= minimumRank;
|
|
437
|
+
}
|
|
438
|
+
function resolveReasoningRank(value) {
|
|
439
|
+
if (!value) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
const normalized = value.trim().toLowerCase();
|
|
443
|
+
switch (normalized) {
|
|
444
|
+
case 'minimal':
|
|
445
|
+
return 0;
|
|
446
|
+
case 'low':
|
|
447
|
+
return 1;
|
|
448
|
+
case 'medium':
|
|
449
|
+
return 2;
|
|
450
|
+
case 'high':
|
|
451
|
+
return 3;
|
|
452
|
+
case 'xhigh':
|
|
453
|
+
return 4;
|
|
454
|
+
case 'maximum':
|
|
455
|
+
return 5;
|
|
456
|
+
default:
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
329
460
|
function normalizeOptionalString(value) {
|
|
330
461
|
if (typeof value !== 'string') {
|
|
331
462
|
return null;
|
|
@@ -10,9 +10,10 @@ Purpose:
|
|
|
10
10
|
export async function writeDoctorIssueLog(options) {
|
|
11
11
|
const cwd = resolve(options.cwd ?? process.cwd());
|
|
12
12
|
const env = options.env ?? process.env;
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
13
|
+
const explicitCwd = options.cwd !== undefined;
|
|
14
|
+
const repoRoot = resolveIssueLogRepoRoot(cwd, env, explicitCwd);
|
|
15
|
+
const runsRoot = resolveIssueLogRootPath(repoRoot, env.CODEX_ORCHESTRATOR_RUNS_DIR, '.runs', explicitCwd);
|
|
16
|
+
const outRoot = resolveIssueLogRootPath(repoRoot, env.CODEX_ORCHESTRATOR_OUT_DIR, 'out', explicitCwd);
|
|
16
17
|
const defaultTaskId = normalizeIssueLogTaskId(env);
|
|
17
18
|
const capturedAt = new Date().toISOString();
|
|
18
19
|
const issueId = formatIssueId(capturedAt);
|
|
@@ -95,14 +96,31 @@ function resolveIssueLogPath(repoRoot, rawPath) {
|
|
|
95
96
|
}
|
|
96
97
|
return resolve(repoRoot, normalized);
|
|
97
98
|
}
|
|
98
|
-
function resolveIssueLogRepoRoot(cwd, env) {
|
|
99
|
+
function resolveIssueLogRepoRoot(cwd, env, explicitCwd) {
|
|
100
|
+
const cwdResolution = resolveRepoRootFromHint(cwd);
|
|
99
101
|
const configuredRoot = normalizeText(env.CODEX_ORCHESTRATOR_ROOT);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
if (configuredRoot === null) {
|
|
103
|
+
return cwdResolution.root;
|
|
104
|
+
}
|
|
105
|
+
const configuredHint = isAbsolute(configuredRoot) ? configuredRoot : resolve(cwd, configuredRoot);
|
|
106
|
+
const configuredResolution = resolveRepoRootFromHint(configuredHint);
|
|
107
|
+
if (!explicitCwd) {
|
|
108
|
+
return configuredResolution.root;
|
|
109
|
+
}
|
|
110
|
+
// Keep explicit override behavior when cwd is inside the configured workspace.
|
|
111
|
+
if (isPathWithin(cwd, configuredResolution.root)) {
|
|
112
|
+
// If cwd sits under a nested workspace boundary, prefer that more local workspace.
|
|
113
|
+
if (cwdResolution.source !== 'hint' && cwdResolution.root !== configuredResolution.root) {
|
|
114
|
+
return cwdResolution.root;
|
|
115
|
+
}
|
|
116
|
+
return configuredResolution.root;
|
|
117
|
+
}
|
|
118
|
+
// If cwd clearly belongs to another workspace (tasks index or git boundary), prefer cwd.
|
|
119
|
+
if (cwdResolution.source !== 'hint') {
|
|
120
|
+
return cwdResolution.root;
|
|
121
|
+
}
|
|
122
|
+
// Fallback to configured root when cwd has no strong workspace signals.
|
|
123
|
+
return configuredResolution.root;
|
|
106
124
|
}
|
|
107
125
|
function resolveRepoRootFromHint(rootHint) {
|
|
108
126
|
const normalizedHint = resolve(rootHint);
|
|
@@ -110,7 +128,7 @@ function resolveRepoRootFromHint(rootHint) {
|
|
|
110
128
|
let current = normalizedHint;
|
|
111
129
|
while (current) {
|
|
112
130
|
if (existsSync(join(current, 'tasks', 'index.json'))) {
|
|
113
|
-
return current;
|
|
131
|
+
return { root: current, source: 'tasks-index' };
|
|
114
132
|
}
|
|
115
133
|
if (gitBoundary && current === gitBoundary) {
|
|
116
134
|
break;
|
|
@@ -121,7 +139,10 @@ function resolveRepoRootFromHint(rootHint) {
|
|
|
121
139
|
}
|
|
122
140
|
current = parent;
|
|
123
141
|
}
|
|
124
|
-
|
|
142
|
+
if (gitBoundary) {
|
|
143
|
+
return { root: gitBoundary, source: 'git-boundary' };
|
|
144
|
+
}
|
|
145
|
+
return { root: normalizedHint, source: 'hint' };
|
|
125
146
|
}
|
|
126
147
|
function findNearestGitBoundary(start) {
|
|
127
148
|
let current = resolve(start);
|
|
@@ -137,15 +158,20 @@ function findNearestGitBoundary(start) {
|
|
|
137
158
|
}
|
|
138
159
|
return null;
|
|
139
160
|
}
|
|
140
|
-
function resolveIssueLogRootPath(repoRoot, configuredPath, fallback) {
|
|
161
|
+
function resolveIssueLogRootPath(repoRoot, configuredPath, fallback, enforceRepoBoundary) {
|
|
141
162
|
const normalized = normalizeText(configuredPath);
|
|
142
163
|
if (!normalized) {
|
|
143
164
|
return resolve(repoRoot, fallback);
|
|
144
165
|
}
|
|
145
|
-
|
|
146
|
-
|
|
166
|
+
const resolvedPath = isAbsolute(normalized) ? normalized : resolve(repoRoot, normalized);
|
|
167
|
+
if (enforceRepoBoundary && !isPathWithin(resolvedPath, repoRoot)) {
|
|
168
|
+
return resolve(repoRoot, fallback);
|
|
147
169
|
}
|
|
148
|
-
return
|
|
170
|
+
return resolvedPath;
|
|
171
|
+
}
|
|
172
|
+
function isPathWithin(candidatePath, rootPath) {
|
|
173
|
+
const rel = relative(rootPath, candidatePath);
|
|
174
|
+
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
149
175
|
}
|
|
150
176
|
function normalizeIssueLogTaskId(env) {
|
|
151
177
|
return normalizeArtifactTaskId(normalizeText(env.MCP_RUNNER_TASK_ID)
|
|
@@ -4,7 +4,7 @@ import { resolve } from 'node:path';
|
|
|
4
4
|
import process from 'node:process';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { logger } from '../logger.js';
|
|
7
|
-
import {
|
|
7
|
+
import { createRuntimeCodexCommandContext, formatRuntimeSelectionSummary, parseRuntimeMode, resolveRuntimeCodexCommand } from './runtime/index.js';
|
|
8
8
|
const DEFAULT_PROMPT = [
|
|
9
9
|
'You are running frontend testing for the current project.',
|
|
10
10
|
'',
|
|
@@ -37,9 +37,9 @@ export async function loadFrontendTestingPrompt(env = process.env) {
|
|
|
37
37
|
}
|
|
38
38
|
return DEFAULT_PROMPT;
|
|
39
39
|
}
|
|
40
|
-
export function resolveFrontendTestingCommand(prompt,
|
|
40
|
+
export function resolveFrontendTestingCommand(prompt, context) {
|
|
41
41
|
const args = ['exec', prompt];
|
|
42
|
-
return
|
|
42
|
+
return resolveRuntimeCodexCommand(args, context);
|
|
43
43
|
}
|
|
44
44
|
function envFlagEnabled(value) {
|
|
45
45
|
if (!value) {
|
|
@@ -59,16 +59,21 @@ function shouldForceNonInteractive(env) {
|
|
|
59
59
|
}
|
|
60
60
|
export async function runFrontendTesting(env = process.env) {
|
|
61
61
|
const prompt = await loadFrontendTestingPrompt(env);
|
|
62
|
-
const
|
|
62
|
+
const repoRoot = typeof env.CODEX_ORCHESTRATOR_ROOT === 'string' && env.CODEX_ORCHESTRATOR_ROOT.trim().length > 0
|
|
63
|
+
? env.CODEX_ORCHESTRATOR_ROOT.trim()
|
|
64
|
+
: process.cwd();
|
|
65
|
+
const runtimeContext = await resolveFrontendTestingRuntimeContext(env, repoRoot);
|
|
66
|
+
logger.info(`[frontend-testing-runtime] ${formatRuntimeSelectionSummary(runtimeContext.runtime)}`);
|
|
67
|
+
const { command, args } = resolveFrontendTestingCommand(prompt, runtimeContext);
|
|
63
68
|
const nonInteractive = shouldForceNonInteractive(env);
|
|
64
|
-
const childEnv = { ...process.env, ...env };
|
|
69
|
+
const childEnv = { ...process.env, ...env, ...runtimeContext.env };
|
|
65
70
|
if (nonInteractive) {
|
|
66
71
|
childEnv.CODEX_NON_INTERACTIVE = childEnv.CODEX_NON_INTERACTIVE ?? '1';
|
|
67
72
|
childEnv.CODEX_NO_INTERACTIVE = childEnv.CODEX_NO_INTERACTIVE ?? '1';
|
|
68
73
|
childEnv.CODEX_INTERACTIVE = childEnv.CODEX_INTERACTIVE ?? '0';
|
|
69
74
|
}
|
|
70
75
|
const stdio = nonInteractive ? ['ignore', 'inherit', 'inherit'] : 'inherit';
|
|
71
|
-
const child = spawn(command, args, { stdio, env: childEnv });
|
|
76
|
+
const child = spawn(command, args, { stdio, env: childEnv, cwd: repoRoot });
|
|
72
77
|
await new Promise((resolvePromise, reject) => {
|
|
73
78
|
child.once('error', (error) => reject(error instanceof Error ? error : new Error(String(error))));
|
|
74
79
|
child.once('exit', (code) => {
|
|
@@ -81,6 +86,19 @@ export async function runFrontendTesting(env = process.env) {
|
|
|
81
86
|
});
|
|
82
87
|
});
|
|
83
88
|
}
|
|
89
|
+
async function resolveFrontendTestingRuntimeContext(env, repoRoot) {
|
|
90
|
+
const requestedMode = parseRuntimeMode(env.CODEX_ORCHESTRATOR_RUNTIME_MODE_ACTIVE ?? env.CODEX_ORCHESTRATOR_RUNTIME_MODE ?? null);
|
|
91
|
+
const runId = typeof env.CODEX_ORCHESTRATOR_RUN_ID === 'string' && env.CODEX_ORCHESTRATOR_RUN_ID.trim().length > 0
|
|
92
|
+
? env.CODEX_ORCHESTRATOR_RUN_ID.trim()
|
|
93
|
+
: `frontend-testing-${Date.now()}`;
|
|
94
|
+
return await createRuntimeCodexCommandContext({
|
|
95
|
+
requestedMode,
|
|
96
|
+
executionMode: 'mcp',
|
|
97
|
+
repoRoot,
|
|
98
|
+
env: { ...process.env, ...env },
|
|
99
|
+
runId
|
|
100
|
+
});
|
|
101
|
+
}
|
|
84
102
|
async function main() {
|
|
85
103
|
await runFrontendTesting();
|
|
86
104
|
}
|