import { readFile, writeFile } from "node:fs/promises"; import { randomUUID } from "node:crypto"; import { ensureBatonScaffolding, ensureRunDirs, getActiveRunPointerPath, getRunManifestPath, getRunStepsDir, } from "./paths.ts"; import type { ActiveRunPointer, RunManifest, RunState, StepRecord } from "./types.ts"; function nowIso(): string { return new Date().toISOString(); } async function readJson(filePath: string): Promise { const text = await readFile(filePath, "utf8"); return JSON.parse(text) as T; } async function writeJson(filePath: string, value: unknown): Promise { await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); } export class ActiveRunGuardError extends Error { constructor(message: string) { super(message); this.name = "ActiveRunGuardError"; } } export async function readActiveRunPointer(cwd: string): Promise { try { return await readJson(getActiveRunPointerPath(cwd)); } catch { return null; } } export async function readRunManifest(cwd: string, runId: string): Promise { return readJson(getRunManifestPath(cwd, runId)); } function isTerminalRunState(state: RunState): boolean { return state === "completed" || state === "failed"; } export async function loadActiveRun(cwd: string): Promise { const pointer = await readActiveRunPointer(cwd); if (!pointer?.runId) return null; try { const manifest = await readRunManifest(cwd, pointer.runId); if (isTerminalRunState(manifest.state)) { return null; } return manifest; } catch { return null; } } export async function loadMostRecentTerminalRun(cwd: string): Promise { const pointer = await readActiveRunPointer(cwd); if (!pointer?.runId) return null; try { const manifest = await readRunManifest(cwd, pointer.runId); if (!isTerminalRunState(manifest.state)) { return null; } return manifest; } catch { return null; } } export async function saveRunManifest(cwd: string, manifest: RunManifest): Promise { manifest.updatedAt = nowIso(); await writeJson(getRunManifestPath(cwd, manifest.id), manifest); } export async function setActiveRunPointer(cwd: string, runId: string): Promise { await writeJson(getActiveRunPointerPath(cwd), { runId } satisfies ActiveRunPointer); } export async function clearActiveRunPointer(cwd: string): Promise { await writeJson(getActiveRunPointerPath(cwd), { runId: null }); } export interface CreateRunInput { workflowId: string; workflowName: string; workflowPath: string; workflowSource: "user" | "builtin"; taskBrief: string; targetDirectory: string; entryStep: string; iterationCap: number; } export async function createIdleRun(cwd: string, input: CreateRunInput): Promise { await ensureBatonScaffolding(cwd); const active = await loadActiveRun(cwd); if (active) { throw new ActiveRunGuardError( `Active run ${active.id} is ${active.state}. Use /baton:status or /baton:run before starting a new run.`, ); } const timestamp = new Date().toISOString().replace(/[-:TZ.]/g, "").slice(0, 14); const runId = `${timestamp}-${randomUUID().slice(0, 8)}`; await ensureRunDirs(cwd, runId); const manifest: RunManifest = { id: runId, state: "idle", workflowId: input.workflowId, workflowName: input.workflowName, workflowPath: input.workflowPath, workflowSource: input.workflowSource, taskBrief: input.taskBrief, targetDirectory: input.targetDirectory, entryStep: input.entryStep, currentStep: input.entryStep, lastStep: null, iteration: 0, iterationCap: input.iterationCap, createdAt: nowIso(), updatedAt: nowIso(), }; await saveRunManifest(cwd, manifest); await setActiveRunPointer(cwd, runId); return manifest; } export async function updateRunState( cwd: string, runId: string, patch: Partial & { state?: RunState }, ): Promise { const manifest = await readRunManifest(cwd, runId); const next = { ...manifest, ...patch, updatedAt: nowIso() }; await saveRunManifest(cwd, next); return next; } export async function writeStepRecord(cwd: string, runId: string, record: StepRecord): Promise { const fileName = `${record.stepName}-${record.iteration}-${record.finishedAt.replace(/[:.]/g, "")}.json`; const filePath = `${getRunStepsDir(cwd, runId)}/${fileName}`; await writeJson(filePath, record); return filePath; }