@kbediako/codex-orchestrator 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -196,10 +196,28 @@ Use an explicit handoff note for reviewers. `NOTES` is required for review runs;
|
|
|
196
196
|
|
|
197
197
|
Template: `Goal: ... | Summary: ... | Risks: ... | Questions (optional): ...`
|
|
198
198
|
|
|
199
|
-
To enable Chrome DevTools for review runs, set `CODEX_REVIEW_DEVTOOLS=1` (uses
|
|
199
|
+
To enable Chrome DevTools for review runs, set `CODEX_REVIEW_DEVTOOLS=1` (uses a codex config override; no repo scripts required).
|
|
200
200
|
Default to the standard `implementation-gate` for general reviews; use `implementation-gate-devtools` only when the review needs Chrome DevTools capabilities (visual/layout checks, network/perf diagnostics). After fixing review feedback, rerun the same gate and include any follow-up questions in `NOTES`.
|
|
201
201
|
To run the full implementation gate with DevTools-enabled review, use `npx codex-orchestrator start implementation-gate-devtools --format json --no-interactive --task <task-id>`.
|
|
202
202
|
|
|
203
|
+
## Frontend Testing
|
|
204
|
+
Frontend testing is a first-class pipeline with DevTools off by default. The shipped pipelines already set `CODEX_NON_INTERACTIVE=1`; add it explicitly for custom automation or when you want the `frontend-test` shortcut to suppress Codex prompts:
|
|
205
|
+
- `CODEX_NON_INTERACTIVE=1 npx codex-orchestrator start frontend-testing --format json --no-interactive --task <task-id>`
|
|
206
|
+
- `CODEX_NON_INTERACTIVE=1 npx codex-orchestrator start frontend-testing-devtools --format json --no-interactive --task <task-id>` (DevTools enabled)
|
|
207
|
+
- `CODEX_NON_INTERACTIVE=1 codex-orchestrator frontend-test` (shortcut; add `--devtools` to enable DevTools)
|
|
208
|
+
|
|
209
|
+
If you run the pipelines from this repo, run `npm run build` first so `dist/` stays current (the pipeline executes the compiled runner).
|
|
210
|
+
|
|
211
|
+
Note: the frontend-testing pipelines toggle the shared `CODEX_REVIEW_DEVTOOLS` flag under the hood; prefer `--devtools` or the devtools pipeline instead of setting it manually.
|
|
212
|
+
|
|
213
|
+
Optional prompt overrides:
|
|
214
|
+
- `CODEX_FRONTEND_TEST_PROMPT` (inline prompt)
|
|
215
|
+
- `CODEX_FRONTEND_TEST_PROMPT_PATH` (path to a prompt file)
|
|
216
|
+
|
|
217
|
+
`--no-interactive` disables the HUD only; set `CODEX_NON_INTERACTIVE=1` when you need to suppress Codex prompts (e.g., shortcut runs or custom automation).
|
|
218
|
+
|
|
219
|
+
Check readiness with `codex-orchestrator doctor --format json` (reports DevTools skill availability).
|
|
220
|
+
|
|
203
221
|
## Mirror Workflows
|
|
204
222
|
- `npm run mirror:fetch -- --project <name> [--dry-run] [--force]`: reads `packages/<project>/mirror.config.json` (origin, routes, asset roots, rewrite/block/allow lists), caches downloads **per project** under `.runs/<task>/mirror/<project>/cache`, strips tracker patterns, rewrites externals to `/external/<host>/...`, localizes OG/twitter preview images, rewrites share links off tracker-heavy hosts, and stages into `.runs/<task>/mirror/<project>/<timestamp>/staging/public` before promoting to `packages/<project>/public`. Non-origin assets fall back to Web Archive when the primary host is down; promotion is skipped if errors are detected unless `--force` is set. Manifests live at `.runs/<task>/mirror/<project>/<timestamp>/manifest.json` (warns when `MCP_RUNNER_TASK_ID` is unset; honors `compliance/permit.json` when present).
|
|
205
223
|
- `npm run mirror:serve -- --project <name> [--port <port>] [--csp <self|strict|off>] [--no-range]`: shared local-mirror server with traversal guard, HTML no-cache/asset immutability, optional CSP, optional Range support, and directory-listing blocks.
|
|
@@ -28,6 +28,9 @@ async function main() {
|
|
|
28
28
|
case 'start':
|
|
29
29
|
await handleStart(orchestrator, args);
|
|
30
30
|
break;
|
|
31
|
+
case 'frontend-test':
|
|
32
|
+
await handleFrontendTest(orchestrator, args);
|
|
33
|
+
break;
|
|
31
34
|
case 'plan':
|
|
32
35
|
await handlePlan(orchestrator, args);
|
|
33
36
|
break;
|
|
@@ -155,6 +158,61 @@ async function handleStart(orchestrator, rawArgs) {
|
|
|
155
158
|
runEvents.dispose();
|
|
156
159
|
}
|
|
157
160
|
}
|
|
161
|
+
async function handleFrontendTest(orchestrator, rawArgs) {
|
|
162
|
+
const { positionals, flags } = parseArgs(rawArgs);
|
|
163
|
+
const format = flags['format'] === 'json' ? 'json' : 'text';
|
|
164
|
+
const devtools = Boolean(flags['devtools']);
|
|
165
|
+
const interactiveRequested = Boolean(flags['interactive'] || flags['ui']);
|
|
166
|
+
const interactiveDisabled = Boolean(flags['no-interactive']);
|
|
167
|
+
const runEvents = new RunEventEmitter();
|
|
168
|
+
const gate = evaluateInteractiveGate({
|
|
169
|
+
requested: interactiveRequested,
|
|
170
|
+
disabled: interactiveDisabled,
|
|
171
|
+
format,
|
|
172
|
+
stdoutIsTTY: process.stdout.isTTY === true,
|
|
173
|
+
stderrIsTTY: process.stderr.isTTY === true,
|
|
174
|
+
term: process.env.TERM ?? null
|
|
175
|
+
});
|
|
176
|
+
const hud = await maybeStartHud(gate, runEvents);
|
|
177
|
+
if (!gate.enabled && interactiveRequested && !interactiveDisabled && gate.reason) {
|
|
178
|
+
console.error(`[HUD disabled] ${gate.reason}`);
|
|
179
|
+
}
|
|
180
|
+
if (positionals.length > 0) {
|
|
181
|
+
console.error(`[frontend-test] ignoring extra arguments: ${positionals.join(' ')}`);
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const pipelineId = devtools ? 'frontend-testing-devtools' : 'frontend-testing';
|
|
185
|
+
const result = await orchestrator.start({
|
|
186
|
+
pipelineId,
|
|
187
|
+
taskId: typeof flags['task'] === 'string' ? flags['task'] : undefined,
|
|
188
|
+
parentRunId: typeof flags['parent-run'] === 'string' ? flags['parent-run'] : undefined,
|
|
189
|
+
approvalPolicy: typeof flags['approval-policy'] === 'string' ? flags['approval-policy'] : undefined,
|
|
190
|
+
targetStageId: resolveTargetStageId(flags),
|
|
191
|
+
runEvents
|
|
192
|
+
});
|
|
193
|
+
hud?.stop();
|
|
194
|
+
const payload = {
|
|
195
|
+
run_id: result.manifest.run_id,
|
|
196
|
+
status: result.manifest.status,
|
|
197
|
+
artifact_root: result.manifest.artifact_root,
|
|
198
|
+
manifest: `${result.manifest.artifact_root}/manifest.json`,
|
|
199
|
+
log_path: result.manifest.log_path
|
|
200
|
+
};
|
|
201
|
+
if (format === 'json') {
|
|
202
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
console.log(`Run started: ${payload.run_id}`);
|
|
206
|
+
console.log(`Status: ${payload.status}`);
|
|
207
|
+
console.log(`Manifest: ${payload.manifest}`);
|
|
208
|
+
console.log(`Log: ${payload.log_path}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
hud?.stop();
|
|
213
|
+
runEvents.dispose();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
158
216
|
async function handlePlan(orchestrator, rawArgs) {
|
|
159
217
|
const { positionals, flags } = parseArgs(rawArgs);
|
|
160
218
|
const pipelineId = positionals[0];
|
|
@@ -467,6 +525,16 @@ Commands:
|
|
|
467
525
|
--interactive | --ui Enable read-only HUD when running in a TTY.
|
|
468
526
|
--no-interactive Force disable HUD (default is off unless requested).
|
|
469
527
|
|
|
528
|
+
frontend-test Run frontend testing pipeline.
|
|
529
|
+
--devtools Enable Chrome DevTools MCP for this run.
|
|
530
|
+
--task <id> Override task identifier (defaults to MCP_RUNNER_TASK_ID).
|
|
531
|
+
--parent-run <id> Link run to parent run id.
|
|
532
|
+
--approval-policy <p> Record approval policy metadata.
|
|
533
|
+
--format json Emit machine-readable output.
|
|
534
|
+
--target <stage-id> Focus plan/build metadata on a specific stage (alias: --target-stage).
|
|
535
|
+
--interactive | --ui Enable read-only HUD when running in a TTY.
|
|
536
|
+
--no-interactive Force disable HUD (default is off unless requested).
|
|
537
|
+
|
|
470
538
|
plan [pipeline] Preview pipeline stages without executing.
|
|
471
539
|
--task <id> Override task identifier.
|
|
472
540
|
--format json Emit machine-readable output.
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
1
4
|
import process from 'node:process';
|
|
2
5
|
import { resolveOptionalDependency } from './utils/optionalDeps.js';
|
|
3
6
|
const OPTIONAL_DEPENDENCIES = [
|
|
@@ -9,6 +12,7 @@ const OPTIONAL_DEPENDENCIES = [
|
|
|
9
12
|
{ name: 'pixelmatch', install: 'npm install --save-dev pixelmatch' },
|
|
10
13
|
{ name: 'cheerio', install: 'npm install --save-dev cheerio' }
|
|
11
14
|
];
|
|
15
|
+
const DEVTOOLS_SKILL_NAME = 'chrome-devtools';
|
|
12
16
|
export function runDoctor(cwd = process.cwd()) {
|
|
13
17
|
const dependencies = OPTIONAL_DEPENDENCIES.map((entry) => {
|
|
14
18
|
const resolved = resolveOptionalDependency(entry.name, cwd);
|
|
@@ -22,11 +26,36 @@ export function runDoctor(cwd = process.cwd()) {
|
|
|
22
26
|
install: entry.install
|
|
23
27
|
};
|
|
24
28
|
});
|
|
29
|
+
const codexHome = resolveCodexHome();
|
|
30
|
+
const skillPath = join(codexHome, 'skills', DEVTOOLS_SKILL_NAME, 'SKILL.md');
|
|
31
|
+
const skillInstalled = existsSync(skillPath);
|
|
32
|
+
const devtools = {
|
|
33
|
+
status: skillInstalled ? 'ok' : 'missing',
|
|
34
|
+
skill: {
|
|
35
|
+
name: DEVTOOLS_SKILL_NAME,
|
|
36
|
+
status: skillInstalled ? 'ok' : 'missing',
|
|
37
|
+
path: skillPath,
|
|
38
|
+
install: skillInstalled
|
|
39
|
+
? undefined
|
|
40
|
+
: [
|
|
41
|
+
`Copy the ${DEVTOOLS_SKILL_NAME} skill into ${join(codexHome, 'skills', DEVTOOLS_SKILL_NAME)}`,
|
|
42
|
+
`Expected file: ${skillPath}`
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
enablement: [
|
|
46
|
+
'Enable DevTools for a run with CODEX_REVIEW_DEVTOOLS=1',
|
|
47
|
+
"Or run Codex with: codex -c 'mcp_servers.chrome-devtools.enabled=true' ..."
|
|
48
|
+
]
|
|
49
|
+
};
|
|
25
50
|
const missing = dependencies.filter((dep) => dep.status === 'missing').map((dep) => dep.name);
|
|
51
|
+
if (!skillInstalled) {
|
|
52
|
+
missing.push(DEVTOOLS_SKILL_NAME);
|
|
53
|
+
}
|
|
26
54
|
return {
|
|
27
55
|
status: missing.length === 0 ? 'ok' : 'warning',
|
|
28
56
|
missing,
|
|
29
|
-
dependencies
|
|
57
|
+
dependencies,
|
|
58
|
+
devtools
|
|
30
59
|
};
|
|
31
60
|
}
|
|
32
61
|
export function formatDoctorSummary(result) {
|
|
@@ -44,5 +73,25 @@ export function formatDoctorSummary(result) {
|
|
|
44
73
|
}
|
|
45
74
|
}
|
|
46
75
|
}
|
|
76
|
+
lines.push(`DevTools: ${result.devtools.status}`);
|
|
77
|
+
if (result.devtools.skill.status === 'ok') {
|
|
78
|
+
lines.push(` - ${result.devtools.skill.name}: ok (${result.devtools.skill.path})`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
lines.push(` - ${result.devtools.skill.name}: missing`);
|
|
82
|
+
for (const instruction of result.devtools.skill.install ?? []) {
|
|
83
|
+
lines.push(` install: ${instruction}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const line of result.devtools.enablement) {
|
|
87
|
+
lines.push(` - ${line}`);
|
|
88
|
+
}
|
|
47
89
|
return lines;
|
|
48
90
|
}
|
|
91
|
+
function resolveCodexHome() {
|
|
92
|
+
const override = process.env.CODEX_HOME?.trim();
|
|
93
|
+
if (override) {
|
|
94
|
+
return override;
|
|
95
|
+
}
|
|
96
|
+
return join(homedir(), '.codex');
|
|
97
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { logger } from '../logger.js';
|
|
7
|
+
import { resolveCodexCommand } from './utils/devtools.js';
|
|
8
|
+
const DEFAULT_PROMPT = [
|
|
9
|
+
'You are running frontend testing for the current project.',
|
|
10
|
+
'',
|
|
11
|
+
'Goals:',
|
|
12
|
+
'- Validate critical user flows and layout responsiveness.',
|
|
13
|
+
'- Check the browser console and network panels for errors.',
|
|
14
|
+
'- If Chrome DevTools MCP is enabled, use it.',
|
|
15
|
+
'',
|
|
16
|
+
'Output a concise report with:',
|
|
17
|
+
'- Summary',
|
|
18
|
+
'- Steps executed',
|
|
19
|
+
'- Issues (severity, repro)',
|
|
20
|
+
'- Follow-up recommendations',
|
|
21
|
+
'',
|
|
22
|
+
'Do not modify code unless explicitly asked.'
|
|
23
|
+
].join('\n');
|
|
24
|
+
export async function loadFrontendTestingPrompt(env = process.env) {
|
|
25
|
+
const promptPath = env.CODEX_FRONTEND_TEST_PROMPT_PATH?.trim();
|
|
26
|
+
if (promptPath) {
|
|
27
|
+
const raw = await readFile(resolve(promptPath), 'utf8');
|
|
28
|
+
const trimmed = raw.trim();
|
|
29
|
+
if (!trimmed) {
|
|
30
|
+
throw new Error(`Frontend testing prompt file is empty: ${promptPath}`);
|
|
31
|
+
}
|
|
32
|
+
return trimmed;
|
|
33
|
+
}
|
|
34
|
+
const inlinePrompt = env.CODEX_FRONTEND_TEST_PROMPT?.trim();
|
|
35
|
+
if (inlinePrompt) {
|
|
36
|
+
return inlinePrompt;
|
|
37
|
+
}
|
|
38
|
+
return DEFAULT_PROMPT;
|
|
39
|
+
}
|
|
40
|
+
export function resolveFrontendTestingCommand(prompt, env = process.env) {
|
|
41
|
+
const args = ['exec', prompt];
|
|
42
|
+
return resolveCodexCommand(args, env);
|
|
43
|
+
}
|
|
44
|
+
function envFlagEnabled(value) {
|
|
45
|
+
if (!value) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const normalized = value.trim().toLowerCase();
|
|
49
|
+
return normalized === '1' || normalized === 'true' || normalized === 'yes';
|
|
50
|
+
}
|
|
51
|
+
function shouldForceNonInteractive(env) {
|
|
52
|
+
const stdinIsTTY = process.stdin?.isTTY === true;
|
|
53
|
+
return (!stdinIsTTY ||
|
|
54
|
+
envFlagEnabled(env.CI) ||
|
|
55
|
+
envFlagEnabled(env.CODEX_REVIEW_NON_INTERACTIVE) ||
|
|
56
|
+
envFlagEnabled(env.CODEX_NON_INTERACTIVE) ||
|
|
57
|
+
envFlagEnabled(env.CODEX_NONINTERACTIVE) ||
|
|
58
|
+
envFlagEnabled(env.CODEX_NO_INTERACTIVE));
|
|
59
|
+
}
|
|
60
|
+
export async function runFrontendTesting(env = process.env) {
|
|
61
|
+
const prompt = await loadFrontendTestingPrompt(env);
|
|
62
|
+
const { command, args } = resolveFrontendTestingCommand(prompt, env);
|
|
63
|
+
const nonInteractive = shouldForceNonInteractive(env);
|
|
64
|
+
const childEnv = { ...process.env, ...env };
|
|
65
|
+
if (nonInteractive) {
|
|
66
|
+
childEnv.CODEX_NON_INTERACTIVE = childEnv.CODEX_NON_INTERACTIVE ?? '1';
|
|
67
|
+
childEnv.CODEX_NO_INTERACTIVE = childEnv.CODEX_NO_INTERACTIVE ?? '1';
|
|
68
|
+
childEnv.CODEX_INTERACTIVE = childEnv.CODEX_INTERACTIVE ?? '0';
|
|
69
|
+
}
|
|
70
|
+
const stdio = nonInteractive ? ['ignore', 'inherit', 'inherit'] : 'inherit';
|
|
71
|
+
const child = spawn(command, args, { stdio, env: childEnv });
|
|
72
|
+
await new Promise((resolvePromise, reject) => {
|
|
73
|
+
child.once('error', (error) => reject(error instanceof Error ? error : new Error(String(error))));
|
|
74
|
+
child.once('exit', (code) => {
|
|
75
|
+
if (code === 0) {
|
|
76
|
+
resolvePromise();
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
reject(new Error(`codex exec exited with code ${code ?? 'unknown'}`));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async function main() {
|
|
85
|
+
await runFrontendTesting();
|
|
86
|
+
}
|
|
87
|
+
const entry = process.argv[1] ? resolve(process.argv[1]) : null;
|
|
88
|
+
const self = resolve(fileURLToPath(import.meta.url));
|
|
89
|
+
if (entry && entry === self) {
|
|
90
|
+
main().catch((error) => {
|
|
91
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { EnvUtils } from '../../../../packages/shared/config/env.js';
|
|
2
|
+
export const DEVTOOLS_CONFIG_OVERRIDE = 'mcp_servers.chrome-devtools.enabled=true';
|
|
3
|
+
export function isDevtoolsEnabled(env = process.env) {
|
|
4
|
+
const raw = env.CODEX_REVIEW_DEVTOOLS;
|
|
5
|
+
if (!raw) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
return EnvUtils.isTrue(raw.trim().toLowerCase());
|
|
9
|
+
}
|
|
10
|
+
export function resolveCodexCommand(args, env = process.env) {
|
|
11
|
+
if (!isDevtoolsEnabled(env)) {
|
|
12
|
+
return { command: 'codex', args };
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
command: 'codex',
|
|
16
|
+
args: ['-c', DEVTOOLS_CONFIG_OVERRIDE, ...args]
|
|
17
|
+
};
|
|
18
|
+
}
|