haansi 0.1.54 → 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 +497 -53
  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.54",
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,7 +85663,7 @@ 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
  }
85241
- async function cleanCheckout(deps, dir, task) {
85666
+ async function cleanClone(deps, dir, sha, unreachable) {
85242
85667
  const steps = [
85243
85668
  [["init", "--quiet", dir], deps.repoDir],
85244
85669
  [
@@ -85248,7 +85673,7 @@ async function cleanCheckout(deps, dir, task) {
85248
85673
  "--no-tags",
85249
85674
  "--no-write-fetch-head",
85250
85675
  deps.repoDir,
85251
- `${task.baseSha}:refs/heads/${REPLAY_BRANCH}`
85676
+ `${sha}:refs/heads/${REPLAY_BRANCH}`
85252
85677
  ],
85253
85678
  dir
85254
85679
  ],
@@ -85256,18 +85681,22 @@ async function cleanCheckout(deps, dir, task) {
85256
85681
  ];
85257
85682
  for (const [args, cwd] of steps) {
85258
85683
  if ((await deps.git(args, cwd)).code !== 0)
85259
- throw new Error(`could not check out ${task.baseSha}`);
85684
+ throw new Error(`could not check out ${sha}`);
85260
85685
  }
85686
+ if (unreachable === null) return;
85261
85687
  const leak = await deps.git(
85262
- ["cat-file", "-e", `${task.targetSha}^{commit}`],
85688
+ ["cat-file", "-e", `${unreachable}^{commit}`],
85263
85689
  dir
85264
85690
  );
85265
85691
  if (leak.code === 0)
85266
85692
  throw new Error("the fix's commit is readable in the clone: run refused");
85267
85693
  }
85694
+ function cleanCheckout(deps, dir, task) {
85695
+ return cleanClone(deps, dir, task.baseSha, task.targetSha);
85696
+ }
85268
85697
  async function replayArm(deps, options, task, arm, playbook) {
85269
85698
  const started = deps.now();
85270
- const dir = (0, import_node_path9.join)(
85699
+ const dir = (0, import_node_path10.join)(
85271
85700
  deps.workDir(),
85272
85701
  `${task.processId.replace(/[^a-zA-Z0-9_-]/g, "")}-${arm}`
85273
85702
  );
@@ -85293,10 +85722,10 @@ async function replayArm(deps, options, task, arm, playbook) {
85293
85722
  }
85294
85723
  deps.log(`\u2026 ${label}: set up in ${since(at)}`);
85295
85724
  }
85296
- 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");
85297
85726
  if (arm === "haansi" && playbook)
85298
85727
  deps.writeFile(
85299
- (0, import_node_path9.join)(dir, ".haansi-replay", "playbook.md"),
85728
+ (0, import_node_path10.join)(dir, ".haansi-replay", "playbook.md"),
85300
85729
  playbook.skillMd
85301
85730
  );
85302
85731
  let feedback = null;
@@ -85313,11 +85742,11 @@ async function replayArm(deps, options, task, arm, playbook) {
85313
85742
  { HAANSI_REPLAY_PROMPT: prompt2, HAANSI_REPLAY_PROMPT_FILE: promptFile },
85314
85743
  options.timeoutMs
85315
85744
  );
85316
- const own = new Map(files.map((f) => [f, deps.readFile((0, import_node_path9.join)(dir, f))]));
85745
+ const own = new Map(files.map((f) => [f, deps.readFile((0, import_node_path10.join)(dir, f))]));
85317
85746
  for (const f of files) {
85318
85747
  const content = await deps.showFile(task.targetSha, f);
85319
- if (content === null) deps.removeFile((0, import_node_path9.join)(dir, f));
85320
- else deps.writeFile((0, import_node_path9.join)(dir, f), content);
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);
85321
85750
  }
85322
85751
  const testAt = deps.now();
85323
85752
  deps.log(`\u2026 ${label}: agent done in ${since(agentAt)}, grading`);
@@ -85331,7 +85760,7 @@ async function replayArm(deps, options, task, arm, playbook) {
85331
85760
  `\u2026 ${label}: tests ${test.code === 0 ? "passed" : "failed"} in ${since(testAt)}`
85332
85761
  );
85333
85762
  if (test.code !== 0) {
85334
- 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))
85335
85764
  deps.log(` ${line}`);
85336
85765
  }
85337
85766
  if (test.code === 0) {
@@ -85346,8 +85775,8 @@ async function replayArm(deps, options, task, arm, playbook) {
85346
85775
  };
85347
85776
  }
85348
85777
  for (const [file2, content] of own) {
85349
- if (content === null) deps.removeFile((0, import_node_path9.join)(dir, file2));
85350
- 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);
85351
85780
  }
85352
85781
  feedback = test.output.slice(-FEEDBACK_CHARS);
85353
85782
  }
@@ -85477,17 +85906,50 @@ function runCommand(file2, args, cwd, env, timeoutMs, stdoutOnly = false) {
85477
85906
  });
85478
85907
  });
85479
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
+ }
85480
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
+ }
85481
85943
  const { sub, options, errors } = parseReplayArgs(argv);
