mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 17:02:28 +00:00
170 lines
7.9 KiB
JavaScript
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');
|
|
}
|