Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ Untracked hook configs are added to `.git/info/exclude` by default; tracked conf
- `burnlist --stamp` prints a local ISO timestamp for completion records.
- `burnlist install` / `burnlist uninstall` manage the independent agent-skill registrations.
- `burnlist hooks install|uninstall|status` manages the independent per-repository native observability hooks.
- `burnlist-codex-bridge` is the optional `CODEX_CLI_PATH` bridge that lets Codex Desktop and Multi Monitor share one App Server for acknowledged live-task messaging.

Use `burnlist --help` for dashboard ports, scan roots, local state paths, and Oven data bindings.

Expand Down
566 changes: 521 additions & 45 deletions audits/oven/console-oven-behavior-policy.json

Large diffs are not rendered by default.

963 changes: 878 additions & 85 deletions audits/oven/console-oven-behavior.json

Large diffs are not rendered by default.

987 changes: 886 additions & 101 deletions audits/oven/terminal-oven-parity.json

Large diffs are not rendered by default.

230 changes: 230 additions & 0 deletions bin/burnlist-codex-bridge.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
import {
chmodSync,
closeSync,
constants,
existsSync,
lstatSync,
mkdirSync,
openSync,
readFileSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import { connect } from "node:net";
import os from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const MACOS_BUNDLED_CODEX = "/Applications/ChatGPT.app/Contents/Resources/codex";
const CONNECT_TIMEOUT_MS = 10_000;
const SOCKET_PROBE_MS = 2_000;
const STARTUP_LOCK_STALE_MS = 30_000;

export function defaultBridgeSocket(env = process.env, home = os.homedir()) {
return env.BURNLIST_CODEX_APP_SERVER_SOCKET?.trim()
|| join(home, ".codex", "burnlist-app-server", "app-server.sock");
}

export function realCodexBinary(env = process.env, platform = process.platform) {
if (env.BURNLIST_CODEX_BIN?.trim()) return env.BURNLIST_CODEX_BIN.trim();
if (platform === "darwin" && existsSync(MACOS_BUNDLED_CODEX)) return MACOS_BUNDLED_CODEX;
return "codex";
}

export function bridgeLaunchArgs(args, socket) {
const appServer = args.indexOf("app-server");
if (appServer < 0) return null;
const common = args.slice(0, appServer);
const serverOptions = [];
const input = args.slice(appServer + 1);
for (let index = 0; index < input.length; index += 1) {
const value = input[index];
if (value === "--stdio") continue;
if (value === "--listen") {
index += 1;
continue;
}
if (value.startsWith("--listen=")) continue;
serverOptions.push(value);
}
return {
proxy: [...common, "app-server", "proxy", "--sock", socket],
server: [...common, "app-server", ...serverOptions, "--listen", `unix://${socket}`],
};
}

function socketConnection(path, timeoutMs = SOCKET_PROBE_MS) {
return new Promise((resolveConnection) => {
const socket = connect(path);
let settled = false;
const done = (result) => {
if (settled) return;
settled = true;
socket.destroy();
resolveConnection(result);
};
socket.once("connect", () => done({ connected: true, code: null, timedOut: false }));
socket.once("error", (error) => done({
connected: false,
code: error?.code ?? null,
timedOut: false,
}));
socket.setTimeout(timeoutMs, () => done({ connected: false, code: null, timedOut: true }));
});
}

async function waitForSocket(path, child, timeoutMs = CONNECT_TIMEOUT_MS) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (child.exitCode !== null) {
throw new Error(`Shared Codex App Server exited with code ${child.exitCode}.`);
}
if ((await socketConnection(path)).connected) return;
await new Promise((resolveWait) => setTimeout(resolveWait, 50));
}
throw new Error(`Shared Codex App Server did not open ${path}.`);
}

async function staleSocket(path) {
if (!existsSync(path)) return;
const entry = lstatSync(path);
if (!entry.isSocket()) {
throw new Error(`Refusing to replace non-socket bridge path: ${path}`);
}
const probe = await socketConnection(path);
if (probe.connected) return false;
if (probe.timedOut || !["ECONNREFUSED", "ENOENT"].includes(probe.code)) {
throw new Error(`Refusing to replace an unresponsive bridge socket: ${path}`);
}
rmSync(path, { force: true });
return true;
}

function lockOwnerIsDead(path) {
try {
const value = JSON.parse(readFileSync(path, "utf8"));
if (!Number.isSafeInteger(value?.pid) || value.pid < 1) return false;
process.kill(value.pid, 0);
return false;
} catch (error) {
return error?.code === "ESRCH";
}
}

function acquireStartupLock(path) {
let descriptor;
try {
descriptor = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
writeFileSync(descriptor, `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`);
closeSync(descriptor);
return true;
} catch (error) {
if (descriptor !== undefined) closeSync(descriptor);
if (error?.code === "EEXIST") return false;
throw error;
}
}

