Files
bitvid/tools/fleet/bin/core/ready.mjs
T
2026-10-26 14:58:19 -04:00

170 lines
7.9 KiB
JavaScript

// Startup readiness. One mailbox round trip, the Codex approval path, health,
// durable deps, and the last checkpoints. No model call and no daemon.
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { send, read, archive } from '../../adapters/mailbox.mjs';
import { fleetHealth } from './health.mjs';
import { readHoldMap, readProgressMap } from './progress.mjs';
const USAGE_RE = /five-hour|usage limit|%\s*remaining|rate limit/i;
export function codexConfigCandidates(repo, env = process.env, home = os.homedir()) {
const root = env.CODEX_HOME || path.join(home, '.codex');
return [path.join(root, 'config.toml'), path.join(repo, '.codex', 'config.toml')];
}
/**
* Read the agent-fleet approval mode out of a Codex config.toml.
* `approve` is the always-allow value. `auto` still requires approval for
* tools that are not marked read-only, and on-request auto-review then runs.
*/
export function parseCodexMcpApproval(text, server = 'agent-fleet') {
let section = '';
const result = { server: false, defaultMode: null, tools: {} };
for (const raw of String(text || '').split('\n')) {
const line = raw.replace(/\s+#.*$/, '').trim();
if (!line || line.startsWith('#')) continue;
const table = /^\[([^\]]+)\]$/.exec(line);
if (table) { section = table[1].trim(); continue; }
const kv = /^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/.exec(line);
if (!kv) continue;
const val = kv[2].trim().replace(/^['"]|['"]$/g, '');
if (section === `mcp_servers.${server}` && kv[1] === 'default_tools_approval_mode') {
result.server = true;
result.defaultMode = val;
}
const tool = new RegExp(`^mcp_servers\\.${server}\\.tools\\.(.+)$`).exec(section);
if (tool && kv[1] === 'approval_mode') {
result.server = true;
result.tools[tool[1]] = val;
}
}
const sendMode = result.tools.mailbox_send;
const tellMode = result.tools.tell;
const mailOk = result.defaultMode === 'approve' || (sendMode === 'approve' && tellMode === 'approve');
return { ...result, mailOk };
}
export function readCodexApproval(repo, opt = {}) {
const files = [];
for (const file of codexConfigCandidates(repo, opt.env, opt.home)) {
if (!file || files.some((row) => row.file === file)) continue;
if (!fs.existsSync(file)) {
files.push({ file, found: false });
continue;
}
const parsed = parseCodexMcpApproval(fs.readFileSync(file, 'utf8'));
files.push({ file, found: true, ...parsed });
}
const hit = files.find((row) => row.mailOk) || files.find((row) => row.found && row.server) || null;
return {
files,
mailOk: Boolean(hit?.mailOk),
mode: hit?.defaultMode ?? null,
file: hit?.file ?? null,
note: 'fleet up runs codex with no --profile and does not set CODEX_HOME. The session reads the same config.toml as any other Codex process in that environment. The server name is agent-fleet. Shell fleet tell is a sandboxed command; default_tools_approval_mode does not apply to it.',
};
}
/** Prove send, read, and archive on an isolated mailbox. Does not wake a pane. */
export function mailboxRoundTrip(dir) {
const probe = path.join(dir, 'probe');
fs.rmSync(probe, { recursive: true, force: true });
const body = `ready ${Date.now()} ${process.pid}`;
try {
fs.mkdirSync(dir, { recursive: true });
const stamp = path.join(dir, `.write-${process.pid}`);
fs.writeFileSync(stamp, 'ok');
fs.unlinkSync(stamp);
const file = send(probe, 'ready', 'ready', body, 'round trip');
const msgs = read(probe, 'ready');
const hit = msgs.find((m) => m.text.includes(body));
if (!hit) return { ok: false, error: 'read did not see the message just written' };
archive(probe, 'ready', hit.file);
const left = read(probe, 'ready');
if (left.length) return { ok: false, error: 'archive left the message unread' };
return { ok: true, file: path.basename(file) };
} catch (e) {
return { ok: false, error: e instanceof Error ? e.message : String(e) };
} finally {
fs.rmSync(probe, { recursive: true, force: true });
}
}
export function depsReady(c) {
const root = path.join(c.stateDir, 'deps');
const manifest = path.join(root, 'manifest.txt');
const missing = [];
if (fs.existsSync(manifest)) {
for (const line of fs.readFileSync(manifest, 'utf8').split('\n')) {
const rel = line.trim();
if (!rel || rel.startsWith('#')) continue;
const file = path.resolve(root, rel);
if (file !== root && !file.startsWith(root + path.sep)) { missing.push(rel); continue; }
if (!fs.existsSync(file)) missing.push(rel);
}
}
const names = ['package.json', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'Cargo.lock', 'go.sum'];
const repoManifests = names.filter((name) => fs.existsSync(path.join(c.repo, name)));
return {
repoManifests,
manifest: fs.existsSync(manifest),
missing,
ok: missing.length === 0,
note: 'Keep runners, lockfiles, pinned archives, and evidence in the worktree or stateDir/deps. /tmp and the npm cache are disposable.',
};
}
export function usageSignal(c) {
const file = path.join(c.stateDir, 'wake-log.jsonl');
if (!fs.existsSync(file)) return { seen: false, line: null };
const lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).slice(-80);
const hit = [...lines].reverse().find((line) => USAGE_RE.test(line)) || null;
return { seen: Boolean(hit), line: hit ? hit.slice(0, 240) : null };
}
export function fleetReady(c, opt = {}) {
const mailbox = mailboxRoundTrip(path.join(c.stateDir, 'ready-mailbox'));
const approval = readCodexApproval(c.repo, opt);
const health = opt.health || fleetHealth(c, { sampleMs: 0, ...(opt.healthOpt || {}) });
const deps = depsReady(c);
const checkpoints = Object.values(readProgressMap(c));
const holds = Object.values(readHoldMap(c));
const usage = usageSignal(c);
return {
ok: mailbox.ok && deps.ok,
mailbox,
approval,
health: { verdict: health.verdict, notes: health.notes || [] },
deps,
checkpoints,
holds,
usage,
};
}
export function formatReady(report) {
const lines = [];
lines.push(report.mailbox.ok ? 'mailbox round trip ok' : `mailbox round trip failed: ${report.mailbox.error}`);
if (report.approval.mailOk) lines.push(`approval ${report.approval.mode || 'per-tool approve'} in ${report.approval.file}`);
else {
const seen = (report.approval.files || []).filter((row) => row.found && row.server);
const where = seen.length ? seen.map((row) => `${row.file} mode=${row.defaultMode || 'unset'}`).join('; ') : 'no [mcp_servers.agent-fleet] table found';
lines.push(`approval want default_tools_approval_mode = "approve" (or per-tool approval_mode = "approve" on mailbox_send and tell). ${where}. auto still asks, and on-request auto-review can reject the call. Offer to write approve only after the user says yes, then reload Codex.`);
}
lines.push(`health ${report.health.verdict}`);
for (const note of report.health.notes || []) lines.push(`health note ${note.words}`);
const manifests = report.deps.repoManifests.length ? report.deps.repoManifests.join(', ') : 'none in the repo root';
lines.push(`deps repo manifests: ${manifests}${report.deps.manifest ? '; stateDir/deps/manifest.txt present' : ''}`);
if (report.deps.missing.length) lines.push(`deps missing ${report.deps.missing.join(', ')}`);
lines.push(`deps note ${report.deps.note}`);
if (!report.checkpoints.length) lines.push('checkpoints none yet');
for (const row of report.checkpoints) lines.push(`checkpoint ${row.area} ${row.at} ${row.line}`);
if (!report.holds.length) lines.push('holds none');
for (const row of report.holds) lines.push(`hold ${row.area} owner ${row.owner}; unblock ${row.unblock}; next ${row.next}`);
if (report.usage.seen) lines.push('usage a recent wake line mentioned a usage limit. Checkpoint and hand off; resume from the checkpoints after the window resets.');
lines.push('resume continue from the checkpoints. A live window is not progress.');
return lines.join('\n');
}