spb/khaelor Public
KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.
TypeScript 82.9%
HTML 14.9%
CSS 1.1%
JavaScript 0.7%
1/**2 * KHAELOR3 * File: src/cli/headless.ts4 * Description: Shared headless run driver — one agent turn in an arbitrary cwd, used by subtasks, goal runs, and /replay (v2 design §2, §6, §7).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import type { ResolvedConfig } from "../config/index.js";11import { buildUsageTotals, writeSessionMeta } from "../session/index.js";12import type { DurableEvent, ModelUsageTotals } from "../session/index.js";13import { assembleEngine, openSessionContext } from "./engine.js";14import type { FileLogger } from "./logger.js";15import { finalAnswerText } from "./print.js";1617export interface HeadlessRunResult {18 outcome: "done" | "failed" | "interrupted" | "idle";19 detail: string;20 sessionId: string;21 finalText: string;22 usage: ModelUsageTotals;23 /** Latest native verify round: true = all checks passed, null = no checks ran. */24 verifyOk: boolean | null;25}2627/** Latest verify verdict per check, folded to one boolean (null = never verified). */28export function latestVerifyVerdict(events: readonly DurableEvent[]): boolean | null {29 const latest = new Map<string, boolean>();30 for (const event of events) {31 if (event.type === "verify.result") latest.set(event.payload.check, event.payload.ok);32 }33 if (latest.size === 0) return null;34 return [...latest.values()].every((ok) => ok);35}3637export interface HeadlessRunOptions {38 config: ResolvedConfig;39 cwd: string;40 prompt: string;41 logger: FileLogger;42 sessionsDir?: string;43 sessionId?: string;44 /** Lineage recorded in <sessionId>.meta.json (v2 §2/§6). */45 meta?: { parent: string | null; replayOf?: string };46}4748/**49 * One full agent turn without a TUI: non-interactive permissions (asks50 * resolve deny), phase gates in the configured mode, native verification51 * active. This is the execution primitive for subtasks (§6), daemon goal52 * runs (§7), and /replay (§2).53 */54export async function runHeadlessTurn(options: HeadlessRunOptions): Promise<HeadlessRunResult> {55 const context = await openSessionContext({56 cwd: options.cwd,57 ...(options.sessionsDir !== undefined ? { sessionsDir: options.sessionsDir } : {}),58 logger: options.logger,59 });60 const engine = await assembleEngine({61 config: options.config,62 cwd: options.cwd,63 context,64 logger: options.logger,65 });66 try {67 if (options.meta !== undefined) {68 const sessionsDirOf = context.log.filePath.slice(69 0,70 context.log.filePath.length - `/${context.hash}/${context.sessionId}.jsonl`.length,71 );72 await writeSessionMeta(sessionsDirOf, context.hash, context.sessionId, {73 parent: options.meta.parent,74 forkPoint: null,75 ...(options.meta.replayOf !== undefined ? { replayOf: options.meta.replayOf } : {}),76 createdAt: Date.now(),77 });78 }79 await engine.captureBaseline();80 engine.session.publishDurable({81 type: "user.message-created",82 payload: { text: options.prompt, mentions: [] },83 });84 const outcome = await engine.runTurn();85 const events = engine.session.events();86 return {87 outcome: outcome.kind === "done" ? "done" : outcome.kind === "failed" ? "failed" : outcome.kind === "interrupted" ? "interrupted" : "idle",88 detail: outcome.kind === "failed" ? outcome.detail : "",89 sessionId: context.sessionId,90 finalText: finalAnswerText(events),91 usage: buildUsageTotals(events).totals,92 verifyOk: latestVerifyVerdict(events),93 };94 } finally {95 await engine.shutdown();96 }97}98