@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 `scripts/codex-devtools.sh` when executable; otherwise falls back to `codex -c ...`).
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbediako/codex-orchestrator",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "type": "module",
6
6
  "bin": {