haansi 0.1.54 → 0.1.56

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 +766 -60
  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.56",
99
99
  description: "Haansi CLI - Session collector and MCP server for Claude Code & OpenAI Codex",
100
100
  bin: {
101
101
  haansi: "./dist/haansi.js"
@@ -19220,6 +19220,16 @@ var init_exclusions = __esm({
19220
19220
  model: ["operational", "The model name the replay ran with."],
19221
19221
  run_ref: ["not-personal", "A link to the customer's CI run."]
19222
19222
  }),
19223
+ ...exchangeExclusions(INTEL, "replay_task_results", {
19224
+ inputs_fingerprint: [
19225
+ "derived",
19226
+ "A SHA-256 of the task's inputs the replay ran on; not reversible."
19227
+ ],
19228
+ arm: ["operational", "Closed CHECK vocabulary (baseline, haansi)."],
19229
+ agent: ["operational", "The agent CLI's name, e.g. claude-code."],
19230
+ model: ["operational", "The model name the replay ran with."],
19231
+ run_ref: ["not-personal", "A link to the customer's CI run."]
19232
+ }),
19223
19233
  ...exchangeExclusions(INTEL, "replay_tasks", {
19224
19234
  process_id: [
19225
19235
  "join-key",
@@ -19236,6 +19246,77 @@ var init_exclusions = __esm({
19236
19246
  "Actions workflow names from CI config that passed on the fix."
19237
19247
  ]
19238
19248
  }),
19249
+ ...exchangeExclusions(INTEL, "stored_replay_tasks", {
19250
+ task_key: [
19251
+ "join-key",
19252
+ "The task's kind with the workstream id or owner/repo#PR numbers it was cut from; upserted by equality."
19253
+ ],
19254
+ kind: [
19255
+ "operational",
19256
+ "Closed CHECK vocabulary (full, milestone, recovery)."
19257
+ ],
19258
+ process_id: [
19259
+ "join-key",
19260
+ "The workstream's process_registry.process_id, joined by equality."
19261
+ ],
19262
+ repository: [
19263
+ "join-key",
19264
+ "owner/repo of the task's PRs, as in workstream_attempts."
19265
+ ],
19266
+ base_sha: ["not-personal", "A commit SHA."],
19267
+ target_sha: ["not-personal", "A commit SHA."],
19268
+ checks: [
19269
+ "not-personal",
19270
+ "Actions workflow names from CI config that passed on the target."
19271
+ ],
19272
+ score_parts: [
19273
+ "not-personal",
19274
+ "Counts and flags the replayability score rests on (files, areas, context records, gradable by tests or checks); no text."
19275
+ ],
19276
+ inputs_fingerprint: [
19277
+ "derived",
19278
+ "A SHA-256 of the task's inputs; not reversible."
19279
+ ],
19280
+ card_state: [
19281
+ "operational",
19282
+ "Closed CHECK vocabulary (ready, dropped, invalid)."
19283
+ ],
19284
+ card_dropped_reason: [
19285
+ "operational",
19286
+ "Closed CHECK vocabulary (leak, insufficient)."
19287
+ ],
19288
+ card_fingerprint: [
19289
+ "derived",
19290
+ "A SHA-256 of what the card was written from; not reversible."
19291
+ ],
19292
+ card_model: ["not-personal", "The model id that wrote the card."],
19293
+ card_prompt_tag: [
19294
+ "not-personal",
19295
+ "The prompt version that wrote the card (replay-task-card-vN)."
19296
+ ],
19297
+ calibration_state: [
19298
+ "operational",
19299
+ "Closed CHECK vocabulary (valid, invalid, flaky, error)."
19300
+ ],
19301
+ calibration_reason: [
19302
+ "operational",
19303
+ "Closed CHECK vocabulary (passes_at_base, fails_at_target, \u2026)."
19304
+ ],
19305
+ calibration_inputs_fingerprint: [
19306
+ "derived",
19307
+ "A SHA-256 of the task's inputs; not reversible."
19308
+ ],
19309
+ calibration_env_fingerprint: [
19310
+ "derived",
19311
+ "A SHA-256 of the CI environment; not reversible."
19312
+ ],
19313
+ calibration_environment: [
19314
+ "not-personal",
19315
+ "The CI's setup and test commands an org admin or CI owner typed, runtime versions, the runner image and the CLI version."
19316
+ ],
19317
+ calibration_runs: ["operational", "Exit codes of the test runs."],
19318
+ calibration_run_ref: ["not-personal", "A link to the customer's CI run."]
19319
+ }),
19239
19320
  ...exchangeExclusions(INTEL, "problem_family_members", {
19240
19321
  process_id: [
19241
19322
  "join-key",
@@ -26495,6 +26576,28 @@ var init_table_registry = __esm({
26495
26576
  entry("intelligence", INTELLIGENCE_DB, "replay_tasks", "direct", {
26496
26577
  scopingColumn: "organization_id"
26497
26578
  }),
26579
+ // Stored replay tasks proposed by the ledger (replay tasks v2 plan §4).
26580
+ entry("intelligence", INTELLIGENCE_DB, "stored_replay_tasks", "direct", {
26581
+ scopingColumn: "organization_id"
26582
+ }),
26583
+ entry("intelligence", INTELLIGENCE_DB, "replay_task_results", "direct", {
26584
+ scopingColumn: "organization_id"
26585
+ }),
26586
+ // A person's revert verdicts, the checks that validate a change and the
26587
+ // replay commands, per repository (outcome evidence v2 plan).
26588
+ entry("intelligence", INTELLIGENCE_DB, "revert_labels", "direct", {
26589
+ scopingColumn: "organization_id"
26590
+ }),
26591
+ entry("intelligence", INTELLIGENCE_DB, "repository_checks", "direct", {
26592
+ scopingColumn: "organization_id"
26593
+ }),
26594
+ entry(
26595
+ "intelligence",
26596
+ INTELLIGENCE_DB,
26597
+ "repository_replay_settings",
26598
+ "direct",
26599
+ { scopingColumn: "organization_id" }
26600
+ ),
26498
26601
  entry("intelligence", INTELLIGENCE_DB, "problem_family_feedback", "direct", {
26499
26602
  scopingColumn: "organization_id"
26500
26603
  }),
@@ -77377,7 +77480,7 @@ var require_view = __commonJS({
77377
77480
  var dirname5 = path2.dirname;
77378
77481
  var basename4 = path2.basename;
77379
77482
  var extname = path2.extname;
77380
- var join16 = path2.join;
77483
+ var join17 = path2.join;
77381
77484
  var resolve5 = path2.resolve;
77382
77485
  module2.exports = View;
77383
77486
  function View(name, options) {
@@ -77439,12 +77542,12 @@ var require_view = __commonJS({
77439
77542
  };
77440
77543
  View.prototype.resolve = function resolve6(dir, file2) {
77441
77544
  var ext = this.ext;
77442
- var path3 = join16(dir, file2);
77545
+ var path3 = join17(dir, file2);
77443
77546
  var stat = tryStat(path3);
77444
77547
  if (stat && stat.isFile()) {
77445
77548
  return path3;
77446
77549
  }
77447
- path3 = join16(dir, basename4(file2, ext), "index" + ext);
77550
+ path3 = join17(dir, basename4(file2, ext), "index" + ext);
77448
77551
  stat = tryStat(path3);
77449
77552
  if (stat && stat.isFile()) {
77450
77553
  return path3;
@@ -81089,7 +81192,7 @@ var require_send = __commonJS({
81089
81192
  var Stream = require("stream");
81090
81193
  var util2 = require("util");
81091
81194
  var extname = path2.extname;
81092
- var join16 = path2.join;
81195
+ var join17 = path2.join;
81093
81196
  var normalize = path2.normalize;
81094
81197
  var resolve5 = path2.resolve;
81095
81198
  var sep2 = path2.sep;
@@ -81261,7 +81364,7 @@ var require_send = __commonJS({
81261
81364
  return res;
81262
81365
  }
81263
81366
  parts = path3.split(sep2);
81264
- path3 = normalize(join16(root, path3));
81367
+ path3 = normalize(join17(root, path3));
81265
81368
  } else {
81266
81369
  if (UP_PATH_REGEXP.test(path3)) {
81267
81370
  debug('malicious path "%s"', path3);
@@ -81394,7 +81497,7 @@ var require_send = __commonJS({
81394
81497
  if (err2) return self.onStatError(err2);
81395
81498
  return self.error(404);
81396
81499
  }
81397
- var p = join16(path3, self._index[i]);
81500
+ var p = join17(path3, self._index[i]);
81398
81501
  debug('stat "%s"', p);
81399
81502
  fs.stat(p, function(err3, stat) {
81400
81503
  if (err3) return next(err3);
@@ -85105,10 +85208,540 @@ var init_skills = __esm({
85105
85208
  }
85106
85209
  });
85107
85210
 
85211
+ // src/commands/replay-calibrate.ts
85212
+ var replay_calibrate_exports = {};
85213
+ __export(replay_calibrate_exports, {
85214
+ RUNTIMES: () => RUNTIMES,
85215
+ calibrate: () => calibrate,
85216
+ calibrateTask: () => calibrateTask,
85217
+ calibrateTaskOptions: () => calibrateTaskOptions,
85218
+ cliVersion: () => cliVersion,
85219
+ environmentOf: () => environmentOf,
85220
+ parseCalibrateArgs: () => parseCalibrateArgs,
85221
+ repositoryOf: () => repositoryOf,
85222
+ runCalibration: () => runCalibration,
85223
+ runnerOf: () => runnerOf
85224
+ });
85225
+ function parseCalibrateArgs(argv) {
85226
+ const options = {
85227
+ setupCmd: null,
85228
+ testCmd: "",
85229
+ repoCommands: false,
85230
+ runs: 2,
85231
+ force: false,
85232
+ limit: null,
85233
+ timeoutMs: 30 * 6e4,
85234
+ runRef: null,
85235
+ dryRun: false
85236
+ };
85237
+ const errors = [];
85238
+ for (let i = 0; i < argv.length; i++) {
85239
+ const a = argv[i];
85240
+ const next = () => argv[++i] ?? "";
85241
+ if (a === "--test-cmd") options.testCmd = next();
85242
+ else if (a === "--setup-cmd") options.setupCmd = next() || null;
85243
+ else if (a === "--repo-commands") options.repoCommands = true;
85244
+ else if (a === "--runs") options.runs = Number(next());
85245
+ else if (a === "--force") options.force = true;
85246
+ else if (a === "--limit") options.limit = Number(next());
85247
+ else if (a === "--timeout-min") options.timeoutMs = Number(next()) * 6e4;
85248
+ else if (a === "--run-ref") options.runRef = next();
85249
+ else if (a === "--dry-run") options.dryRun = true;
85250
+ else errors.push(`Unknown option: ${a}`);
85251
+ }
85252
+ if (!options.testCmd && !options.repoCommands)
85253
+ errors.push(
85254
+ "--test-cmd is required (or --repo-commands, to use the repository's from Haansi)."
85255
+ );
85256
+ if (!Number.isInteger(options.runs) || options.runs < 1 || options.runs > 5)
85257
+ errors.push("--runs must be 1 to 5.");
85258
+ if (options.limit !== null && (!Number.isInteger(options.limit) || options.limit < 1))
85259
+ errors.push("--limit must be a positive whole number.");
85260
+ if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)
85261
+ errors.push("--timeout-min must be a positive number.");
85262
+ return { options, errors };
85263
+ }
85264
+ function calibrateTaskOptions(run3, commands) {
85265
+ return {
85266
+ setupCmd: commands.setupCmd,
85267
+ testCmd: commands.testCmd,
85268
+ repoCommands: false,
85269
+ runs: 2,
85270
+ force: false,
85271
+ limit: run3.limit,
85272
+ timeoutMs: run3.timeoutMs,
85273
+ runRef: run3.runRef,
85274
+ dryRun: run3.dryRun
85275
+ };
85276
+ }
85277
+ function runnerOf(env) {
85278
+ if (env.ImageOS && env.ImageVersion)
85279
+ return `${env.ImageOS}-${env.ImageVersion}`;
85280
+ if (env.RUNNER_OS)
85281
+ return [env.RUNNER_OS, env.RUNNER_ARCH].filter(Boolean).join("/");
85282
+ return null;
85283
+ }
85284
+ async function environmentOf(deps, commands) {
85285
+ const runtimes = {};
85286
+ for (const name of RUNTIMES) {
85287
+ const out = await deps.shell(
85288
+ `command -v ${name} >/dev/null 2>&1 && ${name} --version 2>&1 | head -n 1`,
85289
+ deps.repoDir,
85290
+ {},
85291
+ 3e4
85292
+ );
85293
+ const version3 = out.output.trim().split("\n")[0]?.trim();
85294
+ if (out.code === 0 && version3) runtimes[name] = version3.slice(0, 200);
85295
+ }
85296
+ const described = {
85297
+ setupCommand: commands.setupCmd,
85298
+ testCommand: commands.testCmd,
85299
+ runtimes,
85300
+ runner: runnerOf(deps.env),
85301
+ cliVersion: deps.cliVersion
85302
+ };
85303
+ return {
85304
+ fingerprint: (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(described)).digest("hex"),
85305
+ ...described
85306
+ };
85307
+ }
85308
+ function verdict(codes) {
85309
+ if (codes.every((c) => c === 0)) return "pass";
85310
+ if (codes.every((c) => c !== 0)) return "fail";
85311
+ return "flaky";
85312
+ }
85313
+ async function calibrateTask(deps, options, commands, task) {
85314
+ const label = task.name ?? task.taskKey;
85315
+ const runs = { base: [], target: [] };
85316
+ const done = (reason) => ({
85317
+ taskId: task.taskId,
85318
+ inputsFingerprint: task.inputsFingerprint,
85319
+ state: STATE[reason],
85320
+ reason,
85321
+ runs
85322
+ });
85323
+ for (const side of ["base", "target"]) {
85324
+ const dir = (0, import_node_path9.join)(
85325
+ deps.workDir(),
85326
+ `${task.taskId.replace(/[^a-zA-Z0-9-]/g, "")}-${side}`
85327
+ );
85328
+ try {
85329
+ try {
85330
+ await cleanClone(
85331
+ deps,
85332
+ dir,
85333
+ side === "base" ? task.baseSha : task.targetSha,
85334
+ side === "base" ? task.targetSha : null
85335
+ );
85336
+ } catch (err2) {
85337
+ deps.log(
85338
+ `\u2026 ${label}: ${err2 instanceof Error ? err2.message : String(err2)}`
85339
+ );
85340
+ return done("checkout_failed");
85341
+ }
85342
+ if (commands.setupCmd) {
85343
+ const setup = await deps.shell(
85344
+ commands.setupCmd,
85345
+ dir,
85346
+ {},
85347
+ options.timeoutMs
85348
+ );
85349
+ if (setup.code !== 0) {
85350
+ deps.log(
85351
+ `\u2026 ${label} [${side}]: setup failed: ${setup.output.trim().split("\n").slice(-3).join(" | ")}`
85352
+ );
85353
+ return done("setup_failed");
85354
+ }
85355
+ }
85356
+ if (side === "base") {
85357
+ for (const f of task.testFiles) {
85358
+ const content = await deps.showFile(task.targetSha, f);
85359
+ if (content === null) deps.removeFile((0, import_node_path9.join)(dir, f));
85360
+ else deps.writeFile((0, import_node_path9.join)(dir, f), content);
85361
+ }
85362
+ }
85363
+ for (let i = 0; i < options.runs; i++) {
85364
+ const test = await deps.shell(
85365
+ commands.testCmd,
85366
+ dir,
85367
+ { HAANSI_REPLAY_TEST_FILES: task.testFiles.join(" ") },
85368
+ options.timeoutMs
85369
+ );
85370
+ runs[side].push(test.code);
85371
+ if (side === "target" && test.code !== 0)
85372
+ for (const line of test.output.trim().split("\n").slice(-FAILURE_LINES))
85373
+ deps.log(` ${line}`);
85374
+ }
85375
+ } finally {
85376
+ deps.removeDir(dir);
85377
+ }
85378
+ const seen = verdict(runs[side]);
85379
+ if (side === "base") {
85380
+ if (seen === "flaky") return done("flaky_at_base");
85381
+ if (seen === "pass") return done("passes_at_base");
85382
+ } else {
85383
+ if (seen === "flaky") return done("flaky_at_target");
85384
+ if (seen === "fail") return done("fails_at_target");
85385
+ }
85386
+ }
85387
+ return done("fails_at_base_passes_at_target");
85388
+ }
85389
+ async function repositoryOf(deps) {
85390
+ const remote = await deps.git(["remote", "get-url", "origin"], deps.repoDir);
85391
+ const repository = remote.code === 0 ? repositoryFromRemote(remote.output) : null;
85392
+ if (!repository)
85393
+ throw new Error("Run from a clone of the repository (no origin remote).");
85394
+ return repository;
85395
+ }
85396
+ async function runCalibration(options, deps, preloaded) {
85397
+ const repository = await repositoryOf(deps);
85398
+ const set2 = preloaded ?? await deps.getReplaySet(repository);
85399
+ const repo = options.repoCommands ? set2.repoCommands : null;
85400
+ const setupCmd = options.setupCmd ?? repo?.setupCommand ?? null;
85401
+ const testCmd = options.testCmd || repo?.testCommand || "";
85402
+ if (!testCmd)
85403
+ throw new Error(
85404
+ `No test command: pass --test-cmd, or set one for ${repository} in Haansi.`
85405
+ );
85406
+ const environment = await environmentOf(deps, { setupCmd, testCmd });
85407
+ const calibrations = [];
85408
+ const states = /* @__PURE__ */ new Map();
85409
+ let reused = 0;
85410
+ let skipped = 0;
85411
+ const tasks = options.limit ? set2.tasks.slice(0, options.limit) : set2.tasks;
85412
+ for (const task of tasks) {
85413
+ const label = task.name ?? task.taskKey;
85414
+ if (!SHA.test(task.baseSha) || !SHA.test(task.targetSha)) {
85415
+ skipped += 1;
85416
+ deps.log(`skip ${label}: its commits are not commit ids`);
85417
+ continue;
85418
+ }
85419
+ if (task.testFiles.length === 0) {
85420
+ skipped += 1;
85421
+ deps.log(
85422
+ `skip ${label}: graded by checks (calibrating by checks comes later)`
85423
+ );
85424
+ continue;
85425
+ }
85426
+ if (!task.testFiles.every(safeTestFile)) {
85427
+ skipped += 1;
85428
+ deps.log(`skip ${label}: a test path is not usable`);
85429
+ continue;
85430
+ }
85431
+ const c = task.calibration;
85432
+ if (!options.force && c && c.inputsFingerprint === task.inputsFingerprint && c.envFingerprint === environment.fingerprint) {
85433
+ reused += 1;
85434
+ states.set(task.taskId, c.state);
85435
+ deps.log(`= ${c.state.padEnd(7)} ${label} (calibrated already)`);
85436
+ continue;
85437
+ }
85438
+ if (!await ensureCommit(deps, task.baseSha) || !await ensureCommit(deps, task.targetSha)) {
85439
+ skipped += 1;
85440
+ deps.log(`skip ${label}: its commits are not reachable from origin`);
85441
+ continue;
85442
+ }
85443
+ const result = await calibrateTask(
85444
+ deps,
85445
+ options,
85446
+ { setupCmd, testCmd },
85447
+ task
85448
+ );
85449
+ calibrations.push(result);
85450
+ states.set(task.taskId, result.state);
85451
+ deps.log(
85452
+ `${result.state.padEnd(7)} ${label}: ${result.reason.replace(/_/g, " ")} (base ${result.runs.base.join(",") || "-"}; target ${result.runs.target.join(",") || "-"})`
85453
+ );
85454
+ }
85455
+ if (options.dryRun || calibrations.length === 0)
85456
+ return { calibrations, reused, skipped, recorded: null, states };
85457
+ let recorded = 0;
85458
+ for (let i = 0; i < calibrations.length; i += REPORT_CHUNK) {
85459
+ const out = await deps.report({
85460
+ runRef: options.runRef,
85461
+ environment,
85462
+ results: calibrations.slice(i, i + REPORT_CHUNK)
85463
+ });
85464
+ recorded += out.recorded;
85465
+ if (out.refused.length)
85466
+ deps.log(
85467
+ `Haansi did not record ${out.refused.length} calibration(s): their task changed since it was listed.`
85468
+ );
85469
+ }
85470
+ return { calibrations, reused, skipped, recorded, states };
85471
+ }
85472
+ async function calibrate(argv) {
85473
+ if (argv.includes("--help")) {
85474
+ console.log(USAGE);
85475
+ return;
85476
+ }
85477
+ const { options, errors } = parseCalibrateArgs(argv);
85478
+ if (errors.length) {
85479
+ for (const e of errors) console.error(e);
85480
+ console.log(USAGE);
85481
+ process.exitCode = 1;
85482
+ return;
85483
+ }
85484
+ options.runRef ??= githubRunRef(process.env);
85485
+ const token = resolveToken5();
85486
+ const apiUrl2 = apiUrl();
85487
+ const work = (0, import_node_fs10.mkdtempSync)((0, import_node_path9.join)((0, import_node_os9.tmpdir)(), "haansi-calibrate-"));
85488
+ try {
85489
+ const { calibrations, reused, skipped, recorded } = await runCalibration(
85490
+ options,
85491
+ {
85492
+ ...localDeps(work),
85493
+ getReplaySet: (repository) => apiGet3(
85494
+ `/memory/replays/tasks?repository=${encodeURIComponent(repository)}`,
85495
+ token,
85496
+ apiUrl2
85497
+ ),
85498
+ report: async (body) => await apiPost2(
85499
+ "/memory/replays/tasks/calibration",
85500
+ body,
85501
+ token,
85502
+ apiUrl2
85503
+ ),
85504
+ env: process.env,
85505
+ cliVersion: cliVersion()
85506
+ }
85507
+ );
85508
+ const count = (state) => calibrations.filter((c) => c.state === state).length;
85509
+ console.log(
85510
+ `
85511
+ Valid: ${count("valid")} \xB7 Invalid: ${count("invalid")} \xB7 Flaky: ${count("flaky")} \xB7 Errors: ${count("error")} \xB7 ${reused} already calibrated \xB7 ${skipped} skipped`
85512
+ );
85513
+ console.log(
85514
+ recorded === null ? options.dryRun ? "Dry run: nothing reported." : "Nothing to report." : `Reported ${recorded} calibration(s) to Haansi.`
85515
+ );
85516
+ } catch (err2) {
85517
+ console.error(err2 instanceof Error ? err2.message : String(err2));
85518
+ process.exitCode = 1;
85519
+ } finally {
85520
+ (0, import_node_fs10.rmSync)(work, { recursive: true, force: true });
85521
+ }
85522
+ }
85523
+ function cliVersion() {
85524
+ try {
85525
+ return String(require_package().version);
85526
+ } catch {
85527
+ return null;
85528
+ }
85529
+ }
85530
+ var import_node_crypto2, import_node_fs10, import_node_os9, import_node_path9, RUNTIMES, REPORT_CHUNK, FAILURE_LINES, STATE, USAGE;
85531
+ var init_replay_calibrate = __esm({
85532
+ "src/commands/replay-calibrate.ts"() {
85533
+ "use strict";
85534
+ import_node_crypto2 = require("node:crypto");
85535
+ import_node_fs10 = require("node:fs");
85536
+ import_node_os9 = require("node:os");
85537
+ import_node_path9 = require("node:path");
85538
+ init_paths();
85539
+ init_api2();
85540
+ init_replay();
85541
+ RUNTIMES = ["node", "python3", "go", "java", "ruby", "rustc"];
85542
+ REPORT_CHUNK = 100;
85543
+ FAILURE_LINES = 5;
85544
+ STATE = {
85545
+ fails_at_base_passes_at_target: "valid",
85546
+ passes_at_base: "invalid",
85547
+ fails_at_target: "invalid",
85548
+ flaky_at_base: "flaky",
85549
+ flaky_at_target: "flaky",
85550
+ setup_failed: "error",
85551
+ checkout_failed: "error"
85552
+ };
85553
+ USAGE = `Usage: haansi replay calibrate --test-cmd "<cmd>" [--setup-cmd "<cmd>"] [--repo-commands]
85554
+ [--runs 2] [--force] [--limit n] [--timeout-min 30]
85555
+ [--run-ref <url>] [--dry-run]
85556
+
85557
+ Checks each task of this repository's replay set with no agent: the tests of
85558
+ its target must fail at its base and pass at its target, --runs times each.
85559
+ Already calibrated tasks are not run again until their inputs or the
85560
+ environment (commands, runtimes, runner, CLI version) change; --force runs
85561
+ them all.`;
85562
+ }
85563
+ });
85564
+
85565
+ // src/commands/replay-tasks.ts
85566
+ var replay_tasks_exports = {};
85567
+ __export(replay_tasks_exports, {
85568
+ areaOf: () => areaOf,
85569
+ cardPrompt: () => cardPrompt,
85570
+ changedFiles: () => changedFiles,
85571
+ overlapOf: () => overlapOf,
85572
+ runStoredReplays: () => runStoredReplays
85573
+ });
85574
+ function cardPrompt(task, feedback) {
85575
+ return [
85576
+ "# Task",
85577
+ "",
85578
+ task.card.problem,
85579
+ "",
85580
+ "## Done when",
85581
+ "",
85582
+ task.card.acceptance,
85583
+ ...task.card.area ? ["", "## Part of the code", "", task.card.area] : [],
85584
+ "",
85585
+ ...feedback ? [FEEDBACK_HEADER, "", "```", feedback, "```", ""] : [],
85586
+ "Work in this repository as it is now. Do not look at later commits or other branches."
85587
+ ].join("\n");
85588
+ }
85589
+ function areaOf(path2) {
85590
+ const dirs = path2.split("/").slice(0, -1);
85591
+ return dirs.length ? dirs.slice(0, 2).join("/") : ".";
85592
+ }
85593
+ async function changedFiles(deps, dir, baseSha) {
85594
+ const lines = (out) => out.code === 0 ? out.output.split("\n").map((l) => l.trim()).filter(Boolean) : [];
85595
+ const diff = await deps.git(
85596
+ ["diff", "--name-only", "--no-renames", baseSha],
85597
+ dir
85598
+ );
85599
+ const added = await deps.git(
85600
+ ["ls-files", "--others", "--exclude-standard"],
85601
+ dir
85602
+ );
85603
+ return [.../* @__PURE__ */ new Set([...lines(diff), ...lines(added)])].filter((f) => !f.startsWith(".haansi-replay/")).sort();
85604
+ }
85605
+ function overlapOf(changed, task) {
85606
+ const tests = new Set(task.testFiles);
85607
+ const mine = new Set(changed.filter((f) => !tests.has(f)));
85608
+ const theirs = new Set(task.targetFiles.filter((f) => !tests.has(f)));
85609
+ const both = [...mine].filter((f) => theirs.has(f)).length;
85610
+ const myAreas = new Set([...mine].map(areaOf));
85611
+ const theirAreas = new Set([...theirs].map(areaOf));
85612
+ const bothAreas = [...myAreas].filter((a) => theirAreas.has(a)).length;
85613
+ const known = mine.size > 0 && theirs.size > 0;
85614
+ return {
85615
+ filePrecision: known ? share(both, mine.size) : null,
85616
+ fileRecall: known ? share(both, theirs.size) : null,
85617
+ areaPrecision: known ? share(bothAreas, myAreas.size) : null,
85618
+ areaRecall: known ? share(bothAreas, theirAreas.size) : null,
85619
+ changedFiles: mine.size
85620
+ };
85621
+ }
85622
+ async function runStoredReplays(options, deps) {
85623
+ const repository = await repositoryOf(deps);
85624
+ const set2 = await deps.getReplaySet(repository);
85625
+ const repo = options.repoCommands ? set2.repoCommands : null;
85626
+ const setupCmd = options.setupCmd ?? repo?.setupCommand ?? null;
85627
+ const testCmd = options.testCmd || repo?.testCommand || "";
85628
+ if (!testCmd)
85629
+ throw new Error(
85630
+ `No test command: pass --test-cmd, or set one for ${repository} in Haansi.`
85631
+ );
85632
+ if (set2.tasks.length === 0) {
85633
+ deps.log(
85634
+ `No replay tasks for ${repository} yet: they need a card (replay task cards on for the organization).`
85635
+ );
85636
+ return { results: [], notValid: 0, recorded: null };
85637
+ }
85638
+ const { states } = await runCalibration(
85639
+ calibrateTaskOptions(options, { setupCmd, testCmd }),
85640
+ { ...deps, report: deps.reportCalibration },
85641
+ set2
85642
+ );
85643
+ const valid = set2.tasks.filter(
85644
+ (t) => states.get(t.taskId) === "valid" && t.testFiles.length > 0 && t.testFiles.every(safeTestFile) && SHA.test(t.baseSha) && SHA.test(t.targetSha)
85645
+ );
85646
+ const notValid = set2.tasks.length - valid.length;
85647
+ let arms = options.arms;
85648
+ if (arms.includes("haansi")) {
85649
+ deps.log(
85650
+ "The with-Haansi arm on stored tasks needs Haansi answering as of each task's start; running the baseline arm only."
85651
+ );
85652
+ arms = arms.filter((a) => a !== "haansi");
85653
+ }
85654
+ const chosen = options.limit ? valid.slice(0, options.limit) : valid;
85655
+ const results = [];
85656
+ for (const task of chosen) {
85657
+ const label = task.name ?? task.taskKey;
85658
+ const asTask = {
85659
+ taskId: task.taskId,
85660
+ // Names the clone's directory.
85661
+ processId: task.taskId,
85662
+ name: label,
85663
+ repository: task.repository,
85664
+ baseSha: task.baseSha,
85665
+ targetPr: task.targetPr,
85666
+ targetSha: task.targetSha,
85667
+ testFiles: task.testFiles,
85668
+ context: []
85669
+ };
85670
+ for (const arm of arms) {
85671
+ let changed = [];
85672
+ try {
85673
+ const r = await replayArm(
85674
+ deps,
85675
+ { ...options, setupCmd, testCmd },
85676
+ asTask,
85677
+ arm,
85678
+ null,
85679
+ (feedback) => cardPrompt(task, feedback),
85680
+ async (dir) => {
85681
+ changed = await changedFiles(deps, dir, task.baseSha);
85682
+ }
85683
+ );
85684
+ const overlap = overlapOf(changed, task);
85685
+ results.push({
85686
+ taskId: task.taskId,
85687
+ cardVersion: task.card.version,
85688
+ inputsFingerprint: task.inputsFingerprint,
85689
+ arm,
85690
+ passed: r.passed,
85691
+ attempts: r.attempts,
85692
+ maxAttempts: r.maxAttempts,
85693
+ durationMs: r.durationMs,
85694
+ overlap
85695
+ });
85696
+ deps.log(
85697
+ `${r.passed ? "pass" : "fail"} ${arm.padEnd(8)} ${label} (${r.attempts}/${r.maxAttempts} attempts; files ${pct(overlap.fileRecall)} of the target's, ${pct(overlap.filePrecision)} of its own on target)`
85698
+ );
85699
+ } catch (err2) {
85700
+ deps.log(
85701
+ `error ${arm} ${label}: ${err2 instanceof Error ? err2.message : String(err2)}`
85702
+ );
85703
+ }
85704
+ }
85705
+ }
85706
+ if (options.dryRun || results.length === 0)
85707
+ return { results, notValid, recorded: null };
85708
+ let recorded = 0;
85709
+ for (let i = 0; i < results.length; i += REPORT_CHUNK2) {
85710
+ const out = await deps.reportResults({
85711
+ agent: options.agentName,
85712
+ model: options.model,
85713
+ runRef: options.runRef,
85714
+ results: results.slice(i, i + REPORT_CHUNK2)
85715
+ });
85716
+ recorded += out.recorded;
85717
+ if (out.refused.length)
85718
+ deps.log(
85719
+ `Haansi did not record ${out.refused.length} result(s): their task's card or inputs changed during the run.`
85720
+ );
85721
+ }
85722
+ return { results, notValid, recorded };
85723
+ }
85724
+ var FEEDBACK_HEADER, REPORT_CHUNK2, share, pct;
85725
+ var init_replay_tasks = __esm({
85726
+ "src/commands/replay-tasks.ts"() {
85727
+ "use strict";
85728
+ init_replay_calibrate();
85729
+ init_replay();
85730
+ FEEDBACK_HEADER = "Your previous attempt did not pass the tests. Their output ends with:";
85731
+ REPORT_CHUNK2 = 200;
85732
+ share = (part, whole) => whole === 0 ? null : Math.round(part / whole * 1e3) / 1e3;
85733
+ pct = (v) => v === null ? "n/a" : `${Math.round(v * 100)}%`;
85734
+ }
85735
+ });
85736
+
85108
85737
  // src/commands/replay.ts
85109
85738
  var replay_exports = {};
85110
85739
  __export(replay_exports, {
85740
+ SHA: () => SHA,
85741
+ cleanClone: () => cleanClone,
85742
+ ensureCommit: () => ensureCommit,
85111
85743
  githubRunRef: () => githubRunRef,
85744
+ localDeps: () => localDeps,
85112
85745
  parseReplayArgs: () => parseReplayArgs,
85113
85746
  replay: () => replay,
85114
85747
  replayArm: () => replayArm,
@@ -85160,8 +85793,8 @@ function parseReplayArgs(argv) {
85160
85793
  else if (!sub && !a.startsWith("-")) sub = a;
85161
85794
  else errors.push(`Unknown option: ${a}`);
85162
85795
  }
85163
- if (!/^[0-9a-f-]{36}$/i.test(options.familyId))
85164
- errors.push("--family <id> is required (the family's id).");
85796
+ if (options.familyId && !/^[0-9a-f-]{36}$/i.test(options.familyId))
85797
+ errors.push("--family must be a family's id.");
85165
85798
  if (!options.agentCmd) errors.push("--agent-cmd is required.");
85166
85799
  if (!options.testCmd && !options.repoCommands)
85167
85800
  errors.push(
@@ -85238,7 +85871,7 @@ async function ensureCommit(deps, sha) {
85238
85871
  return true;
85239
85872
  return (await deps.git(["fetch", "--quiet", "origin", sha], deps.repoDir)).code === 0;
85240
85873
  }
85241
- async function cleanCheckout(deps, dir, task) {
85874
+ async function cleanClone(deps, dir, sha, unreachable) {
85242
85875
  const steps = [
85243
85876
  [["init", "--quiet", dir], deps.repoDir],
85244
85877
  [
@@ -85248,7 +85881,7 @@ async function cleanCheckout(deps, dir, task) {
85248
85881
  "--no-tags",
85249
85882
  "--no-write-fetch-head",
85250
85883
  deps.repoDir,
85251
- `${task.baseSha}:refs/heads/${REPLAY_BRANCH}`
85884
+ `${sha}:refs/heads/${REPLAY_BRANCH}`
85252
85885
  ],
85253
85886
  dir
85254
85887
  ],
@@ -85256,18 +85889,22 @@ async function cleanCheckout(deps, dir, task) {
85256
85889
  ];
85257
85890
  for (const [args, cwd] of steps) {
85258
85891
  if ((await deps.git(args, cwd)).code !== 0)
85259
- throw new Error(`could not check out ${task.baseSha}`);
85892
+ throw new Error(`could not check out ${sha}`);
85260
85893
  }
85894
+ if (unreachable === null) return;
85261
85895
  const leak = await deps.git(
85262
- ["cat-file", "-e", `${task.targetSha}^{commit}`],
85896
+ ["cat-file", "-e", `${unreachable}^{commit}`],
85263
85897
  dir
85264
85898
  );
85265
85899
  if (leak.code === 0)
85266
85900
  throw new Error("the fix's commit is readable in the clone: run refused");
85267
85901
  }
85268
- async function replayArm(deps, options, task, arm, playbook) {
85902
+ function cleanCheckout(deps, dir, task) {
85903
+ return cleanClone(deps, dir, task.baseSha, task.targetSha);
85904
+ }
85905
+ async function replayArm(deps, options, task, arm, playbook, promptOf = (feedback) => replayPrompt(task, arm, feedback), inspect) {
85269
85906
  const started = deps.now();
85270
- const dir = (0, import_node_path9.join)(
85907
+ const dir = (0, import_node_path10.join)(
85271
85908
  deps.workDir(),
85272
85909
  `${task.processId.replace(/[^a-zA-Z0-9_-]/g, "")}-${arm}`
85273
85910
  );
@@ -85293,15 +85930,15 @@ async function replayArm(deps, options, task, arm, playbook) {
85293
85930
  }
85294
85931
  deps.log(`\u2026 ${label}: set up in ${since(at)}`);
85295
85932
  }
85296
- const promptFile = (0, import_node_path9.join)(dir, ".haansi-replay", "prompt.md");
85933
+ const promptFile = (0, import_node_path10.join)(dir, ".haansi-replay", "prompt.md");
85297
85934
  if (arm === "haansi" && playbook)
85298
85935
  deps.writeFile(
85299
- (0, import_node_path9.join)(dir, ".haansi-replay", "playbook.md"),
85936
+ (0, import_node_path10.join)(dir, ".haansi-replay", "playbook.md"),
85300
85937
  playbook.skillMd
85301
85938
  );
85302
85939
  let feedback = null;
85303
85940
  for (let attempt = 1; attempt <= options.attempts; attempt++) {
85304
- const prompt2 = replayPrompt(task, arm, feedback);
85941
+ const prompt2 = promptOf(feedback);
85305
85942
  deps.writeFile(promptFile, prompt2);
85306
85943
  const agentAt = deps.now();
85307
85944
  deps.log(
@@ -85313,11 +85950,11 @@ async function replayArm(deps, options, task, arm, playbook) {
85313
85950
  { HAANSI_REPLAY_PROMPT: prompt2, HAANSI_REPLAY_PROMPT_FILE: promptFile },
85314
85951
  options.timeoutMs
85315
85952
  );
85316
- const own = new Map(files.map((f) => [f, deps.readFile((0, import_node_path9.join)(dir, f))]));
85953
+ const own = new Map(files.map((f) => [f, deps.readFile((0, import_node_path10.join)(dir, f))]));
85317
85954
  for (const f of files) {
85318
85955
  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);
85956
+ if (content === null) deps.removeFile((0, import_node_path10.join)(dir, f));
85957
+ else deps.writeFile((0, import_node_path10.join)(dir, f), content);
85321
85958
  }
85322
85959
  const testAt = deps.now();
85323
85960
  deps.log(`\u2026 ${label}: agent done in ${since(agentAt)}, grading`);
@@ -85331,10 +85968,11 @@ async function replayArm(deps, options, task, arm, playbook) {
85331
85968
  `\u2026 ${label}: tests ${test.code === 0 ? "passed" : "failed"} in ${since(testAt)}`
85332
85969
  );
85333
85970
  if (test.code !== 0) {
85334
- for (const line of test.output.trim().split("\n").slice(-FAILURE_LINES))
85971
+ for (const line of test.output.trim().split("\n").slice(-FAILURE_LINES2))
85335
85972
  deps.log(` ${line}`);
85336
85973
  }
85337
85974
  if (test.code === 0) {
85975
+ if (inspect) await inspect(dir);
85338
85976
  return {
85339
85977
  processId: task.processId,
85340
85978
  arm,
@@ -85346,11 +85984,12 @@ async function replayArm(deps, options, task, arm, playbook) {
85346
85984
  };
85347
85985
  }
85348
85986
  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);
85987
+ if (content === null) deps.removeFile((0, import_node_path10.join)(dir, file2));
85988
+ else deps.writeFile((0, import_node_path10.join)(dir, file2), content);
85351
85989
  }
85352
85990
  feedback = test.output.slice(-FEEDBACK_CHARS);
85353
85991
  }
85992
+ if (inspect) await inspect(dir);
85354
85993
  return {
85355
85994
  processId: task.processId,
85356
85995
  arm,
@@ -85477,17 +86116,54 @@ function runCommand(file2, args, cwd, env, timeoutMs, stdoutOnly = false) {
85477
86116
  });
85478
86117
  });
85479
86118
  }
86119
+ function localDeps(work) {
86120
+ return {
86121
+ git: (args, cwd) => runCommand("git", args, cwd, {}, 10 * 6e4),
86122
+ showFile: async (sha, path2) => {
86123
+ const shown = await runCommand(
86124
+ "git",
86125
+ ["show", `${sha}:${path2}`],
86126
+ process.cwd(),
86127
+ {},
86128
+ 6e4,
86129
+ true
86130
+ );
86131
+ return shown.code === 0 ? shown.output : null;
86132
+ },
86133
+ shell: (command2, cwd, env, timeoutMs) => runCommand("sh", ["-c", command2], cwd, env, timeoutMs),
86134
+ repoDir: process.cwd(),
86135
+ workDir: () => work,
86136
+ writeFile: (path2, content) => {
86137
+ (0, import_node_fs11.mkdirSync)((0, import_node_path10.dirname)(path2), { recursive: true });
86138
+ (0, import_node_fs11.writeFileSync)(path2, content);
86139
+ },
86140
+ readFile: (path2) => (0, import_node_fs11.existsSync)(path2) ? (0, import_node_fs11.readFileSync)(path2, "utf-8") : null,
86141
+ removeFile: (path2) => (0, import_node_fs11.rmSync)(path2, { force: true }),
86142
+ removeDir: (path2) => (0, import_node_fs11.rmSync)(path2, { recursive: true, force: true }),
86143
+ now: () => Date.now(),
86144
+ log: (line) => console.log(line)
86145
+ };
86146
+ }
85480
86147
  async function replay(argv) {
86148
+ if (argv[0] === "calibrate") {
86149
+ const { calibrate: calibrate2 } = await Promise.resolve().then(() => (init_replay_calibrate(), replay_calibrate_exports));
86150
+ await calibrate2(argv.slice(1));
86151
+ return;
86152
+ }
85481
86153
  const { sub, options, errors } = parseReplayArgs(argv);
85482
86154
  if (sub !== "run" || errors.length) {
85483
86155
  for (const e of sub === "run" ? errors : []) console.error(e);
85484
- console.log(USAGE);
86156
+ console.log(USAGE2);
85485
86157
  if (sub === "run") process.exitCode = 1;
85486
86158
  return;
85487
86159
  }
85488
86160
  const token = resolveToken5();
85489
86161
  const apiUrl2 = apiUrl();
85490
- const work = (0, import_node_fs10.mkdtempSync)((0, import_node_path9.join)((0, import_node_os9.tmpdir)(), "haansi-replay-"));
86162
+ if (!options.familyId) {
86163
+ await replayStored(options, token, apiUrl2);
86164
+ return;
86165
+ }
86166
+ const work = (0, import_node_fs11.mkdtempSync)((0, import_node_path10.join)((0, import_node_os10.tmpdir)(), "haansi-replay-"));
85491
86167
  try {
85492
86168
  const { results, skipped, recorded } = await runReplays(options, {
85493
86169
  getTasks: async (familyId, cutoff) => (await apiGet3(
@@ -85506,30 +86182,7 @@ async function replay(argv) {
85506
86182
  token,
85507
86183
  apiUrl2
85508
86184
  ),
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)
86185
+ ...localDeps(work)
85533
86186
  });
85534
86187
  const passed = (arm) => results.filter((r) => r.arm === arm && r.passed).length;
85535
86188
  const ran = (arm) => results.filter((r) => r.arm === arm).length;
@@ -85541,30 +86194,79 @@ Baseline: ${passed("baseline")}/${ran("baseline")} passed \xB7 With Haansi: ${pa
85541
86194
  recorded === null ? options.dryRun ? "Dry run: nothing reported." : "Nothing to report." : `Reported ${recorded} result(s) to Haansi.`
85542
86195
  );
85543
86196
  } finally {
85544
- (0, import_node_fs10.rmSync)(work, { recursive: true, force: true });
86197
+ (0, import_node_fs11.rmSync)(work, { recursive: true, force: true });
85545
86198
  }
85546
86199
  }
85547
- var import_node_child_process4, import_node_fs10, import_node_os9, import_node_path9, SHA, ARMS, FEEDBACK_CHARS, FAILURE_LINES, REPLAY_BRANCH, USAGE;
86200
+ async function replayStored(options, token, apiUrl2) {
86201
+ const { runStoredReplays: runStoredReplays2 } = await Promise.resolve().then(() => (init_replay_tasks(), replay_tasks_exports));
86202
+ const { cliVersion: cliVersion2 } = await Promise.resolve().then(() => (init_replay_calibrate(), replay_calibrate_exports));
86203
+ const work = (0, import_node_fs11.mkdtempSync)((0, import_node_path10.join)((0, import_node_os10.tmpdir)(), "haansi-replay-"));
86204
+ try {
86205
+ const { results, notValid, recorded } = await runStoredReplays2(options, {
86206
+ ...localDeps(work),
86207
+ getReplaySet: (repository) => apiGet3(
86208
+ `/memory/replays/tasks?repository=${encodeURIComponent(repository)}`,
86209
+ token,
86210
+ apiUrl2
86211
+ ),
86212
+ reportCalibration: async (body) => await apiPost2(
86213
+ "/memory/replays/tasks/calibration",
86214
+ body,
86215
+ token,
86216
+ apiUrl2
86217
+ ),
86218
+ reportResults: async (body) => await apiPost2(
86219
+ "/memory/replays/tasks/results",
86220
+ body,
86221
+ token,
86222
+ apiUrl2
86223
+ ),
86224
+ env: process.env,
86225
+ cliVersion: cliVersion2()
86226
+ });
86227
+ const passed = results.filter((r) => r.passed).length;
86228
+ console.log(
86229
+ `
86230
+ Baseline: ${passed}/${results.length} passed \xB7 ${notValid} task(s) not valid to replay`
86231
+ );
86232
+ console.log(
86233
+ recorded === null ? options.dryRun ? "Dry run: nothing reported." : "Nothing to report." : `Reported ${recorded} result(s) to Haansi.`
86234
+ );
86235
+ } catch (err2) {
86236
+ console.error(err2 instanceof Error ? err2.message : String(err2));
86237
+ process.exitCode = 1;
86238
+ } finally {
86239
+ (0, import_node_fs11.rmSync)(work, { recursive: true, force: true });
86240
+ }
86241
+ }
86242
+ var import_node_child_process4, import_node_fs11, import_node_os10, import_node_path10, SHA, ARMS, FEEDBACK_CHARS, FAILURE_LINES2, REPLAY_BRANCH, USAGE2;
85548
86243
  var init_replay = __esm({
85549
86244
  "src/commands/replay.ts"() {
85550
86245
  "use strict";
85551
86246
  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");
86247
+ import_node_fs11 = require("node:fs");
86248
+ import_node_os10 = require("node:os");
86249
+ import_node_path10 = require("node:path");
85555
86250
  init_paths();
85556
86251
  init_api2();
85557
86252
  SHA = /^[0-9a-f]{7,40}$/i;
85558
86253
  ARMS = ["baseline", "haansi"];
85559
86254
  FEEDBACK_CHARS = 6e3;
85560
- FAILURE_LINES = 8;
86255
+ FAILURE_LINES2 = 8;
85561
86256
  REPLAY_BRANCH = "replay";
85562
- USAGE = `Usage: haansi replay run --family <id> --agent-cmd "<cmd>" --test-cmd "<cmd>"
86257
+ USAGE2 = `Usage: haansi replay run [--family <id>] --agent-cmd "<cmd>" --test-cmd "<cmd>"
85563
86258
  [--setup-cmd "<cmd>"] [--attempts 3] [--arms baseline,haansi] [--limit n]
85564
86259
  [--cutoff <date>] [--timeout-min 30]
85565
86260
  [--agent-name <name>] [--model <name>] [--run-ref <url>] [--dry-run]
85566
86261
  [--repo-commands]
85567
86262
 
86263
+ haansi replay calibrate --test-cmd "<cmd>" [--setup-cmd "<cmd>"] (see --help)
86264
+
86265
+ Without --family, runs this repository's stored replay tasks: calibrates them
86266
+ first, then replays the valid ones with their task card as the prompt, graded
86267
+ by the target's tests and where the change landed. With --family, replays that
86268
+ problem family's tasks with and without its playbook.
86269
+
85568
86270
  --repo-commands uses the setup and test commands an org admin set for the
85569
86271
  repository in Haansi (Settings \u2192 Data sources) for any not given here.
85570
86272
 
@@ -85790,9 +86492,13 @@ Commands:
85790
86492
  digest. [--date YYYY-MM-DD] [--workstream <id>]
85791
86493
  digest [--week|--month|--since YYYY-MM-DD]
85792
86494
  Your weekly digest (default: the latest week)
85793
- replay run --family <id> --agent-cmd "<cmd>" --test-cmd "<cmd>"
85794
- Replay a problem family's tasks in your CI, with and
85795
- without its playbook, and report the results
86495
+ replay run --agent-cmd "<cmd>" --test-cmd "<cmd>" [--family <id>]
86496
+ Replay this repository's tasks in your CI (or a
86497
+ problem family's, with and without its playbook)
86498
+ and report the results
86499
+ replay calibrate --test-cmd "<cmd>"
86500
+ Check this repository's replay tasks are valid: their
86501
+ tests fail at the start and pass at the fix
85796
86502
  skills sync [--agent <sel>] [--project] [--dir <path>]
85797
86503
  Copy your team's published skills into Claude Code / Codex
85798
86504
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haansi",
3
- "version": "0.1.54",
3
+ "version": "0.1.56",
4
4
  "description": "Haansi CLI - Session collector and MCP server for Claude Code & OpenAI Codex",
5
5
  "bin": {
6
6
  "haansi": "./dist/haansi.js"