85482
85944
  if (sub !== "run" || errors.length) {
85483
85945
  for (const e of sub === "run" ? errors : []) console.error(e);
85484
- console.log(USAGE);
85946
+ console.log(USAGE2);
85485
85947
  if (sub === "run") process.exitCode = 1;
85486
85948
  return;
85487
85949
  }
85488
85950
  const token = resolveToken5();
85489
85951
  const apiUrl2 = apiUrl();
85490
- 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-"));
85491
85953
  try {
85492
85954
  const { results, skipped, recorded } = await runReplays(options, {
85493
85955
  getTasks: async (familyId, cutoff) => (await apiGet3(
@@ -85506,30 +85968,7 @@ async function replay(argv) {
85506
85968
  token,
85507
85969
  apiUrl2
85508
85970
  ),
85509
- git: (args, cwd) => runCommand("git", args, cwd, {}, 10 * 6e4),
85510
- showFile: async (sha, path2) => {
85511
- const shown = await runCommand(
85512
- "git",
85513
- ["show", `${sha}:${path2}`],
85514
- process.cwd(),
85515
- {},
85516
- 6e4,
85517
- true
85518
- );
85519
- return shown.code === 0 ? shown.output : null;
85520
- },
85521
- shell: (command2, cwd, env, timeoutMs) => runCommand("sh", ["-c", command2], cwd, env, timeoutMs),
85522
- repoDir: process.cwd(),
85523
- workDir: () => work,
85524
- writeFile: (path2, content) => {
85525
- (0, import_node_fs10.mkdirSync)((0, import_node_path9.dirname)(path2), { recursive: true });
85526
- (0, import_node_fs10.writeFileSync)(path2, content);
85527
- },
85528
- readFile: (path2) => (0, import_node_fs10.existsSync)(path2) ? (0, import_node_fs10.readFileSync)(path2, "utf-8") : null,
85529
- removeFile: (path2) => (0, import_node_fs10.rmSync)(path2, { force: true }),
85530
- removeDir: (path2) => (0, import_node_fs10.rmSync)(path2, { recursive: true, force: true }),
85531
- now: () => Date.now(),
85532
- log: (line) => console.log(line)
85971
+ ...localDeps(work)
85533
85972
  });
85534
85973
  const passed = (arm) => results.filter((r) => r.arm === arm && r.passed).length;
85535
85974
  const ran = (arm) => results.filter((r) => r.arm === arm).length;
@@ -85541,30 +85980,32 @@ Baseline: ${passed("baseline")}/${ran("baseline")} passed \xB7 With Haansi: ${pa
85541
85980
  recorded === null ? options.dryRun ? "Dry run: nothing reported." : "Nothing to report." : `Reported ${recorded} result(s) to Haansi.`
85542
85981
  );
85543
85982
  } finally {
85544
- (0, import_node_fs10.rmSync)(work, { recursive: true, force: true });
85983
+ (0, import_node_fs11.rmSync)(work, { recursive: true, force: true });
85545
85984
  }
85546
85985
  }
85547
- var import_node_child_process4, import_node_fs10, import_node_os9, import_node_path9, SHA, ARMS, FEEDBACK_CHARS, FAILURE_LINES, REPLAY_BRANCH, 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;
85548
85987
  var init_replay = __esm({
85549
85988
  "src/commands/replay.ts"() {
85550
85989
  "use strict";
85551
85990
  import_node_child_process4 = require("node:child_process");
85552
- import_node_fs10 = require("node:fs");
85553
- import_node_os9 = require("node:os");
85554
- 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");
85555
85994
  init_paths();
85556
85995
  init_api2();
85557
85996
  SHA = /^[0-9a-f]{7,40}$/i;
85558
85997
  ARMS = ["baseline", "haansi"];
85559
85998
  FEEDBACK_CHARS = 6e3;
85560
- FAILURE_LINES = 8;
85999
+ FAILURE_LINES2 = 8;
85561
86000
  REPLAY_BRANCH = "replay";
85562
- USAGE = `Usage: haansi replay run --family <id> --agent-cmd "<cmd>" --test-cmd "<cmd>"
86001
+ USAGE2 = `Usage: haansi replay run --family <id> --agent-cmd "<cmd>" --test-cmd "<cmd>"
85563
86002
  [--setup-cmd "<cmd>"] [--attempts 3] [--arms baseline,haansi] [--limit n]
85564
86003
  [--cutoff <date>] [--timeout-min 30]
85565
86004
  [--agent-name <name>] [--model <name>] [--run-ref <url>] [--dry-run]
85566
86005
  [--repo-commands]
85567
86006
 
86007
+ haansi replay calibrate --test-cmd "<cmd>" [--setup-cmd "<cmd>"] (see --help)
86008
+
85568
86009
  --repo-commands uses the setup and test commands an org admin set for the
85569
86010
  repository in Haansi (Settings \u2192 Data sources) for any not given here.
85570
86011
 
@@ -85793,6 +86234,9 @@ Commands:
85793
86234
  replay run --family <id> --agent-cmd "<cmd>" --test-cmd "<cmd>"
85794
86235
  Replay a problem family's tasks in your CI, with and
85795
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
85796
86240
  skills sync [--agent <sel>] [--project] [--dir <path>]
85797
86241
  Copy your team's published skills into Claude Code / Codex
85798
86242
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haansi",
3
- "version": "0.1.54",
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"