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/config.ts4 * Description: Verification config — .khaelor/verify.json loading and project auto-detection (v2 design §4).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import type { Workspace } from "../workspace/index.js";1112export type VerifyPolicy = "after-each-edit-batch" | "before-final-answer" | "off";1314export interface VerifyCheck {15 name: string;16 cmd: string;17 timeoutMs: number;18 /** The check mutates files (e.g. eslint --fix) — run it last. */19 autofix: boolean;20}2122export interface VerifyConfig {23 checks: VerifyCheck[];24 policy: VerifyPolicy;25 /** Bounded repair loop: at most this many failing-verify → model-repair rounds per user turn. */26 maxRepairLoops: number;27}2829export const VERIFY_FILE = ".khaelor/verify.json";30export const DEFAULT_MAX_REPAIR_LOOPS = 3;31const DEFAULT_TIMEOUT_MS = 120_000;3233const POLICIES: readonly string[] = ["after-each-edit-batch", "before-final-answer", "off"];3435/** Parse a raw verify.json object. Invalid entries are skipped, never fatal. */36export function parseVerifyConfig(raw: unknown): VerifyConfig | null {37 if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;38 const record = raw as Record<string, unknown>;39 const checks: VerifyCheck[] = [];40 for (const [name, value] of Object.entries(record)) {41 if (name === "policy" || name === "maxRepairLoops") continue;42 if (value === null || typeof value !== "object" || Array.isArray(value)) continue;43 const entry = value as Record<string, unknown>;44 const cmd = entry["cmd"];45 if (typeof cmd !== "string" || cmd.length === 0) continue;46 const timeout = entry["timeout"];47 const timeoutMs =48 typeof timeout === "number" && timeout > 0 ? Math.round(timeout * 1000) : DEFAULT_TIMEOUT_MS;49 checks.push({ name, cmd, timeoutMs, autofix: entry["autofix"] === true });50 }51 const policyRaw = record["policy"];52 const policy: VerifyPolicy =53 typeof policyRaw === "string" && POLICIES.includes(policyRaw)54 ? (policyRaw as VerifyPolicy)55 : "after-each-edit-batch";56 const loopsRaw = record["maxRepairLoops"];57 const maxRepairLoops =58 typeof loopsRaw === "number" && Number.isInteger(loopsRaw) && loopsRaw >= 059 ? loopsRaw60 : DEFAULT_MAX_REPAIR_LOOPS;61 return { checks, policy, maxRepairLoops };62}6364async function readJson(workspace: Workspace, path: string): Promise<unknown | null> {65 try {66 return JSON.parse(await workspace.readFile(path)) as unknown;67 } catch {68 return null;69 }70}7172async function exists(workspace: Workspace, path: string): Promise<boolean> {73 try {74 await workspace.readFile(path);75 return true;76 } catch {77 return false;78 }79}8081/**82 * Auto-detect checks from the project (package.json scripts, tsconfig,83 * Cargo.toml, pyproject.toml). Best-effort and conservative: only well-known84 * commands, never destructive ones.85 */86export async function detectVerifyChecks(workspace: Workspace): Promise<VerifyCheck[]> {87 const cwd = workspace.cwd();88 const checks: VerifyCheck[] = [];8990 const pkg = await readJson(workspace, `${cwd}/package.json`);91 if (pkg !== null && typeof pkg === "object" && !Array.isArray(pkg)) {92 const scripts = (pkg as Record<string, unknown>)["scripts"];93 const scriptSet =94 scripts !== null && typeof scripts === "object" && !Array.isArray(scripts)95 ? (scripts as Record<string, unknown>)96 : {};97 if (typeof scriptSet["typecheck"] === "string") {98 checks.push({ name: "typecheck", cmd: "npm run typecheck", timeoutMs: DEFAULT_TIMEOUT_MS, autofix: false });99 } else if (await exists(workspace, `${cwd}/tsconfig.json`)) {100 checks.push({ name: "typecheck", cmd: "npx tsc --noEmit", timeoutMs: DEFAULT_TIMEOUT_MS, autofix: false });101 }102 if (typeof scriptSet["test"] === "string") {103 checks.push({ name: "test", cmd: "npm test", timeoutMs: 300_000, autofix: false });104 }105 if (typeof scriptSet["lint"] === "string") {106 checks.push({ name: "lint", cmd: "npm run lint", timeoutMs: DEFAULT_TIMEOUT_MS, autofix: false });107 }108 return checks;109 }110111 if (await exists(workspace, `${cwd}/Cargo.toml`)) {112 checks.push({ name: "check", cmd: "cargo check", timeoutMs: 300_000, autofix: false });113 checks.push({ name: "test", cmd: "cargo test", timeoutMs: 600_000, autofix: false });114 return checks;115 }116117 if (await exists(workspace, `${cwd}/pyproject.toml`)) {118 checks.push({ name: "test", cmd: "python3 -m pytest -x -q", timeoutMs: 300_000, autofix: false });119 return checks;120 }121122 return checks;123}124125/**126 * Load the effective verify config: .khaelor/verify.json when present,127 * auto-detection otherwise (v2 §4 — surchargeable).128 */129export async function loadVerifyConfig(workspace: Workspace): Promise<VerifyConfig> {130 const raw = await readJson(workspace, `${workspace.cwd()}/${VERIFY_FILE}`);131 if (raw !== null) {132 const parsed = parseVerifyConfig(raw);133 if (parsed !== null) return parsed;134 }135 return {136 checks: await detectVerifyChecks(workspace),137 policy: "after-each-edit-batch",138 maxRepairLoops: DEFAULT_MAX_REPAIR_LOOPS,139 };140}141