async function startupLock(path, socket) {
if (acquireStartupLock(path)) return true;
const deadline = Date.now() + CONNECT_TIMEOUT_MS;
while (Date.now() < deadline) {
if ((await socketConnection(socket)).connected) return false;
await new Promise((resolveWait) => setTimeout(resolveWait, 50));
}
const old = Date.now() - statSync(path).mtimeMs >= STARTUP_LOCK_STALE_MS;
if (!old || !lockOwnerIsDead(path)) {
throw new Error(`Shared Codex App Server startup is already in progress: ${path}`);
}
rmSync(path, { force: true });
if (!acquireStartupLock(path)) {
throw new Error(`Could not acquire shared Codex App Server startup lock: ${path}`);
}
return true;
}

function waitForExit(child) {
if (child.exitCode !== null) return Promise.resolve(child.exitCode);
return new Promise((resolveExit, reject) => {
child.once("error", reject);
child.once("exit", (code, signal) => resolveExit(
Number.isInteger(code) ? code : signal ? 1 : 0,
));
});
}

export async function runCodexBridge({
args = process.argv.slice(2),
env = process.env,
spawnProcess = spawn,
} = {}) {
const binary = realCodexBinary(env);
const socket = defaultBridgeSocket(env);
const socketDirectory = dirname(socket);
const lockPath = `${socket}.startup`;
const launch = bridgeLaunchArgs(args, socket);
if (!launch) {
const delegated = spawnProcess(binary, args, { env, shell: false, stdio: "inherit" });
return waitForExit(delegated);
}
mkdirSync(socketDirectory, { recursive: true, mode: 0o700 });
if (!env.BURNLIST_CODEX_APP_SERVER_SOCKET?.trim()) chmodSync(socketDirectory, 0o700);
let server = null;
let ownsLock = false;
if (!(await socketConnection(socket)).connected) {
ownsLock = await startupLock(lockPath, socket);
try {
if (!(await socketConnection(socket)).connected) {
await staleSocket(socket);
server = spawnProcess(binary, launch.server, {
env,
shell: false,
stdio: ["ignore", "ignore", "inherit"],
});
await waitForSocket(socket, server);
}
chmodSync(socket, 0o600);
} catch (error) {
if (server?.exitCode === null) server.kill("SIGTERM");
throw error;
} finally {
if (ownsLock) rmSync(lockPath, { force: true });
}
} else {
chmodSync(socket, 0o600);
}
const proxy = spawnProcess(binary, launch.proxy, { env, shell: false, stdio: "inherit" });
const forward = (signal) => {
if (proxy.exitCode === null) proxy.kill(signal);
};
process.once("SIGINT", forward);
process.once("SIGTERM", forward);
try {
return await waitForExit(proxy);
} finally {
process.removeListener("SIGINT", forward);
process.removeListener("SIGTERM", forward);
if (server?.exitCode === null) server.kill("SIGTERM");
}
}

async function main() {
if (process.argv.includes("--bridge-help")) {
console.log(`Use this executable as CODEX_CLI_PATH to give Codex Desktop and Burnlist
one shared App Server. The default socket is:

${defaultBridgeSocket()}

Override the real Codex binary with BURNLIST_CODEX_BIN and the socket with
BURNLIST_CODEX_APP_SERVER_SOCKET.`);
return;
}
process.exitCode = await runCodexBridge();
}

if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
await main();
}
70 changes: 70 additions & 0 deletions bin/burnlist-codex-bridge.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import assert from "node:assert/strict";
import test from "node:test";

import {
bridgeLaunchArgs,
defaultBridgeSocket,
realCodexBinary,
} from "./burnlist-codex-bridge.mjs";

test("Codex bridge derives one stable local socket", () => {
assert.equal(
defaultBridgeSocket({}, "/tmp/fixture-home"),
"/tmp/fixture-home/.codex/burnlist-app-server/app-server.sock",
);
assert.equal(
defaultBridgeSocket({ BURNLIST_CODEX_APP_SERVER_SOCKET: "/tmp/shared.sock" }, "/unused"),
"/tmp/shared.sock",
);
});

test("Codex bridge preserves Desktop config while replacing stdio with server and proxy", () => {
assert.deepEqual(bridgeLaunchArgs([
"-c",
"features.code_mode_host=true",
"app-server",
"--analytics-default-enabled",
], "/tmp/shared.sock"), {
server: [
"-c",
"features.code_mode_host=true",
"app-server",
"--analytics-default-enabled",
"--listen",
"unix:///tmp/shared.sock",
],
proxy: [
"-c",
"features.code_mode_host=true",
"app-server",
"proxy",
"--sock",
"/tmp/shared.sock",
],
});
assert.equal(bridgeLaunchArgs(["--version"], "/tmp/shared.sock"), null);
});

