Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions docs/troubleshooting/repair-harness.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,13 @@ artifact, merge the shard reports, ask Codex for a bounded timing repair, and
independently verify it:

```bash
PHASERFORGE_CODEX_COMMAND=codex \\
npm run repair:ci -- e2e-timing-repair \\
--pr 123 --agent=codex
--pr 123
```

The harness automatically discovers Codex from `PATH` or the VS Code extension
installation and invokes `codex exec` non-interactively.

Use `--run <run-id>` instead of `--pr` when the Actions run is known. The
download is automatic; no artifact or `index.html` download is required. `--pr`
also works when the E2E check passed, because timing failures are distinct from
Expand All @@ -133,9 +135,8 @@ verified change should be committed, pushed on an `agent/*` branch, and opened
as a draft PR:

```bash
PHASERFORGE_CODEX_COMMAND=codex \\
npm run repair:ci -- e2e-timing-repair \\
--pr 123 --agent=codex --publish
--pr 123 --publish
```

The command does not repair warnings (7–10 seconds); only tests over the
Expand Down
34 changes: 28 additions & 6 deletions scripts/repair-harness/agent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { spawn } from 'node:child_process';
import { existsSync, readdirSync } from 'node:fs';
import path from 'node:path';

import type { RepairDiagnosis } from './types';

Expand All @@ -8,14 +10,14 @@ export interface AgentOptions { kind: AgentCallKind; packet: string; cwd: string
export interface AgentResult { kind: AgentCallKind; stdout: string; stderr: string; exitCode: number | null; durationMs: number; packetBytes: number; tokenUsage?: { input?: number; output?: number; total?: number }; }

/**
* Set PHASERFORGE_CODEX_COMMAND to the local Codex executable (and optionally
* PHASERFORGE_CODEX_ARGS as a JSON string array). The adapter passes the packet
* on stdin and never reads, creates, or embeds credentials.
* The adapter discovers Codex from PATH or the VS Code extension install. The
* environment variables remain available for unusual installations. Packets
* are passed on stdin and credentials are never read, created, or embedded.
*/
export function runAgent(options: AgentOptions): Promise<AgentResult> {
const command = options.command ?? process.env.PHASERFORGE_CODEX_COMMAND;
if (!command) return Promise.reject(new Error('Codex agent is disabled; set PHASERFORGE_CODEX_COMMAND to opt in.'));
let configuredArgs: string[] = [];
const command = options.command ?? process.env.PHASERFORGE_CODEX_COMMAND ?? discoverCodexCommand();
if (!command) return Promise.reject(new Error('Codex executable not found. Install Codex or add it to PATH.'));
let configuredArgs: string[] = ['exec', '--sandbox', 'workspace-write'];
if (options.args) configuredArgs = options.args;
else if (process.env.PHASERFORGE_CODEX_ARGS) {
try { configuredArgs = JSON.parse(process.env.PHASERFORGE_CODEX_ARGS) as string[]; } catch { return Promise.reject(new Error('PHASERFORGE_CODEX_ARGS must be a JSON string array.')); }
Expand All @@ -37,6 +39,26 @@ export function runAgent(options: AgentOptions): Promise<AgentResult> {
});
}

export function discoverCodexCommand(): string | undefined {
const pathResult = spawnSync('which', ['codex'], { encoding: 'utf8' });
const fromPath = pathResult.status === 0 ? pathResult.stdout.trim() : '';
if (fromPath) return fromPath;
const home = process.env.HOME;
if (!home) return undefined;
const extensions = path.join(home, '.vscode-server', 'extensions');
if (!existsSync(extensions)) return undefined;
for (const extension of readdirSync(extensions)) {
if (!extension.startsWith('openai.chatgpt-')) continue;
const binRoot = path.join(extensions, extension, 'bin');
if (!existsSync(binRoot)) continue;
for (const platform of readdirSync(binRoot)) {
const candidate = path.join(binRoot, platform, 'codex');
if (existsSync(candidate)) return candidate;
}
}
return undefined;
}

function jsonResponse(text: string): unknown {
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1] ?? text;
const start = fenced.indexOf('{'); const end = fenced.lastIndexOf('}');
Expand Down
1 change: 0 additions & 1 deletion scripts/repair-harness/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ if (args[0] === 'e2e-timing') {
} else if (args[0] === 'e2e-timing-repair') {
try {
if (!value('--pr') && !value('--run')) throw new Error('Expected --pr <number> or --run <run-id> for e2e-timing-repair.');
if (value('--agent') !== 'codex') throw new Error('Automated timing repair requires explicit --agent=codex.');
const result = await runAutomatedTimingRepair({
repo: value('--repo') ?? repositoryRoot,
pr: value('--pr'),
Expand Down
Loading