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/verify/runner.ts4 * Description: Native verification runner — parallel checks, errors-first truncation, bounded repair loop accounting (v2 design §4).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import type { DurableEvent, DurableEventInput } from "../session/index.js";11import type { Workspace } from "../workspace/index.js";12import type { VerifyCheck, VerifyConfig } from "./config.js";1314/** The session seam the runner needs — satisfied by EventLogSession. */15export interface VerifySessionHandle {16 events(): readonly DurableEvent[];17 publishDurable(input: DurableEventInput): unknown;18}1920export interface VerifyOutcome {21 ok: boolean;22 results: { check: string; ok: boolean; exitCode: number | null; durationMs: number }[];23}2425const OUTPUT_BUDGET = 4000;26const ERROR_LINE = /error|fail|✗|FAIL|Err\b|exception/i;2728/**29 * Errors-first truncation: lines that look like failures are kept ahead of30 * everything else, then head-fill up to the budget (v2 §4 — "tronqué31 * intelligemment: erreurs d'abord").32 */33export function truncateErrorsFirst(output: string, budget = OUTPUT_BUDGET): string {34 if (output.length <= budget) return output;35 const lines = output.split("\n");36 const errorLines: string[] = [];37 const otherLines: string[] = [];38 for (const line of lines) {39 if (ERROR_LINE.test(line)) errorLines.push(line);40 else otherLines.push(line);41 }42 const kept: string[] = [];43 let used = 0;44 for (const line of [...errorLines, ...otherLines]) {45 if (used + line.length + 1 > budget) break;46 kept.push(line);47 used += line.length + 1;48 }49 return `${kept.join("\n")}\n[... verify output truncated: errors shown first]`;50}5152/** Count failing verify results since the last user message — the repair-loop bound. */53export function countRepairFailures(events: readonly DurableEvent[]): number {54 let count = 0;55 for (const event of events) {56 if (event.type === "user.message-created") count = 0;57 else if (event.type === "verify.result" && !event.payload.ok) count += 1;58 }59 return count;60}6162export interface VerifyRunnerOptions {63 workspace: Workspace;64 session: VerifySessionHandle;65 config: VerifyConfig;66}6768/**69 * Runs the configured checks through the workspace seam, publishes one70 * durable `verify.result` per check, and enforces the bounded repair loop:71 * once `maxRepairLoops` failing rounds have been recorded since the last72 * user message, the runner stops re-running — the agent reports honestly73 * instead of looping (v2 §4).74 */75export class VerifyRunner {76 readonly #workspace: Workspace;77 readonly #session: VerifySessionHandle;78 readonly #config: VerifyConfig;7980 constructor(options: VerifyRunnerOptions) {81 this.#workspace = options.workspace;82 this.#session = options.session;83 this.#config = options.config;84 }8586 get policy(): VerifyConfig["policy"] {87 return this.#config.policy;88 }8990 get hasChecks(): boolean {91 return this.#config.checks.length > 0;92 }9394 /** True when another failing round is still within the repair budget. */95 withinRepairBudget(): boolean {96 return countRepairFailures(this.#session.events()) < this.#config.maxRepairLoops;97 }9899 /** Run all checks (autofix checks last, serially) and record the results. */100 async runAll(signal?: AbortSignal): Promise<VerifyOutcome> {101 const parallel = this.#config.checks.filter((check) => !check.autofix);102 const fixers = this.#config.checks.filter((check) => check.autofix);103104 const results: VerifyOutcome["results"] = [];105 const settled = await Promise.all(parallel.map((check) => this.#runOne(check, signal)));106 results.push(...settled);107 for (const fixer of fixers) {108 if (signal?.aborted === true) break;109 results.push(await this.#runOne(fixer, signal));110 }111 return { ok: results.every((result) => result.ok), results };112 }113114 async #runOne(115 check: VerifyCheck,116 signal?: AbortSignal,117 ): Promise<VerifyOutcome["results"][number]> {118 const startedAt = Date.now();119 let exitCode: number | null = null;120 let output = "";121 try {122 const result = await this.#workspace.exec({123 cmd: check.cmd,124 timeoutMs: check.timeoutMs,125 ...(signal !== undefined ? { signal } : {}),126 });127 exitCode = result.exitCode;128 output = [result.stdout, result.stderr].filter((part) => part.length > 0).join("\n");129 } catch (error) {130 exitCode = null;131 output = error instanceof Error ? error.message : String(error);132 }133 const ok = exitCode === 0;134 const durationMs = Date.now() - startedAt;135 this.#session.publishDurable({136 type: "verify.result",137 payload: {138 check: check.name,139 command: check.cmd,140 ok,141 exitCode,142 output: ok ? "" : truncateErrorsFirst(output),143 durationMs,144 },145 });146 return { check: check.name, ok, exitCode, durationMs };147 }148}149