UNPKG

framework

Version:

The (AI) Framework: turnkey, zero-config AI orchestration that wraps a coding-agent CLI (Claude Code) as a black box and takes you from an idea to a running app. Vite for AI.

70 lines 3.22 kB
import { appendFile, mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { isHandoffLevel } from './handoff-level.js'; import { FRAMEWORK_DIR } from './store/index.js'; import { JsonlTailer, followFile } from './jsonl-tail.js'; /** * The dashboard-to-agent control channel (#344): the reverse of the event log. * Events flow run -> `.the-framework/events.jsonl` -> daemon -> browser; steering * flows browser -> daemon -> `.the-framework/control.jsonl` -> run. The daemon * appends a {@link ControlEntry} per Stop click / choice pick, and the agent tails * the file, aborting or resolving its parked gate. Same file-is-the-seam design * as the forward direction — no run<->daemon IPC. */ /** The control log filename under `.the-framework/`. */ export const CONTROL_FILE = 'control.jsonl'; /** The control log path for a workspace. */ export function controlPath(cwd) { return join(cwd, FRAMEWORK_DIR, CONTROL_FILE); } /** Append one entry to the workspace's control log, creating it as needed. */ export async function appendControl(cwd, entry) { await mkdir(join(cwd, FRAMEWORK_DIR), { recursive: true }); await appendFile(controlPath(cwd), JSON.stringify(entry) + '\n'); } /** * Truncate the control log. An agent calls this at start so a previous agent's picks * can never fire into this one (gate ids like `plan-approval` repeat across runs). */ export async function resetControl(cwd) { await mkdir(join(cwd, FRAMEWORK_DIR), { recursive: true }); await writeFile(controlPath(cwd), ''); } /** * Tail the workspace's control log, dispatching each well-formed entry as it is * appended. An `fs.watch` on `.the-framework/` plus a poll backstop, mirroring the * daemon's event tail (`fs.watch` is unreliable across platforms). Malformed or * unknown lines are skipped so a bad write can never crash an agent. */ export function watchControl(cwd, onEntry, pollMs = 300) { const tailer = new JsonlTailer(controlPath(cwd), entry => { if (isControlEntry(entry)) onEntry(entry); }); // unref: never keep the process alive just for steering. const stop = followFile(join(cwd, FRAMEWORK_DIR), () => tailer.pull(), { pollMs, unref: true }); return { close: stop }; } /** Shape-check a parsed line. A multi-select pick may legitimately be `[]`. */ function isControlEntry(value) { if (!value || typeof value !== 'object') return false; const v = value; if (v['kind'] === 'stop') return true; if (v['kind'] === 'merge') return true; // The rung must be one of the four: a half-written entry would otherwise disarm by accident, // and this decides whether the session's work reaches the remote at all. if (v['kind'] === 'handoff') return isHandoffLevel(v['level']); if (v['kind'] === 'message') return typeof v['text'] === 'string' && v['text'].length > 0; if (v['kind'] !== 'choice') return false; if (typeof v['id'] !== 'string' || !v['id']) return false; const pick = v['pick']; return typeof pick === 'string' || (Array.isArray(pick) && pick.every(p => typeof p === 'string')); } //# sourceMappingURL=control.js.map