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: prototypes/tui-spike/driver/run.ts4 * Description: Headless measurement driver — runs a candidate inside a real pty (node-pty),5 * injects synthetic typing at 30 cps during streaming, resizes the pty mid-stream,6 * sends Esc to end the run, then collects the candidate's in-process metrics and7 * performs settled-content integrity checks on the captured output.8 *9 * Author: Simon-Pierre Boucher10 * Contact: contact@spboucher.ai11 */1213import fs from "node:fs";14import path from "node:path";15import { fileURLToPath } from "node:url";16import pty from "node-pty";17import { stripAnsi } from "../shared/ansi";18import { FENCE_TEXT } from "../shared/demo";1920const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");21const cand = process.argv[2];22if (cand !== "a" && cand !== "b") {23 console.error("usage: tsx driver/run.ts <a|b> (DURATION_MS env, default 60000)");24 process.exit(1);25}26const durationMs = Number(process.env.DURATION_MS ?? 60_000);27const entry = cand === "a" ? "candidate-a/main.ts" : "candidate-b/main.tsx";28const metricsOut = path.join(root, "out", `metrics-${cand}.json`);29const capturePath = path.join(root, "out", `capture-${cand}.txt`);30fs.mkdirSync(path.join(root, "out"), { recursive: true });31if (fs.existsSync(metricsOut)) fs.unlinkSync(metricsOut);3233const p = pty.spawn(process.execPath, [path.join(root, "node_modules", "tsx", "dist", "cli.mjs"), entry], {34 name: "xterm-256color",35 cols: 100,36 rows: 30,37 cwd: root,38 env: { ...process.env, METRICS_OUT: metricsOut, DURATION_MS: String(durationMs), FORCE_COLOR: "1" },39});4041let capture = "";42p.onData((d) => {43 capture += d;44});4546/* synthetic typing: 30 cps while the stream runs; Enter every 40 chars keeps the47 composer line short enough to survive the 60-column resize phase. */48const phrase = "check the session store retry logic and rerun the tests then verify the journal ";49let typed = 0;50let typeTimer: NodeJS.Timeout | null = null;51setTimeout(() => {52 typeTimer = setInterval(() => {53 if (typed > 0 && typed % 40 === 0) p.write("\r");54 p.write(phrase[typed % phrase.length]);55 typed++;56 }, 33);57}, 1500);58setTimeout(() => {59 if (typeTimer !== null) clearInterval(typeTimer);60}, Math.max(2000, durationMs - 5000));6162/* resize the pty mid-stream (delivers a real SIGWINCH + winsize change to the child) */63const resizePlan: Array<[number, number, number]> = [64 [0.33, 120, 30],65 [0.42, 60, 30],66 [0.5, 200, 45],67 [0.58, 100, 30],68];69for (const [frac, c, r] of resizePlan) {70 setTimeout(() => {71 try {72 p.resize(c, r);73 } catch {74 /* child may have exited */75 }76 }, Math.round(durationMs * frac));77}7879/* end of run: Esc, then hard kill as a backstop */80setTimeout(() => p.write("\x1b"), durationMs);81const killer = setTimeout(() => p.kill(), durationMs + 8000);8283p.onExit(({ exitCode }) => {84 clearTimeout(killer);85 if (typeTimer !== null) clearInterval(typeTimer);86 fs.writeFileSync(capturePath, capture);8788 const stripped = stripAnsi(capture).replace(/\r/g, "");89 const fenceIntact = stripped.includes(FENCE_TEXT);90 const fullClearsInCapture = (capture.match(/\x1b\[[23]J/g) ?? []).length;9192 let metrics: unknown = null;93 try {94 metrics = JSON.parse(fs.readFileSync(metricsOut, "utf8"));95 } catch {96 console.error(`no metrics file at ${metricsOut} — child exit code ${exitCode}`);97 }9899 const summary = {100 candidate: cand,101 childExitCode: exitCode,102 durationMs,103 typedChars: typed,104 driverChecks: {105 settledFenceBlockByteIdentical: fenceIntact,106 fullScreenClearsInCapture: fullClearsInCapture,107 captureBytes: capture.length,108 capturePath,109 },110 metrics,111 };112 const summaryPath = path.join(root, "out", `summary-${cand}.json`);113 fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2) + "\n");114 console.log(JSON.stringify(summary, null, 2));115 process.exit(0);116});117