/** * KHAELOR * File: src/cli/headless.ts * Description: Shared headless run driver — one agent turn in an arbitrary cwd, used by subtasks, goal runs, and /replay (v2 design §2, §6, §7). * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import type { ResolvedConfig } from "../config/index.js"; import { buildUsageTotals, writeSessionMeta } from "../session/index.js"; import type { DurableEvent, ModelUsageTotals } from "../session/index.js"; import { assembleEngine, openSessionContext } from "./engine.js"; import type { FileLogger } from "./logger.js"; import { finalAnswerText } from "./print.js"; export interface HeadlessRunResult { outcome: "done" | "failed" | "interrupted" | "idle"; detail: string; sessionId: string; finalText: string; usage: ModelUsageTotals; /** Latest native verify round: true = all checks passed, null = no checks ran. */ verifyOk: boolean | null; } /** Latest verify verdict per check, folded to one boolean (null = never verified). */ export function latestVerifyVerdict(events: readonly DurableEvent[]): boolean | null { const latest = new Map(); for (const event of events) { if (event.type === "verify.result") latest.set(event.payload.check, event.payload.ok); } if (latest.size === 0) return null; return [...latest.values()].every((ok) => ok); } export interface HeadlessRunOptions { config: ResolvedConfig; cwd: string; prompt: string; logger: FileLogger; sessionsDir?: string; sessionId?: string; /** Lineage recorded in .meta.json (v2 §2/§6). */ meta?: { parent: string | null; replayOf?: string }; } /** * One full agent turn without a TUI: non-interactive permissions (asks * resolve deny), phase gates in the configured mode, native verification * active. This is the execution primitive for subtasks (§6), daemon goal * runs (§7), and /replay (§2). */ export async function runHeadlessTurn(options: HeadlessRunOptions): Promise { const context = await openSessionContext({ cwd: options.cwd, ...(options.sessionsDir !== undefined ? { sessionsDir: options.sessionsDir } : {}), logger: options.logger, }); const engine = await assembleEngine({ config: options.config, cwd: options.cwd, context, logger: options.logger, }); try { if (options.meta !== undefined) { const sessionsDirOf = context.log.filePath.slice( 0, context.log.filePath.length - `/${context.hash}/${context.sessionId}.jsonl`.length, ); await writeSessionMeta(sessionsDirOf, context.hash, context.sessionId, { parent: options.meta.parent, forkPoint: null, ...(options.meta.replayOf !== undefined ? { replayOf: options.meta.replayOf } : {}), createdAt: Date.now(), }); } await engine.captureBaseline(); engine.session.publishDurable({ type: "user.message-created", payload: { text: options.prompt, mentions: [] }, }); const outcome = await engine.runTurn(); const events = engine.session.events(); return { outcome: outcome.kind === "done" ? "done" : outcome.kind === "failed" ? "failed" : outcome.kind === "interrupted" ? "interrupted" : "idle", detail: outcome.kind === "failed" ? outcome.detail : "", sessionId: context.sessionId, finalText: finalAnswerText(events), usage: buildUsageTotals(events).totals, verifyOk: latestVerifyVerdict(events), }; } finally { await engine.shutdown(); } }