haansi 0.1.53 → 0.1.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/haansi.js +536 -51
  2. package/package.json +1 -1
package/dist/haansi.js CHANGED
@@ -95,7 +95,7 @@ var require_package = __commonJS({
95
95
  "package.json"(exports2, module2) {
96
96
  module2.exports = {
97
97
  name: "haansi",
98
- version: "0.1.53",
98
+ version: "0.1.55",
99
99
  description: "Haansi CLI - Session collector and MCP server for Claude Code & OpenAI Codex",
100
100
  bin: {
101
101
  haansi: "./dist/haansi.js"
@@ -19236,6 +19236,77 @@ var init_exclusions = __esm({
19236
19236
  "Actions workflow names from CI config that passed on the fix."
19237
19237
  ]
19238
19238
  }),
19239
+ ...exchangeExclusions(INTEL, "stored_replay_tasks", {
19240
+ task_key: [
19241
+ "join-key",
19242
+ "The task's kind with the workstream id or owner/repo#PR numbers it was cut from; upserted by equality."
19243
+ ],
19244
+ kind: [
19245
+ "operational",
19246
+ "Closed CHECK vocabulary (full, milestone, recovery)."
19247
+ ],
19248
+ process_id: [
19249
+ "join-key",
19250
+ "The workstream's process_registry.process_id, joined by equality."
19251
+ ],
19252
+ repository: [
19253
+ "join-key",
19254
+ "owner/repo of the task's PRs, as in workstream_attempts."
19255
+ ],
19256
+ base_sha: ["not-personal", "A commit SHA."],
19257
+ target_sha: ["not-personal", "A commit SHA."],
19258
+ checks: [
19259
+ "not-personal",
19260
+ "Actions workflow names from CI config that passed on the target."
19261
+ ],
19262
+ score_parts: [
19263
+ "not-personal",
19264
+ "Counts and flags the replayability score rests on (files, areas, context records, gradable by tests or checks); no text."
19265
+ ],
19266
+ inputs_fingerprint: [
19267
+ "derived",
19268
+ "A SHA-256 of the task's inputs; not reversible."
19269
+ ],
19270
+ card_state: [
19271
+ "operational",
19272
+ "Closed CHECK vocabulary (ready, dropped, invalid)."
19273
+ ],
19274
+ card_dropped_reason: [
19275
+ "operational",
19276
+ "Closed CHECK vocabulary (leak, insufficient)."
19277
+ ],
19278
+ card_fingerprint: [
19279
+ "derived",
19280
+ "A SHA-256 of what the card was written from; not reversible."
19281
+ ],
19282
+ card_model: ["not-personal", "The model id that wrote the card."],
19283
+ card_prompt_tag: [
19284
+ "not-personal",
19285
+ "The prompt version that wrote the card (replay-task-card-vN)."
19286
+ ],
19287
+ calibration_state: [
19288
+ "operational",
19289
+ "Closed CHECK vocabulary (valid, invalid, flaky, error)."
19290
+ ],
19291
+ calibration_reason: [
19292
+ "operational",
19293
+ "Closed CHECK vocabulary (passes_at_base, fails_at_target, \u2026)."
19294
+ ],
19295
+ calibration_inputs_fingerprint: [
19296
+ "derived",
19297
+ "A SHA-256 of the task's inputs; not reversible."
19298
+ ],
19299
+ calibration_env_fingerprint: [
19300
+ "derived",
19301
+ "A SHA-256 of the CI environment; not reversible."
19302
+ ],
19303
+ calibration_environment: [
19304
+ "not-personal",
19305
+ "The CI's setup and test commands an org admin or CI owner typed, runtime versions, the runner image and the CLI version."
19306
+ ],
19307
+ calibration_runs: ["operational", "Exit codes of the test runs."],
19308
+ calibration_run_ref: ["not-personal", "A link to the customer's CI run."]
19309
+ }),
19239
19310
  ...exchangeExclusions(INTEL, "problem_family_members", {
19240
19311
  process_id: [
19241
19312
  "join-key",
@@ -26495,6 +26566,25 @@ var init_table_registry = __esm({
26495
26566
  entry("intelligence", INTELLIGENCE_DB, "replay_tasks", "direct", {
26496
26567
  scopingColumn: "organization_id"
26497
26568
  }),
26569
+ // Stored replay tasks proposed by the ledger (replay tasks v2 plan §4).
26570
+ entry("intelligence", INTELLIGENCE_DB, "stored_replay_tasks", "direct", {
26571
+ scopingColumn: "organization_id"
26572
+ }),
26573
+ // A person's revert verdicts, the checks that validate a change and the
26574
+ // replay commands, per repository (outcome evidence v2 plan).
26575
+ entry("intelligence", INTELLIGENCE_DB, "revert_labels", "direct", {
26576
+ scopingColumn: "organization_id"
26577
+ }),
26578
+ entry("intelligence", INTELLIGENCE_DB, "repository_checks", "direct", {
26579
+ scopingColumn: "organization_id"
26580
+ }),
26581
+ entry(
26582
+ "intelligence",
26583
+ INTELLIGENCE_DB,
26584
+ "repository_replay_settings",
26585
+ "direct",
26586
+ { scopingColumn: "organization_id" }
26587
+ ),
26498
26588
  entry("intelligence", INTELLIGENCE_DB, "problem_family_feedback", "direct", {
26499
26589
  scopingColumn: "organization_id"
26500
26590
  }),
@@ -77377,7 +77467,7 @@ var require_view = __commonJS({
77377
77467
  var dirname5 = path2.dirname;
77378
77468
  var basename4 = path2.basename;
77379
77469
  var extname = path2.extname;
77380
- var join16 = path2.join;
77470
+ var join17 = path2.join;
77381
77471
  var resolve5 = path2.resolve;
77382
77472
  module2.exports = View;
77383
77473
  function View(name, options) {
@@ -77439,12 +77529,12 @@ var require_view = __commonJS({
77439
77529
  };
77440
77530
  View.prototype.resolve = function resolve6(dir, file2) {
77441
77531
  var ext = this.ext;
77442
- var path3 = join16(dir, file2);
77532
+ var path3 = join17(dir, file2);
77443
77533
  var stat = tryStat(path3);
77444
77534
  if (stat && stat.isFile()) {
77445
77535
  return path3;
77446
77536
  }
77447
- path3 = join16(dir, basename4(file2, ext), "index" + ext);
77537
+ path3 = join17(dir, basename4(file2, ext), "index" + ext);
77448
77538
  stat = tryStat(path3);
77449
77539
  if (stat && stat.isFile()) {
77450
77540
  return path3;
@@ -81089,7 +81179,7 @@ var require_send = __commonJS({
81089
81179
  var Stream = require("stream");
81090
81180
  var util2 = require("util");
81091
81181
  var extname = path2.extname;
81092
- var join16 = path2.join;
81182
+ var join17 = path2.join;
81093
81183
  var normalize = path2.normalize;
81094
81184
  var resolve5 = path2.resolve;
81095
81185
  var sep2 = path2.sep;
@@ -81261,7 +81351,7 @@ var require_send = __commonJS({
81261
81351
  return res;
81262
81352
  }
81263
81353
  parts = path3.split(sep2);
81264
- path3 = normalize(join16(root, path3));
81354
+ path3 = normalize(join17(root, path3));
81265
81355
  } else {
81266
81356
  if (UP_PATH_REGEXP.test(path3)) {
81267
81357
  debug('malicious path "%s"', path3);
@@ -81394,7 +81484,7 @@ var require_send = __commonJS({
81394
81484
  if (err2) return self.onStatError(err2);
81395
81485
  return self.error(404);
81396
81486
  }
81397
- var p = join16(path3, self._index[i]);
81487
+ var p = join17(path3, self._index[i]);
81398
81488
  debug('stat "%s"', p);
81399
81489
  fs.stat(p, function(err3, stat) {
81400
81490
  if (err3) return next(err3);
@@ -85105,10 +85195,345 @@ var init_skills = __esm({
85105
85195
  }
85106
85196
  });
85107
85197
 
85198
+ // src/commands/replay-calibrate.ts
85199
+ var replay_calibrate_exports = {};
85200
+ __export(replay_calibrate_exports, {
85201
+ RUNTIMES: () => RUNTIMES,
85202
+ calibrate: () => calibrate,
85203
+ calibrateTask: () => calibrateTask,
85204
+ environmentOf: () => environmentOf,
85205
+ parseCalibrateArgs: () => parseCalibrateArgs,
85206
+ runCalibration: () => runCalibration,
85207
+ runnerOf: () => runnerOf
85208
+ });
85209
+ function parseCalibrateArgs(argv) {
85210
+ const options = {
85211
+ setupCmd: null,
85212
+ testCmd: "",
85213
+ repoCommands: false,
85214
+ runs: 2,
85215
+ force: false,
85216
+ limit: null,
85217
+ timeoutMs: 30 * 6e4,
85218
+ runRef: null,
85219
+ dryRun: false
85220
+ };
85221
+ const errors = [];
85222
+ for (let i = 0; i < argv.length; i++) {
85223
+ const a = argv[i];
85224
+ const next = () => argv[++i] ?? "";
85225
+ if (a === "--test-cmd") options.testCmd = next();
85226
+ else if (a === "--setup-cmd") options.setupCmd = next() || null;
85227
+ else if (a === "--repo-commands") options.repoCommands = true;
85228
+ else if (a === "--runs") options.runs = Number(next());
85229
+ else if (a === "--force") options.force = true;
85230
+ else if (a === "--limit") options.limit = Number(next());
85231
+ else if (a === "--timeout-min") options.timeoutMs = Number(next()) * 6e4;
85232
+ else if (a === "--run-ref") options.runRef = next();
85233
+ else if (a === "--dry-run") options.dryRun = true;
85234
+ else errors.push(`Unknown option: ${a}`);
85235
+ }
85236
+ if (!options.testCmd && !options.repoCommands)
85237
+ errors.push(
85238
+ "--test-cmd is required (or --repo-commands, to use the repository's from Haansi)."
85239
+ );
85240
+ if (!Number.isInteger(options.runs) || options.runs < 1 || options.runs > 5)
85241
+ errors.push("--runs must be 1 to 5.");
85242
+ if (options.limit !== null && (!Number.isInteger(options.limit) || options.limit < 1))
85243
+ errors.push("--limit must be a positive whole number.");
85244
+ if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)
85245
+ errors.push("--timeout-min must be a positive number.");
85246
+ return { options, errors };
85247
+ }
85248
+ function runnerOf(env) {
85249
+ if (env.ImageOS && env.ImageVersion)
85250
+ return `${env.ImageOS}-${env.ImageVersion}`;
85251
+ if (env.RUNNER_OS)
85252
+ return [env.RUNNER_OS, env.RUNNER_ARCH].filter(Boolean).join("/");
85253
+ return null;
85254
+ }
85255
+ async function environmentOf(deps, commands) {
85256
+ const runtimes = {};
85257
+ for (const name of RUNTIMES) {
85258
+ const out = await deps.shell(
85259
+ `command -v ${name} >/dev/null 2>&1 && ${name} --version 2>&1 | head -n 1`,
85260
+ deps.repoDir,
85261
+ {},
85262
+ 3e4
85263
+ );
85264
+ const version3 = out.output.trim().split("\n")[0]?.trim();
85265
+ if (out.code === 0 && version3) runtimes[name] = version3.slice(0, 200);
85266
+ }
85267
+ const described = {
85268
+ setupCommand: commands.setupCmd,
85269
+ testCommand: commands.testCmd,
85270
+ runtimes,
85271
+ runner: runnerOf(deps.env),
85272
+ cliVersion: deps.cliVersion
85273
+ };
85274
+ return {
85275
+ fingerprint: (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(described)).digest("hex"),
85276
+ ...described
85277
+ };
85278
+ }
85279
+ function verdict(codes) {
85280
+ if (codes.every((c) => c === 0)) return "pass";
85281
+ if (codes.every((c) => c !== 0)) return "fail";
85282
+ return "flaky";
85283
+ }
85284
+ async function calibrateTask(deps, options, commands, task) {
85285
+ const label = task.name ?? task.taskKey;
85286
+ const runs = { base: [], target: [] };
85287
+ const done = (reason) => ({
85288
+ taskId: task.taskId,
85289
+ inputsFingerprint: task.inputsFingerprint,
85290
+ state: STATE[reason],
85291
+ reason,
85292
+ runs
85293
+ });
85294
+ for (const side of ["base", "target"]) {
85295
+ const dir = (0, import_node_path9.join)(
85296
+ deps.workDir(),
85297
+ `${task.taskId.replace(/[^a-zA-Z0-9-]/g, "")}-${side}`
85298
+ );
85299
+ try {
85300
+ try {
85301
+ await cleanClone(
85302
+ deps,
85303
+ dir,
85304
+ side === "base" ? task.baseSha : task.targetSha,
85305
+ side === "base" ? task.targetSha : null
85306
+ );
85307
+ } catch (err2) {
85308
+ deps.log(
85309
+ `\u2026 ${label}: ${err2 instanceof Error ? err2.message : String(err2)}`
85310
+ );
85311
+ return done("checkout_failed");
85312
+ }
85313
+ if (commands.setupCmd) {
85314
+ const setup = await deps.shell(
85315
+ commands.setupCmd,
85316
+ dir,
85317
+ {},
85318
+ options.timeoutMs
85319
+ );
85320
+ if (setup.code !== 0) {
85321
+ deps.log(
85322
+ `\u2026 ${label} [${side}]: setup failed: ${setup.output.trim().split("\n").slice(-3).join(" | ")}`
85323
+ );
85324
+ return done("setup_failed");
85325
+ }
85326
+ }
85327
+ if (side === "base") {
85328
+ for (const f of task.testFiles) {
85329
+ const content = await deps.showFile(task.targetSha, f);
85330
+ if (content === null) deps.removeFile((0, import_node_path9.join)(dir, f));
85331
+ else deps.writeFile((0, import_node_path9.join)(dir, f), content);
85332
+ }
85333
+ }
85334
+ for (let i = 0; i < options.runs; i++) {
85335
+ const test = await deps.shell(
85336
+ commands.testCmd,
85337
+ dir,
85338
+ { HAANSI_REPLAY_TEST_FILES: task.testFiles.join(" ") },
85339
+ options.timeoutMs
85340
+ );
85341
+ runs[side].push(test.code);
85342
+ if (side === "target" && test.code !== 0)
85343
+ for (const line of test.output.trim().split("\n").slice(-FAILURE_LINES))
85344
+ deps.log(` ${line}`);
85345
+ }
85346
+ } finally {
85347
+ deps.removeDir(dir);
85348
+ }
85349
+ const seen = verdict(runs[side]);
85350
+ if (side === "base") {
85351
+ if (seen === "flaky") return done("flaky_at_base");
85352
+ if (seen === "pass") return done("passes_at_base");
85353
+ } else {
85354
+ if (seen === "flaky") return done("flaky_at_target");
85355
+ if (seen === "fail") return done("fails_at_target");
85356
+ }
85357
+ }
85358
+ return done("fails_at_base_passes_at_target");
85359
+ }
85360
+ async function runCalibration(options, deps) {
85361
+ const remote = await deps.git(["remote", "get-url", "origin"], deps.repoDir);
85362
+ const repository = remote.code === 0 ? repositoryFromRemote(remote.output) : null;
85363
+ if (!repository)
85364
+ throw new Error("Run from a clone of the repository (no origin remote).");
85365
+ const set2 = await deps.getReplaySet(repository);
85366
+ const repo = options.repoCommands ? set2.repoCommands : null;
85367
+ const setupCmd = options.setupCmd ?? repo?.setupCommand ?? null;
85368
+ const testCmd = options.testCmd || repo?.testCommand || "";
85369
+ if (!testCmd)
85370
+ throw new Error(
85371
+ `No test command: pass --test-cmd, or set one for ${repository} in Haansi.`
85372
+ );
85373
+ const environment = await environmentOf(deps, { setupCmd, testCmd });
85374
+ const calibrations = [];
85375
+ let reused = 0;
85376
+ let skipped = 0;
85377
+ const tasks = options.limit ? set2.tasks.slice(0, options.limit) : set2.tasks;
85378
+ for (const task of tasks) {
85379
+ const label = task.name ?? task.taskKey;
85380
+ const c = task.calibration;
85381
+ if (!options.force && c && c.inputsFingerprint === task.inputsFingerprint && c.envFingerprint === environment.fingerprint) {
85382
+ reused += 1;
85383
+ deps.log(`= ${c.state.padEnd(7)} ${label} (calibrated already)`);
85384
+ continue;
85385
+ }
85386
+ if (!SHA.test(task.baseSha) || !SHA.test(task.targetSha)) {
85387
+ skipped += 1;
85388
+ deps.log(`skip ${label}: its commits are not commit ids`);
85389
+ continue;
85390
+ }
85391
+ if (task.testFiles.length === 0) {
85392
+ skipped += 1;
85393
+ deps.log(
85394
+ `skip ${label}: graded by checks (calibrating by checks comes later)`
85395
+ );
85396
+ continue;
85397
+ }
85398
+ if (!task.testFiles.every(safeTestFile)) {
85399
+ skipped += 1;
85400
+ deps.log(`skip ${label}: a test path is not usable`);
85401
+ continue;
85402
+ }
85403
+ if (!await ensureCommit(deps, task.baseSha) || !await ensureCommit(deps, task.targetSha)) {
85404
+ skipped += 1;
85405
+ deps.log(`skip ${label}: its commits are not reachable from origin`);
85406
+ continue;
85407
+ }
85408
+ const result = await calibrateTask(
85409
+ deps,
85410
+ options,
85411
+ { setupCmd, testCmd },
85412
+ task
85413
+ );
85414
+ calibrations.push(result);
85415
+ deps.log(
85416
+ `${result.state.padEnd(7)} ${label}: ${result.reason.replace(/_/g, " ")} (base ${result.runs.base.join(",") || "-"}; target ${result.runs.target.join(",") || "-"})`
85417
+ );
85418
+ }
85419
+ if (options.dryRun || calibrations.length === 0)
85420
+ return { calibrations, reused, skipped, recorded: null };
85421
+ let recorded = 0;
85422
+ for (let i = 0; i < calibrations.length; i += REPORT_CHUNK) {
85423
+ const out = await deps.report({
85424
+ runRef: options.runRef,
85425
+ environment,
85426
+ results: calibrations.slice(i, i + REPORT_CHUNK)
85427
+ });
85428
+ recorded += out.recorded;
85429
+ if (out.refused.length)
85430
+ deps.log(
85431
+ `Haansi did not record ${out.refused.length} calibration(s): their task changed since it was listed.`
85432
+ );
85433
+ }
85434
+ return { calibrations, reused, skipped, recorded };
85435
+ }
85436
+ async function calibrate(argv) {
85437
+ if (argv.includes("--help")) {
85438
+ console.log(USAGE);
85439
+ return;
85440
+ }
85441
+ const { options, errors } = parseCalibrateArgs(argv);
85442
+ if (errors.length) {
85443
+ for (const e of errors) console.error(e);
85444
+ console.log(USAGE);
85445
+ process.exitCode = 1;
85446
+ return;
85447
+ }
85448
+ options.runRef ??= githubRunRef(process.env);
85449
+ const token = resolveToken5();
85450
+ const apiUrl2 = apiUrl();
85451
+ const work = (0, import_node_fs10.mkdtempSync)((0, import_node_path9.join)((0, import_node_os9.tmpdir)(), "haansi-calibrate-"));
85452
+ try {
85453
+ const { calibrations, reused, skipped, recorded } = await runCalibration(
85454
+ options,
85455
+ {
85456
+ ...localDeps(work),
85457
+ getReplaySet: (repository) => apiGet3(
85458
+ `/memory/replays/tasks?repository=${encodeURIComponent(repository)}`,
85459
+ token,
85460
+ apiUrl2
85461
+ ),
85462
+ report: async (body) => await apiPost2(
85463
+ "/memory/replays/tasks/calibration",
85464
+ body,
85465
+ token,
85466
+ apiUrl2
85467
+ ),
85468
+ env: process.env,
85469
+ cliVersion: cliVersion()
85470
+ }
85471
+ );
85472
+ const count = (state) => calibrations.filter((c) => c.state === state).length;
85473
+ console.log(
85474
+ `
85475
+ Valid: ${count("valid")} \xB7 Invalid: ${count("invalid")} \xB7 Flaky: ${count("flaky")} \xB7 Errors: ${count("error")} \xB7 ${reused} already calibrated \xB7 ${skipped} skipped`
85476
+ );
85477
+ console.log(
85478
+ recorded === null ? options.dryRun ? "Dry run: nothing reported." : "Nothing to report." : `Reported ${recorded} calibration(s) to Haansi.`
85479
+ );
85480
+ } catch (err2) {
85481
+ console.error(err2 instanceof Error ? err2.message : String(err2));
85482
+ process.exitCode = 1;
85483
+ } finally {
85484
+ (0, import_node_fs10.rmSync)(work, { recursive: true, force: true });
85485
+ }
85486
+ }
85487
+ function cliVersion() {
85488
+ try {
85489
+ return String(require_package().version);
85490
+ } catch {
85491
+ return null;
85492
+ }
85493
+ }
85494
+ var import_node_crypto2, import_node_fs10, import_node_os9, import_node_path9, RUNTIMES, REPORT_CHUNK, FAILURE_LINES, STATE, USAGE;
85495
+ var init_replay_calibrate = __esm({
85496
+ "src/commands/replay-calibrate.ts"() {
85497
+ "use strict";
85498
+ import_node_crypto2 = require("node:crypto");
85499
+ import_node_fs10 = require("node:fs");
85500
+ import_node_os9 = require("node:os");
85501
+ import_node_path9 = require("node:path");
85502
+ init_paths();
85503
+ init_api2();
85504
+ init_replay();
85505
+ RUNTIMES = ["node", "python3", "go", "java", "ruby", "rustc"];
85506
+ REPORT_CHUNK = 100;
85507
+ FAILURE_LINES = 5;
85508
+ STATE = {
85509
+ fails_at_base_passes_at_target: "valid",
85510
+ passes_at_base: "invalid",
85511
+ fails_at_target: "invalid",
85512
+ flaky_at_base: "flaky",
85513
+ flaky_at_target: "flaky",
85514
+ setup_failed: "error",
85515
+ checkout_failed: "error"
85516
+ };
85517
+ USAGE = `Usage: haansi replay calibrate --test-cmd "<cmd>" [--setup-cmd "<cmd>"] [--repo-commands]
85518
+ [--runs 2] [--force] [--limit n] [--timeout-min 30]
85519
+ [--run-ref <url>] [--dry-run]
85520
+
85521
+ Checks each task of this repository's replay set with no agent: the tests of
85522
+ its target must fail at its base and pass at its target, --runs times each.
85523
+ Already calibrated tasks are not run again until their inputs or the
85524
+ environment (commands, runtimes, runner, CLI version) change; --force runs
85525
+ them all.`;
85526
+ }
85527
+ });
85528
+
85108
85529
  // src/commands/replay.ts
85109
85530
  var replay_exports = {};
85110
85531
  __export(replay_exports, {
85532
+ SHA: () => SHA,
85533
+ cleanClone: () => cleanClone,
85534
+ ensureCommit: () => ensureCommit,
85111
85535
  githubRunRef: () => githubRunRef,
85536
+ localDeps: () => localDeps,
85112
85537
  parseReplayArgs: () => parseReplayArgs,
85113
85538
  replay: () => replay,
85114
85539
  replayArm: () => replayArm,
@@ -85238,19 +85663,46 @@ async function ensureCommit(deps, sha) {
85238
85663
  return true;
85239
85664
  return (await deps.git(["fetch", "--quiet", "origin", sha], deps.repoDir)).code === 0;
85240
85665
  }
85666
+ async function cleanClone(deps, dir, sha, unreachable) {
85667
+ const steps = [
85668
+ [["init", "--quiet", dir], deps.repoDir],
85669
+ [
85670
+ [
85671
+ "fetch",
85672
+ "--quiet",
85673
+ "--no-tags",
85674
+ "--no-write-fetch-head",
85675
+ deps.repoDir,
85676
+ `${sha}:refs/heads/${REPLAY_BRANCH}`
85677
+ ],
85678
+ dir
85679
+ ],
85680
+ [["checkout", "--quiet", REPLAY_BRANCH], dir]
85681
+ ];
85682
+ for (const [args, cwd] of steps) {
85683
+ if ((await deps.git(args, cwd)).code !== 0)
85684
+ throw new Error(`could not check out ${sha}`);
85685
+ }
85686
+ if (unreachable === null) return;
85687
+ const leak = await deps.git(
85688
+ ["cat-file", "-e", `${unreachable}^{commit}`],
85689
+ dir
85690
+ );
85691
+ if (leak.code === 0)
85692
+ throw new Error("the fix's commit is readable in the clone: run refused");
85693
+ }
85694
+ function cleanCheckout(deps, dir, task) {
85695
+ return cleanClone(deps, dir, task.baseSha, task.targetSha);
85696
+ }
85241
85697
  async function replayArm(deps, options, task, arm, playbook) {
85242
85698
  const started = deps.now();
85243
- const dir = (0, import_node_path9.join)(
85699
+ const dir = (0, import_node_path10.join)(
85244
85700
  deps.workDir(),
85245
85701
  `${task.processId.replace(/[^a-zA-Z0-9_-]/g, "")}-${arm}`
85246
85702
  );
85247
85703
  const files = task.testFiles;
85248
- const add = await deps.git(
85249
- ["worktree", "add", "--detach", dir, task.baseSha],
85250
- deps.repoDir
85251
- );
85252
- if (add.code !== 0) throw new Error(`could not check out ${task.baseSha}`);
85253
85704
  try {
85705
+ await cleanCheckout(deps, dir, task);
85254
85706
  const label = `${task.name ?? task.processId} [${arm}]`;
85255
85707
  const since = (from) => `${Math.round((deps.now() - from) / 1e3)}s`;
85256
85708
  deps.log(`\u2026 ${label}: checked out ${task.baseSha.slice(0, 9)}`);
@@ -85270,10 +85722,10 @@ async function replayArm(deps, options, task, arm, playbook) {
85270
85722
  }
85271
85723
  deps.log(`\u2026 ${label}: set up in ${since(at)}`);
85272
85724
  }
85273
- const promptFile = (0, import_node_path9.join)(dir, ".haansi-replay", "prompt.md");
85725
+ const promptFile = (0, import_node_path10.join)(dir, ".haansi-replay", "prompt.md");
85274
85726
  if (arm === "haansi" && playbook)
85275
85727
  deps.writeFile(
85276
- (0, import_node_path9.join)(dir, ".haansi-replay", "playbook.md"),
85728
+ (0, import_node_path10.join)(dir, ".haansi-replay", "playbook.md"),
85277
85729
  playbook.skillMd
85278
85730
  );
85279
85731
  let feedback = null;
@@ -85290,13 +85742,12 @@ async function replayArm(deps, options, task, arm, playbook) {
85290
85742
  { HAANSI_REPLAY_PROMPT: prompt2, HAANSI_REPLAY_PROMPT_FILE: promptFile },
85291
85743
  options.timeoutMs
85292
85744
  );
85293
- const own = new Map(files.map((f) => [f, deps.readFile((0, import_node_path9.join)(dir, f))]));
85294
- const checkout = await deps.git(
85295
- ["checkout", task.targetSha, "--", ...files],
85296
- dir
85297
- );
85298
- if (checkout.code !== 0)
85299
- throw new Error("could not check out the fix's tests");
85745
+ const own = new Map(files.map((f) => [f, deps.readFile((0, import_node_path10.join)(dir, f))]));
85746
+ for (const f of files) {
85747
+ const content = await deps.showFile(task.targetSha, f);
85748
+ if (content === null) deps.removeFile((0, import_node_path10.join)(dir, f));
85749
+ else deps.writeFile((0, import_node_path10.join)(dir, f), content);
85750
+ }
85300
85751
  const testAt = deps.now();
85301
85752
  deps.log(`\u2026 ${label}: agent done in ${since(agentAt)}, grading`);
85302
85753
  const test = await deps.shell(
@@ -85309,7 +85760,7 @@ async function replayArm(deps, options, task, arm, playbook) {
85309
85760
  `\u2026 ${label}: tests ${test.code === 0 ? "passed" : "failed"} in ${since(testAt)}`
85310
85761
  );
85311
85762
  if (test.code !== 0) {
85312
- for (const line of test.output.trim().split("\n").slice(-FAILURE_LINES))
85763
+ for (const line of test.output.trim().split("\n").slice(-FAILURE_LINES2))
85313
85764
  deps.log(` ${line}`);
85314
85765
  }
85315
85766
  if (test.code === 0) {
@@ -85324,8 +85775,8 @@ async function replayArm(deps, options, task, arm, playbook) {
85324
85775
  };
85325
85776
  }
85326
85777
  for (const [file2, content] of own) {
85327
- if (content === null) deps.removeFile((0, import_node_path9.join)(dir, file2));
85328
- else deps.writeFile((0, import_node_path9.join)(dir, file2), content);
85778
+ if (content === null) deps.removeFile((0, import_node_path10.join)(dir, file2));
85779
+ else deps.writeFile((0, import_node_path10.join)(dir, file2), content);
85329
85780
  }
85330
85781
  feedback = test.output.slice(-FEEDBACK_CHARS);
85331
85782
  }
@@ -85339,7 +85790,6 @@ async function replayArm(deps, options, task, arm, playbook) {
85339
85790
  revisionId: arm === "haansi" ? playbook?.revisionId ?? null : null
85340
85791
  };
85341
85792
  } finally {
85342
- await deps.git(["worktree", "remove", "--force", dir], deps.repoDir);
85343
85793
  deps.removeDir(dir);
85344
85794
  }
85345
85795
  }
@@ -85424,7 +85874,7 @@ async function runReplays(options, deps) {
85424
85874
  );
85425
85875
  return { results, skipped, recorded: reported.recorded };
85426
85876
  }
85427
- function runCommand(file2, args, cwd, env, timeoutMs) {
85877
+ function runCommand(file2, args, cwd, env, timeoutMs, stdoutOnly = false) {
85428
85878
  return new Promise((resolvePromise) => {
85429
85879
  const child = (0, import_node_child_process4.spawn)(file2, args, {
85430
85880
  cwd,
@@ -85435,8 +85885,16 @@ function runCommand(file2, args, cwd, env, timeoutMs) {
85435
85885
  const keep = (chunk) => {
85436
85886
  output = (output + chunk.toString()).slice(-2e5);
85437
85887
  };
85438
- child.stdout.on("data", keep);
85439
- child.stderr.on("data", keep);
85888
+ if (stdoutOnly) {
85889
+ const chunks = [];
85890
+ child.stdout.on("data", (chunk) => chunks.push(chunk));
85891
+ child.stdout.on("end", () => {
85892
+ output = Buffer.concat(chunks).toString();
85893
+ });
85894
+ } else {
85895
+ child.stdout.on("data", keep);
85896
+ child.stderr.on("data", keep);
85897
+ }
85440
85898
  const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
85441
85899
  child.on("close", (code) => {
85442
85900
  clearTimeout(timer);
@@ -85448,17 +85906,50 @@ function runCommand(file2, args, cwd, env, timeoutMs) {
85448
85906
  });
85449
85907
  });
85450
85908
  }
85909
+ function localDeps(work) {
85910
+ return {
85911
+ git: (args, cwd) => runCommand("git", args, cwd, {}, 10 * 6e4),
85912
+ showFile: async (sha, path2) => {
85913
+ const shown = await runCommand(
85914
+ "git",
85915
+ ["show", `${sha}:${path2}`],
85916
+ process.cwd(),
85917
+ {},
85918
+ 6e4,
85919
+ true
85920
+ );
85921
+ return shown.code === 0 ? shown.output : null;
85922
+ },
85923
+ shell: (command2, cwd, env, timeoutMs) => runCommand("sh", ["-c", command2], cwd, env, timeoutMs),
85924
+ repoDir: process.cwd(),
85925
+ workDir: () => work,
85926
+ writeFile: (path2, content) => {
85927
+ (0, import_node_fs11.mkdirSync)((0, import_node_path10.dirname)(path2), { recursive: true });
85928
+ (0, import_node_fs11.writeFileSync)(path2, content);
85929
+ },
85930
+ readFile: (path2) => (0, import_node_fs11.existsSync)(path2) ? (0, import_node_fs11.readFileSync)(path2, "utf-8") : null,
85931
+ removeFile: (path2) => (0, import_node_fs11.rmSync)(path2, { force: true }),
85932
+ removeDir: (path2) => (0, import_node_fs11.rmSync)(path2, { recursive: true, force: true }),
85933
+ now: () => Date.now(),
85934
+ log: (line) => console.log(line)
85935
+ };
85936
+ }
85451
85937
  async function replay(argv) {
85938
+ if (argv[0] === "calibrate") {
85939
+ const { calibrate: calibrate2 } = await Promise.resolve().then(() => (init_replay_calibrate(), replay_calibrate_exports));
85940
+ await calibrate2(argv.slice(1));
85941
+ return;
85942
+ }
85452
85943
  const { sub, options, errors } = parseReplayArgs(argv);
85453
85944
  if (sub !== "run" || errors.length) {
85454
85945
  for (const e of sub === "run" ? errors : []) console.error(e);
85455
- console.log(USAGE);
85946
+ console.log(USAGE2);
85456
85947
  if (sub === "run") process.exitCode = 1;
85457
85948
  return;
85458
85949
  }
85459
85950
  const token = resolveToken5();
85460
85951
  const apiUrl2 = apiUrl();
85461
- const work = (0, import_node_fs10.mkdtempSync)((0, import_node_path9.join)((0, import_node_os9.tmpdir)(), "haansi-replay-"));
85952
+ const work = (0, import_node_fs11.mkdtempSync)((0, import_node_path10.join)((0, import_node_os10.tmpdir)(), "haansi-replay-"));
85462
85953
  try {
85463
85954
  const { results, skipped, recorded } = await runReplays(options, {
85464
85955
  getTasks: async (familyId, cutoff) => (await apiGet3(
@@ -85477,19 +85968,7 @@ async function replay(argv) {
85477
85968
  token,
85478
85969
  apiUrl2
85479
85970
  ),
85480
- git: (args, cwd) => runCommand("git", args, cwd, {}, 10 * 6e4),
85481
- shell: (command2, cwd, env, timeoutMs) => runCommand("sh", ["-c", command2], cwd, env, timeoutMs),
85482
- repoDir: process.cwd(),
85483
- workDir: () => work,
85484
- writeFile: (path2, content) => {
85485
- (0, import_node_fs10.mkdirSync)((0, import_node_path9.dirname)(path2), { recursive: true });
85486
- (0, import_node_fs10.writeFileSync)(path2, content);
85487
- },
85488
- readFile: (path2) => (0, import_node_fs10.existsSync)(path2) ? (0, import_node_fs10.readFileSync)(path2, "utf-8") : null,
85489
- removeFile: (path2) => (0, import_node_fs10.rmSync)(path2, { force: true }),
85490
- removeDir: (path2) => (0, import_node_fs10.rmSync)(path2, { recursive: true, force: true }),
85491
- now: () => Date.now(),
85492
- log: (line) => console.log(line)
85971
+ ...localDeps(work)
85493
85972
  });
85494
85973
  const passed = (arm) => results.filter((r) => r.arm === arm && r.passed).length;
85495
85974
  const ran = (arm) => results.filter((r) => r.arm === arm).length;
@@ -85501,29 +85980,32 @@ Baseline: ${passed("baseline")}/${ran("baseline")} passed \xB7 With Haansi: ${pa
85501
85980
  recorded === null ? options.dryRun ? "Dry run: nothing reported." : "Nothing to report." : `Reported ${recorded} result(s) to Haansi.`
85502
85981
  );
85503
85982
  } finally {
85504
- (0, import_node_fs10.rmSync)(work, { recursive: true, force: true });
85983
+ (0, import_node_fs11.rmSync)(work, { recursive: true, force: true });
85505
85984
  }
85506
85985
  }
85507
- var import_node_child_process4, import_node_fs10, import_node_os9, import_node_path9, SHA, ARMS, FEEDBACK_CHARS, FAILURE_LINES, USAGE;
85986
+ var import_node_child_process4, import_node_fs11, import_node_os10, import_node_path10, SHA, ARMS, FEEDBACK_CHARS, FAILURE_LINES2, REPLAY_BRANCH, USAGE2;
85508
85987
  var init_replay = __esm({
85509
85988
  "src/commands/replay.ts"() {
85510
85989
  "use strict";
85511
85990
  import_node_child_process4 = require("node:child_process");
85512
- import_node_fs10 = require("node:fs");
85513
- import_node_os9 = require("node:os");
85514
- import_node_path9 = require("node:path");
85991
+ import_node_fs11 = require("node:fs");
85992
+ import_node_os10 = require("node:os");
85993
+ import_node_path10 = require("node:path");
85515
85994
  init_paths();
85516
85995
  init_api2();
85517
85996
  SHA = /^[0-9a-f]{7,40}$/i;
85518
85997
  ARMS = ["baseline", "haansi"];
85519
85998
  FEEDBACK_CHARS = 6e3;
85520
- FAILURE_LINES = 8;
85521
- USAGE = `Usage: haansi replay run --family <id> --agent-cmd "<cmd>" --test-cmd "<cmd>"
85999
+ FAILURE_LINES2 = 8;
86000
+ REPLAY_BRANCH = "replay";
86001
+ USAGE2 = `Usage: haansi replay run --family <id> --agent-cmd "<cmd>" --test-cmd "<cmd>"
85522
86002
  [--setup-cmd "<cmd>"] [--attempts 3] [--arms baseline,haansi] [--limit n]
85523
86003
  [--cutoff <date>] [--timeout-min 30]
85524
86004
  [--agent-name <name>] [--model <name>] [--run-ref <url>] [--dry-run]
85525
86005
  [--repo-commands]
85526
86006
 
86007
+ haansi replay calibrate --test-cmd "<cmd>" [--setup-cmd "<cmd>"] (see --help)
86008
+
85527
86009
  --repo-commands uses the setup and test commands an org admin set for the
85528
86010
  repository in Haansi (Settings \u2192 Data sources) for any not given here.
85529
86011
 
@@ -85752,6 +86234,9 @@ Commands:
85752
86234
  replay run --family <id> --agent-cmd "<cmd>" --test-cmd "<cmd>"
85753
86235
  Replay a problem family's tasks in your CI, with and
85754
86236
  without its playbook, and report the results
86237
+ replay calibrate --test-cmd "<cmd>"
86238
+ Check this repository's replay tasks are valid: their
86239
+ tests fail at the start and pass at the fix
85755
86240
  skills sync [--agent <sel>] [--project] [--dir <path>]
85756
86241
  Copy your team's published skills into Claude Code / Codex
85757
86242
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haansi",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
4
4
  "description": "Haansi CLI - Session collector and MCP server for Claude Code & OpenAI Codex",
5
5
  "bin": {
6
6
  "haansi": "./dist/haansi.js"