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.9 KB · 117 lines typescript
Raw Blame History
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