test("Codex bridge removes every inherited transport before adding its Unix socket", () => {
assert.deepEqual(bridgeLaunchArgs([
"app-server",
"--listen",
"stdio://",
"--listen=ws://127.0.0.1:4500",
"--stdio",
"--analytics-default-enabled",
], "/tmp/shared.sock").server, [
"app-server",
"--analytics-default-enabled",
"--listen",
"unix:///tmp/shared.sock",
]);
});

test("Codex bridge binary override never reads CODEX_CLI_PATH recursively", () => {
assert.equal(realCodexBinary({
BURNLIST_CODEX_BIN: "/opt/codex",
CODEX_CLI_PATH: "/opt/burnlist-codex-bridge",
}, "linux"), "/opt/codex");
assert.equal(realCodexBinary({ CODEX_CLI_PATH: "/opt/burnlist-codex-bridge" }, "linux"), "codex");
});
2 changes: 2 additions & 0 deletions bin/burnlist.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ Options:
--auto-port Try the next available loopback port.
--host <host> Bind host; loopback is required by default.
--state-dir <path> Override ignored dashboard observer state.
--codex-app-server-socket <path>
Share Codex App Server ownership for live-task messaging.
--ovens-dir <path> Override launch-repository custom Oven storage only.
--runs-dir <path> Override Run snapshot storage.
--oven-data <id=path> Bind one Oven to a read-only normalized JSON payload.
Expand Down
10 changes: 5 additions & 5 deletions dashboard/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useMemo, useState } from "react";
import { ListChecks } from "lucide-react";
import { AgentMonitor, AppHeader, BurnlistTable, ChecklistOvenView, CustomOvenView, DashboardError, DifferentialTestingOvenPage, EmptyState, FILTERS, Filters, LensSwitcher, ModelLabPage, NewOvenPage, OvenCatalog, OvenDefinition, OvenExplainer, PerformanceTracingOvenPage, ProjectGroup, RunBurnPage, StreamingDiff, VisualParityPage } from "@components";
import { AgentMonitor, AppHeader, BurnlistTable, ChecklistOvenView, CustomOvenView, DashboardError, DifferentialTestingOvenPage, EmptyState, FILTERS, Filters, LensSwitcher, ModelLabPage, NewOvenPage, OvenCatalog, OvenDefinition, OvenExplainer, PerformanceTracingOvenPage, ProjectGroup, RunBurnPage, StreamingDiff, MultiMonitor, VisualParityPage } from "@components";
import { useDashboardData } from "@hooks";
import { checklistOvenRepoKey, currentSection, customOvenSelection, filterFromUrl, ovenRepoKey, selectedBurnlist } from "@lib";
import type { Filter } from "@lib";
Expand All @@ -12,7 +12,7 @@ export function App() {
const repoKey = ovenRepoKey();
const customOven = section === "custom-oven" ? customOvenSelection() : null;
const [filter, setFilter] = useState(() => filterFromUrl(FILTERS));
const dashboardSection = ["landing", "burnlist", "agent-monitor", "streaming-diff"].includes(section) || (section === "custom-oven" && selected) ? "burnlists" : section;
const dashboardSection = ["landing", "burnlist", "agent-monitor", "streaming-diff", "multi-monitor"].includes(section) || (section === "custom-oven" && selected) ? "burnlists" : section;
const { projects, progress, error, loading, stale } = useDashboardData({ section: dashboardSection, selected });
const checklistRepoKey = checklistOvenRepoKey(progress, selected);
const visibleBurnlistCount = projects.reduce((total, project) => total + project.entries.filter((entry) => filter === "all" || entry.status === filter).length, 0);
Expand All @@ -27,13 +27,13 @@ export function App() {
setFilter(nextFilter);
};

const fullLayout = ["agent-monitor", "differential-testing", "model-lab", "performance-tracing", "streaming-diff", "visual-parity", "custom-oven"].includes(section) || selected;
const fullLayout = ["agent-monitor", "differential-testing", "model-lab", "performance-tracing", "streaming-diff", "multi-monitor", "visual-parity", "custom-oven"].includes(section) || selected;

return (
<div className="dashboard-app">
<AppHeader detail={progress} ovenId={customOven?.id} section={section} />
{section !== "multi-monitor" && <AppHeader detail={progress} ovenId={customOven?.id} section={section} />}
<main className="dashboard-main" data-layout={fullLayout ? "full" : "index"} data-section={section}>
{section === "agent-monitor" ? <OvenDefinition id="agent-monitor" repoKey={repoKey}>{(ir) => <AgentMonitor ir={ir} projects={projects} projectsLoading={loading} />}</OvenDefinition> : section === "differential-testing" ? <OvenDefinition id="differential-testing" repoKey={repoKey}>{(ir) => <DifferentialTestingOvenPage ir={ir} />}</OvenDefinition> : section === "model-lab" ? <OvenDefinition id="model-lab" repoKey={repoKey}>{(ir) => <ModelLabPage ir={ir} />}</OvenDefinition> : section === "performance-tracing" ? <OvenDefinition id="performance-tracing" repoKey={repoKey}>{(ir) => <PerformanceTracingOvenPage ir={ir} />}</OvenDefinition> : section === "streaming-diff" ? <OvenDefinition id="streaming-diff" repoKey={repoKey}>{(ir) => <StreamingDiff ir={ir} projects={projects} projectsLoading={loading} />}</OvenDefinition> : section === "visual-parity" ? <OvenDefinition id="visual-parity" repoKey={repoKey}>{(ir) => <VisualParityPage ir={ir} />}</OvenDefinition> : section === "custom-oven" ? <CustomOvenView error={error} loading={loading} progress={progress} stale={stale} /> : section === "new-oven" ? <NewOvenPage /> : section === "run-burn" ? <RunBurnPage /> : section === "ovens-catalog" ? <OvenCatalog /> : section === "oven-explainer" ? <OvenExplainer /> : selected ? (
{section === "agent-monitor" ? <OvenDefinition id="agent-monitor" repoKey={repoKey}>{(ir) => <AgentMonitor ir={ir} projects={projects} projectsLoading={loading} />}</OvenDefinition> : section === "differential-testing" ? <OvenDefinition id="differential-testing" repoKey={repoKey}>{(ir) => <DifferentialTestingOvenPage ir={ir} />}</OvenDefinition> : section === "model-lab" ? <OvenDefinition id="model-lab" repoKey={repoKey}>{(ir) => <ModelLabPage ir={ir} />}</OvenDefinition> : section === "performance-tracing" ? <OvenDefinition id="performance-tracing" repoKey={repoKey}>{(ir) => <PerformanceTracingOvenPage ir={ir} />}</OvenDefinition> : section === "streaming-diff" ? <OvenDefinition id="streaming-diff" repoKey={repoKey}>{(ir) => <StreamingDiff ir={ir} projects={projects} projectsLoading={loading} />}</OvenDefinition> : section === "multi-monitor" ? <OvenDefinition id="multi-monitor" repoKey={repoKey}>{(ir) => <MultiMonitor ir={ir} projects={projects} projectsLoading={loading} repoKey={repoKey} />}</OvenDefinition> : section === "visual-parity" ? <OvenDefinition id="visual-parity" repoKey={repoKey}>{(ir) => <VisualParityPage ir={ir} />}</OvenDefinition> : section === "custom-oven" ? <CustomOvenView error={error} loading={loading} progress={progress} stale={stale} /> : section === "new-oven" ? <NewOvenPage /> : section === "run-burn" ? <RunBurnPage /> : section === "ovens-catalog" ? <OvenCatalog /> : section === "oven-explainer" ? <OvenExplainer /> : selected ? (
loading && !progress ? <EmptyState title="Loading progress" detail="Reading the selected Burnlist." /> : progress ? (
<>{error && <DashboardError message={error} />}<LensSwitcher /><OvenDefinition id="checklist" repoKey={checklistRepoKey}>{(ir) => <ChecklistOvenView data={progress} ir={ir} />}</OvenDefinition></>
) : error ? <DashboardError message={error} /> : <EmptyState title="Choose a Burnlist" detail="Select an item from the list to inspect its progress." icon={ListChecks} />
Expand Down
3 changes: 2 additions & 1 deletion dashboard/src/components/AppHeader/AppHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ const HEADER_LINKS = [
{ href: "/ovens/new", label: "New Oven", section: "new-oven" },
] as const;

const OVEN_SECTIONS = ["agent-monitor", "custom-oven", "differential-testing", "model-lab", "performance-tracing", "streaming-diff", "visual-parity"];
const OVEN_SECTIONS = ["agent-monitor", "custom-oven", "differential-testing", "model-lab", "performance-tracing", "streaming-diff", "multi-monitor", "visual-parity"];

export function AppHeader({ detail, ovenId, section }: { detail: ChecklistProgressData | null; ovenId?: string | null; section: string }) {
const title = section === "agent-monitor" ? "Agent Monitor"
: section === "differential-testing" ? "Differential Testing"
: section === "model-lab" ? "Model Lab"
: section === "performance-tracing" ? "Performance Tracing"
: section === "streaming-diff" ? "Streaming Diff"
: section === "multi-monitor" ? "Multi Monitor"
: section === "visual-parity" ? "Visual Parity"
: section === "custom-oven" ? detail?.title ?? ovenId : detail?.title;
return (
Expand Down
Loading
Loading