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