SPB Git

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%
3.5 KB · 98 lines typescript
Raw Blame History
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