haansi 0.1.55 → 0.1.57

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 +405 -26
  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.55",
98
+ version: "0.1.57",
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,34 @@ 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_sessions", {
19224
+ excluded_process_ids: [
19225
+ "join-key",
19226
+ "process_registry.process_id of the replayed workstream, matched by equality."
19227
+ ],
19228
+ excluded_family_ids: [
19229
+ "join-key",
19230
+ "problem_families ids holding the replayed workstream, matched by equality."
19231
+ ]
19232
+ }),
19233
+ ...exchangeExclusions(INTEL, "replay_session_calls", {
19234
+ server: ["operational", "The MCP server's name, e.g. haansi-skills."],
19235
+ tool: ["operational", "The MCP tool's name, e.g. search_skills."],
19236
+ returned_ids: [
19237
+ "join-key",
19238
+ "Ids of the solutions, skills and revisions a tool returned."
19239
+ ]
19240
+ }),
19241
+ ...exchangeExclusions(INTEL, "replay_task_results", {
19242
+ inputs_fingerprint: [
19243
+ "derived",
19244
+ "A SHA-256 of the task's inputs the replay ran on; not reversible."
19245
+ ],
19246
+ arm: ["operational", "Closed CHECK vocabulary (baseline, haansi)."],
19247
+ agent: ["operational", "The agent CLI's name, e.g. claude-code."],
19248
+ model: ["operational", "The model name the replay ran with."],
19249
+ run_ref: ["not-personal", "A link to the customer's CI run."]
19250
+ }),
19223
19251
  ...exchangeExclusions(INTEL, "replay_tasks", {
19224
19252
  process_id: [
19225
19253
  "join-key",
@@ -19305,7 +19333,11 @@ var init_exclusions = __esm({
19305
19333
  "The CI's setup and test commands an org admin or CI owner typed, runtime versions, the runner image and the CLI version."
19306
19334
  ],
19307
19335
  calibration_runs: ["operational", "Exit codes of the test runs."],
19308
- calibration_run_ref: ["not-personal", "A link to the customer's CI run."]
19336
+ calibration_run_ref: ["not-personal", "A link to the customer's CI run."],
19337
+ review_state: [
19338
+ "operational",
19339
+ "Closed CHECK vocabulary (approved, excluded)."
19340
+ ]
19309
19341
  }),
19310
19342
  ...exchangeExclusions(INTEL, "problem_family_members", {
19311
19343
  process_id: [
@@ -26570,6 +26602,15 @@ var init_table_registry = __esm({
26570
26602
  entry("intelligence", INTELLIGENCE_DB, "stored_replay_tasks", "direct", {
26571
26603
  scopingColumn: "organization_id"
26572
26604
  }),
26605
+ entry("intelligence", INTELLIGENCE_DB, "replay_task_results", "direct", {
26606
+ scopingColumn: "organization_id"
26607
+ }),
26608
+ entry("intelligence", INTELLIGENCE_DB, "replay_sessions", "direct", {
26609
+ scopingColumn: "organization_id"
26610
+ }),
26611
+ entry("intelligence", INTELLIGENCE_DB, "replay_session_calls", "direct", {
26612
+ scopingColumn: "organization_id"
26613
+ }),
26573
26614
  // A person's revert verdicts, the checks that validate a change and the
26574
26615
  // replay commands, per repository (outcome evidence v2 plan).
26575
26616
  entry("intelligence", INTELLIGENCE_DB, "revert_labels", "direct", {
@@ -83591,7 +83632,7 @@ function resolveToken3() {
83591
83632
  return null;
83592
83633
  }
83593
83634
  function triggerCollectInBackground() {
83594
- if (isCollecting) return;
83635
+ if (isCollecting || REPLAY_SESSION) return;
83595
83636
  isCollecting = true;
83596
83637
  collect({}).then(() => {
83597
83638
  lastCollectTime = Date.now();
@@ -83608,7 +83649,8 @@ function maybeCollect() {
83608
83649
  async function apiGet2(path2, options) {
83609
83650
  const headers = {
83610
83651
  Authorization: `Bearer ${TOKEN}`,
83611
- ...options?.extraHeaders
83652
+ ...options?.extraHeaders,
83653
+ ...REPLAY_HEADERS
83612
83654
  };
83613
83655
  const response = await fetchWithTimeout(`${API_URL}/api/v1${path2}`, {
83614
83656
  headers
@@ -83623,7 +83665,8 @@ async function apiPost(path2, body, options) {
83623
83665
  const headers = {
83624
83666
  Authorization: `Bearer ${TOKEN}`,
83625
83667
  "Content-Type": "application/json",
83626
- ...options?.extraHeaders
83668
+ ...options?.extraHeaders,
83669
+ ...REPLAY_HEADERS
83627
83670
  };
83628
83671
  const response = await fetchWithTimeout(`${API_URL}/api/v1${path2}`, {
83629
83672
  method: "POST",
@@ -83668,7 +83711,7 @@ async function main3() {
83668
83711
  }
83669
83712
  warmupInBackground();
83670
83713
  }
83671
- var import_dotenv3, import_path5, import_fs6, import_os5, DEFAULT_API_URL3, CREDENTIALS_FILE5, API_URL, AGENT_SOURCE, DEFAULT_SOURCE_TOOL, TOKEN, contextSchema, isCollecting, lastCollectTime, COLLECT_COOLDOWN_MS, mcpServer, server;
83714
+ var import_dotenv3, import_path5, import_fs6, import_os5, DEFAULT_API_URL3, CREDENTIALS_FILE5, API_URL, REPLAY_SESSION, REPLAY_HEADERS, AGENT_SOURCE, DEFAULT_SOURCE_TOOL, TOKEN, contextSchema, isCollecting, lastCollectTime, COLLECT_COOLDOWN_MS, mcpServer, server;
83672
83715
  var init_mcp_server2 = __esm({
83673
83716
  "../service-capture/agent-sessions/mcp-server.ts"() {
83674
83717
  "use strict";
@@ -83687,6 +83730,10 @@ var init_mcp_server2 = __esm({
83687
83730
  DEFAULT_API_URL3 = SERVICE_URL_DEFAULTS.haansiApiBaseUrl;
83688
83731
  CREDENTIALS_FILE5 = (0, import_path5.join)((0, import_os5.homedir)(), ".haansi", "credentials.json");
83689
83732
  API_URL = process.env.HAANSI_API_URL ?? DEFAULT_API_URL3;
83733
+ REPLAY_SESSION = /^[0-9a-f-]{36}$/i.test(
83734
+ process.env.HAANSI_REPLAY_SESSION?.trim() ?? ""
83735
+ ) ? process.env.HAANSI_REPLAY_SESSION.trim() : null;
83736
+ REPLAY_HEADERS = REPLAY_SESSION ? { "X-Haansi-Replay-Session": REPLAY_SESSION } : {};
83690
83737
  AGENT_SOURCE = process.env.HAANSI_AGENT_SOURCE?.toLowerCase();
83691
83738
  DEFAULT_SOURCE_TOOL = AGENT_SOURCE === "codex" ? "codex-mcp" : "claude-code-mcp";
83692
83739
  TOKEN = resolveToken3();
@@ -83907,6 +83954,13 @@ var init_mcp_server2 = __esm({
83907
83954
  };
83908
83955
  }
83909
83956
  case "save_solution": {
83957
+ if (REPLAY_SESSION) {
83958
+ return {
83959
+ content: [
83960
+ { type: "text", text: "Not saved: nothing is recorded here." }
83961
+ ]
83962
+ };
83963
+ }
83910
83964
  const {
83911
83965
  problem_description,
83912
83966
  solution_summary,
@@ -85201,8 +85255,11 @@ __export(replay_calibrate_exports, {
85201
85255
  RUNTIMES: () => RUNTIMES,
85202
85256
  calibrate: () => calibrate,
85203
85257
  calibrateTask: () => calibrateTask,
85258
+ calibrateTaskOptions: () => calibrateTaskOptions,
85259
+ cliVersion: () => cliVersion,
85204
85260
  environmentOf: () => environmentOf,
85205
85261
  parseCalibrateArgs: () => parseCalibrateArgs,
85262
+ repositoryOf: () => repositoryOf,
85206
85263
  runCalibration: () => runCalibration,
85207
85264
  runnerOf: () => runnerOf
85208
85265
  });
@@ -85245,6 +85302,19 @@ function parseCalibrateArgs(argv) {
85245
85302
  errors.push("--timeout-min must be a positive number.");
85246
85303
  return { options, errors };
85247
85304
  }
85305
+ function calibrateTaskOptions(run3, commands) {
85306
+ return {
85307
+ setupCmd: commands.setupCmd,
85308
+ testCmd: commands.testCmd,
85309
+ repoCommands: false,
85310
+ runs: 2,
85311
+ force: false,
85312
+ limit: run3.limit,
85313
+ timeoutMs: run3.timeoutMs,
85314
+ runRef: run3.runRef,
85315
+ dryRun: run3.dryRun
85316
+ };
85317
+ }
85248
85318
  function runnerOf(env) {
85249
85319
  if (env.ImageOS && env.ImageVersion)
85250
85320
  return `${env.ImageOS}-${env.ImageVersion}`;
@@ -85357,12 +85427,16 @@ async function calibrateTask(deps, options, commands, task) {
85357
85427
  }
85358
85428
  return done("fails_at_base_passes_at_target");
85359
85429
  }
85360
- async function runCalibration(options, deps) {
85430
+ async function repositoryOf(deps) {
85361
85431
  const remote = await deps.git(["remote", "get-url", "origin"], deps.repoDir);
85362
85432
  const repository = remote.code === 0 ? repositoryFromRemote(remote.output) : null;
85363
85433
  if (!repository)
85364
85434
  throw new Error("Run from a clone of the repository (no origin remote).");
85365
- const set2 = await deps.getReplaySet(repository);
85435
+ return repository;
85436
+ }
85437
+ async function runCalibration(options, deps, preloaded) {
85438
+ const repository = await repositoryOf(deps);
85439
+ const set2 = preloaded ?? await deps.getReplaySet(repository);
85366
85440
  const repo = options.repoCommands ? set2.repoCommands : null;
85367
85441
  const setupCmd = options.setupCmd ?? repo?.setupCommand ?? null;
85368
85442
  const testCmd = options.testCmd || repo?.testCommand || "";
@@ -85372,17 +85446,12 @@ async function runCalibration(options, deps) {
85372
85446
  );
85373
85447
  const environment = await environmentOf(deps, { setupCmd, testCmd });
85374
85448
  const calibrations = [];
85449
+ const states = /* @__PURE__ */ new Map();
85375
85450
  let reused = 0;
85376
85451
  let skipped = 0;
85377
85452
  const tasks = options.limit ? set2.tasks.slice(0, options.limit) : set2.tasks;
85378
85453
  for (const task of tasks) {
85379
85454
  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
85455
  if (!SHA.test(task.baseSha) || !SHA.test(task.targetSha)) {
85387
85456
  skipped += 1;
85388
85457
  deps.log(`skip ${label}: its commits are not commit ids`);
@@ -85400,6 +85469,13 @@ async function runCalibration(options, deps) {
85400
85469
  deps.log(`skip ${label}: a test path is not usable`);
85401
85470
  continue;
85402
85471
  }
85472
+ const c = task.calibration;
85473
+ if (!options.force && c && c.inputsFingerprint === task.inputsFingerprint && c.envFingerprint === environment.fingerprint) {
85474
+ reused += 1;
85475
+ states.set(task.taskId, c.state);
85476
+ deps.log(`= ${c.state.padEnd(7)} ${label} (calibrated already)`);
85477
+ continue;
85478
+ }
85403
85479
  if (!await ensureCommit(deps, task.baseSha) || !await ensureCommit(deps, task.targetSha)) {
85404
85480
  skipped += 1;
85405
85481
  deps.log(`skip ${label}: its commits are not reachable from origin`);
@@ -85412,12 +85488,13 @@ async function runCalibration(options, deps) {
85412
85488
  task
85413
85489
  );
85414
85490
  calibrations.push(result);
85491
+ states.set(task.taskId, result.state);
85415
85492
  deps.log(
85416
85493
  `${result.state.padEnd(7)} ${label}: ${result.reason.replace(/_/g, " ")} (base ${result.runs.base.join(",") || "-"}; target ${result.runs.target.join(",") || "-"})`
85417
85494
  );
85418
85495
  }
85419
85496
  if (options.dryRun || calibrations.length === 0)
85420
- return { calibrations, reused, skipped, recorded: null };
85497
+ return { calibrations, reused, skipped, recorded: null, states };
85421
85498
  let recorded = 0;
85422
85499
  for (let i = 0; i < calibrations.length; i += REPORT_CHUNK) {
85423
85500
  const out = await deps.report({
@@ -85431,7 +85508,7 @@ async function runCalibration(options, deps) {
85431
85508
  `Haansi did not record ${out.refused.length} calibration(s): their task changed since it was listed.`
85432
85509
  );
85433
85510
  }
85434
- return { calibrations, reused, skipped, recorded };
85511
+ return { calibrations, reused, skipped, recorded, states };
85435
85512
  }
85436
85513
  async function calibrate(argv) {
85437
85514
  if (argv.includes("--help")) {
@@ -85526,6 +85603,227 @@ them all.`;
85526
85603
  }
85527
85604
  });
85528
85605
 
85606
+ // src/commands/replay-tasks.ts
85607
+ var replay_tasks_exports = {};
85608
+ __export(replay_tasks_exports, {
85609
+ HAANSI_LINE: () => HAANSI_LINE,
85610
+ areaOf: () => areaOf,
85611
+ cardPrompt: () => cardPrompt,
85612
+ changedFiles: () => changedFiles,
85613
+ haansiText: () => haansiText,
85614
+ haansiUseOf: () => haansiUseOf,
85615
+ mcpConfig: () => mcpConfig,
85616
+ overlapOf: () => overlapOf,
85617
+ runStoredReplays: () => runStoredReplays
85618
+ });
85619
+ function cardPrompt(task, feedback, arm = "baseline") {
85620
+ return [
85621
+ "# Task",
85622
+ "",
85623
+ task.card.problem,
85624
+ "",
85625
+ "## Done when",
85626
+ "",
85627
+ task.card.acceptance,
85628
+ ...task.card.area ? ["", "## Part of the code", "", task.card.area] : [],
85629
+ "",
85630
+ ...arm === "haansi" ? [HAANSI_LINE, ""] : [],
85631
+ ...feedback ? [FEEDBACK_HEADER, "", "```", feedback, "```", ""] : [],
85632
+ "Work in this repository as it is now. Do not look at later commits or other branches."
85633
+ ].join("\n");
85634
+ }
85635
+ function mcpConfig(arm, session) {
85636
+ if (arm !== "haansi" || !session) return JSON.stringify({ mcpServers: {} });
85637
+ const base = session.apiUrl.replace(/\/+$/, "");
85638
+ return JSON.stringify(
85639
+ {
85640
+ mcpServers: {
85641
+ "haansi-solutions": {
85642
+ command: "haansi",
85643
+ args: ["mcp-server"],
85644
+ env: {
85645
+ HAANSI_API_URL: base,
85646
+ HAANSI_TOKEN: session.token,
85647
+ HAANSI_REPLAY_SESSION: session.sessionId
85648
+ }
85649
+ },
85650
+ "haansi-skills": {
85651
+ type: "http",
85652
+ url: `${base}/api/v1/memory/skills/mcp`,
85653
+ headers: {
85654
+ Authorization: `Bearer ${session.token}`,
85655
+ "X-Haansi-Replay-Session": session.sessionId
85656
+ }
85657
+ }
85658
+ }
85659
+ },
85660
+ null,
85661
+ 2
85662
+ );
85663
+ }
85664
+ function areaOf(path2) {
85665
+ const dirs = path2.split("/").slice(0, -1);
85666
+ return dirs.length ? dirs.slice(0, 2).join("/") : ".";
85667
+ }
85668
+ async function changedFiles(deps, dir, baseSha) {
85669
+ const lines = (out) => out.code === 0 ? out.output.split("\n").map((l) => l.trim()).filter(Boolean) : [];
85670
+ const diff = await deps.git(
85671
+ ["diff", "--name-only", "--no-renames", baseSha],
85672
+ dir
85673
+ );
85674
+ const added = await deps.git(
85675
+ ["ls-files", "--others", "--exclude-standard"],
85676
+ dir
85677
+ );
85678
+ return [.../* @__PURE__ */ new Set([...lines(diff), ...lines(added)])].filter((f) => !f.startsWith(".haansi-replay/")).sort();
85679
+ }
85680
+ function overlapOf(changed, task) {
85681
+ const tests = new Set(task.testFiles);
85682
+ const mine = new Set(changed.filter((f) => !tests.has(f)));
85683
+ const theirs = new Set(task.targetFiles.filter((f) => !tests.has(f)));
85684
+ const both = [...mine].filter((f) => theirs.has(f)).length;
85685
+ const myAreas = new Set([...mine].map(areaOf));
85686
+ const theirAreas = new Set([...theirs].map(areaOf));
85687
+ const bothAreas = [...myAreas].filter((a) => theirAreas.has(a)).length;
85688
+ const known = mine.size > 0 && theirs.size > 0;
85689
+ return {
85690
+ filePrecision: known ? share(both, mine.size) : null,
85691
+ fileRecall: known ? share(both, theirs.size) : null,
85692
+ areaPrecision: known ? share(bothAreas, myAreas.size) : null,
85693
+ areaRecall: known ? share(bothAreas, theirAreas.size) : null,
85694
+ changedFiles: mine.size
85695
+ };
85696
+ }
85697
+ async function runStoredReplays(options, deps) {
85698
+ const repository = await repositoryOf(deps);
85699
+ const set2 = await deps.getReplaySet(repository);
85700
+ const repo = options.repoCommands ? set2.repoCommands : null;
85701
+ const setupCmd = options.setupCmd ?? repo?.setupCommand ?? null;
85702
+ const testCmd = options.testCmd || repo?.testCommand || "";
85703
+ if (!testCmd)
85704
+ throw new Error(
85705
+ `No test command: pass --test-cmd, or set one for ${repository} in Haansi.`
85706
+ );
85707
+ if (set2.tasks.length === 0) {
85708
+ deps.log(
85709
+ `No replay tasks for ${repository} yet: they need a card (replay task cards on for the organization).`
85710
+ );
85711
+ return { results: [], notValid: 0, recorded: null };
85712
+ }
85713
+ const { states } = await runCalibration(
85714
+ calibrateTaskOptions(options, { setupCmd, testCmd }),
85715
+ { ...deps, report: deps.reportCalibration },
85716
+ set2
85717
+ );
85718
+ const valid = set2.tasks.filter(
85719
+ (t) => states.get(t.taskId) === "valid" && t.testFiles.length > 0 && t.testFiles.every(safeTestFile) && SHA.test(t.baseSha) && SHA.test(t.targetSha)
85720
+ );
85721
+ const notValid = set2.tasks.length - valid.length;
85722
+ const arms = options.arms;
85723
+ const chosen = options.limit ? valid.slice(0, options.limit) : valid;
85724
+ const results = [];
85725
+ for (const task of chosen) {
85726
+ const label = task.name ?? task.taskKey;
85727
+ const asTask = {
85728
+ taskId: task.taskId,
85729
+ // Names the clone's directory.
85730
+ processId: task.taskId,
85731
+ name: label,
85732
+ repository: task.repository,
85733
+ baseSha: task.baseSha,
85734
+ targetPr: task.targetPr,
85735
+ targetSha: task.targetSha,
85736
+ testFiles: task.testFiles,
85737
+ context: []
85738
+ };
85739
+ for (const arm of arms) {
85740
+ let changed = [];
85741
+ try {
85742
+ const session = arm === "haansi" ? {
85743
+ ...await deps.openSession(task.taskId),
85744
+ apiUrl: deps.apiUrl,
85745
+ token: deps.token
85746
+ } : null;
85747
+ const configPath = `${deps.workDir()}/${task.taskId}-${arm}.mcp.json`;
85748
+ deps.writeFile(configPath, mcpConfig(arm, session));
85749
+ const r = await replayArm(
85750
+ deps,
85751
+ { ...options, setupCmd, testCmd },
85752
+ asTask,
85753
+ arm,
85754
+ null,
85755
+ (feedback) => cardPrompt(task, feedback, arm),
85756
+ async (dir) => {
85757
+ changed = await changedFiles(deps, dir, task.baseSha);
85758
+ },
85759
+ { HAANSI_REPLAY_MCP_CONFIG: configPath }
85760
+ );
85761
+ deps.removeFile(configPath);
85762
+ const haansi = session ? haansiUseOf((await deps.getCalls(session.sessionId)).calls) : null;
85763
+ const overlap = overlapOf(changed, task);
85764
+ results.push({
85765
+ taskId: task.taskId,
85766
+ cardVersion: task.card.version,
85767
+ inputsFingerprint: task.inputsFingerprint,
85768
+ arm,
85769
+ passed: r.passed,
85770
+ attempts: r.attempts,
85771
+ maxAttempts: r.maxAttempts,
85772
+ durationMs: r.durationMs,
85773
+ overlap,
85774
+ haansi
85775
+ });
85776
+ deps.log(
85777
+ `${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${haansi ? `; ${haansiText(haansi)}` : ""})`
85778
+ );
85779
+ } catch (err2) {
85780
+ deps.log(
85781
+ `error ${arm} ${label}: ${err2 instanceof Error ? err2.message : String(err2)}`
85782
+ );
85783
+ }
85784
+ }
85785
+ }
85786
+ if (options.dryRun || results.length === 0)
85787
+ return { results, notValid, recorded: null };
85788
+ let recorded = 0;
85789
+ for (let i = 0; i < results.length; i += REPORT_CHUNK2) {
85790
+ const out = await deps.reportResults({
85791
+ agent: options.agentName,
85792
+ model: options.model,
85793
+ runRef: options.runRef,
85794
+ results: results.slice(i, i + REPORT_CHUNK2)
85795
+ });
85796
+ recorded += out.recorded;
85797
+ if (out.refused.length)
85798
+ deps.log(
85799
+ `Haansi did not record ${out.refused.length} result(s): their task's card or inputs changed during the run.`
85800
+ );
85801
+ }
85802
+ return { results, notValid, recorded };
85803
+ }
85804
+ function haansiUseOf(calls) {
85805
+ return {
85806
+ calls: calls.length,
85807
+ returned: calls.reduce((n, c) => n + c.returnedIds.length, 0)
85808
+ };
85809
+ }
85810
+ function haansiText(use) {
85811
+ return use.calls === 0 ? "did not call Haansi" : `called Haansi ${use.calls}\xD7, ${use.returned} item${use.returned === 1 ? "" : "s"} returned`;
85812
+ }
85813
+ var FEEDBACK_HEADER, REPORT_CHUNK2, HAANSI_LINE, share, pct;
85814
+ var init_replay_tasks = __esm({
85815
+ "src/commands/replay-tasks.ts"() {
85816
+ "use strict";
85817
+ init_replay_calibrate();
85818
+ init_replay();
85819
+ FEEDBACK_HEADER = "Your previous attempt did not pass the tests. Their output ends with:";
85820
+ REPORT_CHUNK2 = 200;
85821
+ HAANSI_LINE = "Haansi's MCP servers are available to you: they give access to your organization's knowledge (solutions to past problems, team skills, digests, guides and Exchange). Use them if they help.";
85822
+ share = (part, whole) => whole === 0 ? null : Math.round(part / whole * 1e3) / 1e3;
85823
+ pct = (v) => v === null ? "n/a" : `${Math.round(v * 100)}%`;
85824
+ }
85825
+ });
85826
+
85529
85827
  // src/commands/replay.ts
85530
85828
  var replay_exports = {};
85531
85829
  __export(replay_exports, {
@@ -85585,8 +85883,8 @@ function parseReplayArgs(argv) {
85585
85883
  else if (!sub && !a.startsWith("-")) sub = a;
85586
85884
  else errors.push(`Unknown option: ${a}`);
85587
85885
  }
85588
- if (!/^[0-9a-f-]{36}$/i.test(options.familyId))
85589
- errors.push("--family <id> is required (the family's id).");
85886
+ if (options.familyId && !/^[0-9a-f-]{36}$/i.test(options.familyId))
85887
+ errors.push("--family must be a family's id.");
85590
85888
  if (!options.agentCmd) errors.push("--agent-cmd is required.");
85591
85889
  if (!options.testCmd && !options.repoCommands)
85592
85890
  errors.push(
@@ -85694,7 +85992,7 @@ async function cleanClone(deps, dir, sha, unreachable) {
85694
85992
  function cleanCheckout(deps, dir, task) {
85695
85993
  return cleanClone(deps, dir, task.baseSha, task.targetSha);
85696
85994
  }
85697
- async function replayArm(deps, options, task, arm, playbook) {
85995
+ async function replayArm(deps, options, task, arm, playbook, promptOf = (feedback) => replayPrompt(task, arm, feedback), inspect, agentEnv = {}) {
85698
85996
  const started = deps.now();
85699
85997
  const dir = (0, import_node_path10.join)(
85700
85998
  deps.workDir(),
@@ -85723,6 +86021,11 @@ async function replayArm(deps, options, task, arm, playbook) {
85723
86021
  deps.log(`\u2026 ${label}: set up in ${since(at)}`);
85724
86022
  }
85725
86023
  const promptFile = (0, import_node_path10.join)(dir, ".haansi-replay", "prompt.md");
86024
+ let mcpConfigFile = agentEnv.HAANSI_REPLAY_MCP_CONFIG;
86025
+ if (!mcpConfigFile) {
86026
+ mcpConfigFile = `${dir}.mcp.json`;
86027
+ deps.writeFile(mcpConfigFile, JSON.stringify({ mcpServers: {} }));
86028
+ }
85726
86029
  if (arm === "haansi" && playbook)
85727
86030
  deps.writeFile(
85728
86031
  (0, import_node_path10.join)(dir, ".haansi-replay", "playbook.md"),
@@ -85730,7 +86033,7 @@ async function replayArm(deps, options, task, arm, playbook) {
85730
86033
  );
85731
86034
  let feedback = null;
85732
86035
  for (let attempt = 1; attempt <= options.attempts; attempt++) {
85733
- const prompt2 = replayPrompt(task, arm, feedback);
86036
+ const prompt2 = promptOf(feedback);
85734
86037
  deps.writeFile(promptFile, prompt2);
85735
86038
  const agentAt = deps.now();
85736
86039
  deps.log(
@@ -85739,7 +86042,12 @@ async function replayArm(deps, options, task, arm, playbook) {
85739
86042
  await deps.shell(
85740
86043
  options.agentCmd,
85741
86044
  dir,
85742
- { HAANSI_REPLAY_PROMPT: prompt2, HAANSI_REPLAY_PROMPT_FILE: promptFile },
86045
+ {
86046
+ ...agentEnv,
86047
+ HAANSI_REPLAY_MCP_CONFIG: mcpConfigFile,
86048
+ HAANSI_REPLAY_PROMPT: prompt2,
86049
+ HAANSI_REPLAY_PROMPT_FILE: promptFile
86050
+ },
85743
86051
  options.timeoutMs
85744
86052
  );
85745
86053
  const own = new Map(files.map((f) => [f, deps.readFile((0, import_node_path10.join)(dir, f))]));
@@ -85764,6 +86072,7 @@ async function replayArm(deps, options, task, arm, playbook) {
85764
86072
  deps.log(` ${line}`);
85765
86073
  }
85766
86074
  if (test.code === 0) {
86075
+ if (inspect) await inspect(dir);
85767
86076
  return {
85768
86077
  processId: task.processId,
85769
86078
  arm,
@@ -85780,6 +86089,7 @@ async function replayArm(deps, options, task, arm, playbook) {
85780
86089
  }
85781
86090
  feedback = test.output.slice(-FEEDBACK_CHARS);
85782
86091
  }
86092
+ if (inspect) await inspect(dir);
85783
86093
  return {
85784
86094
  processId: task.processId,
85785
86095
  arm,
@@ -85791,6 +86101,7 @@ async function replayArm(deps, options, task, arm, playbook) {
85791
86101
  };
85792
86102
  } finally {
85793
86103
  deps.removeDir(dir);
86104
+ if (!agentEnv.HAANSI_REPLAY_MCP_CONFIG) deps.removeFile(`${dir}.mcp.json`);
85794
86105
  }
85795
86106
  }
85796
86107
  async function runReplays(options, deps) {
@@ -85949,6 +86260,10 @@ async function replay(argv) {
85949
86260
  }
85950
86261
  const token = resolveToken5();
85951
86262
  const apiUrl2 = apiUrl();
86263
+ if (!options.familyId) {
86264
+ await replayStored(options, token, apiUrl2);
86265
+ return;
86266
+ }
85952
86267
  const work = (0, import_node_fs11.mkdtempSync)((0, import_node_path10.join)((0, import_node_os10.tmpdir)(), "haansi-replay-"));
85953
86268
  try {
85954
86269
  const { results, skipped, recorded } = await runReplays(options, {
@@ -85983,6 +86298,64 @@ Baseline: ${passed("baseline")}/${ran("baseline")} passed \xB7 With Haansi: ${pa
85983
86298
  (0, import_node_fs11.rmSync)(work, { recursive: true, force: true });
85984
86299
  }
85985
86300
  }
86301
+ async function replayStored(options, token, apiUrl2) {
86302
+ const { runStoredReplays: runStoredReplays2 } = await Promise.resolve().then(() => (init_replay_tasks(), replay_tasks_exports));
86303
+ const { cliVersion: cliVersion2 } = await Promise.resolve().then(() => (init_replay_calibrate(), replay_calibrate_exports));
86304
+ const work = (0, import_node_fs11.mkdtempSync)((0, import_node_path10.join)((0, import_node_os10.tmpdir)(), "haansi-replay-"));
86305
+ try {
86306
+ const { results, notValid, recorded } = await runStoredReplays2(options, {
86307
+ ...localDeps(work),
86308
+ getReplaySet: (repository) => apiGet3(
86309
+ `/memory/replays/tasks?repository=${encodeURIComponent(repository)}`,
86310
+ token,
86311
+ apiUrl2
86312
+ ),
86313
+ reportCalibration: async (body) => await apiPost2(
86314
+ "/memory/replays/tasks/calibration",
86315
+ body,
86316
+ token,
86317
+ apiUrl2
86318
+ ),
86319
+ reportResults: async (body) => await apiPost2(
86320
+ "/memory/replays/tasks/results",
86321
+ body,
86322
+ token,
86323
+ apiUrl2
86324
+ ),
86325
+ env: process.env,
86326
+ cliVersion: cliVersion2(),
86327
+ openSession: async (taskId) => await apiPost2(
86328
+ `/memory/replays/tasks/${encodeURIComponent(taskId)}/sessions`,
86329
+ {},
86330
+ token,
86331
+ apiUrl2
86332
+ ),
86333
+ getCalls: (sessionId) => apiGet3(
86334
+ `/memory/replays/sessions/${encodeURIComponent(sessionId)}/calls`,
86335
+ token,
86336
+ apiUrl2
86337
+ ),
86338
+ apiUrl: apiUrl2,
86339
+ token
86340
+ });
86341
+ const arm = (a) => {
86342
+ const mine = results.filter((r) => r.arm === a);
86343
+ return `${mine.filter((r) => r.passed).length}/${mine.length}`;
86344
+ };
86345
+ console.log(
86346
+ `
86347
+ Baseline: ${arm("baseline")} passed \xB7 With Haansi: ${arm("haansi")} passed \xB7 ${notValid} task(s) not valid to replay`
86348
+ );
86349
+ console.log(
86350
+ recorded === null ? options.dryRun ? "Dry run: nothing reported." : "Nothing to report." : `Reported ${recorded} result(s) to Haansi.`
86351
+ );
86352
+ } catch (err2) {
86353
+ console.error(err2 instanceof Error ? err2.message : String(err2));
86354
+ process.exitCode = 1;
86355
+ } finally {
86356
+ (0, import_node_fs11.rmSync)(work, { recursive: true, force: true });
86357
+ }
86358
+ }
85986
86359
  var import_node_child_process4, import_node_fs11, import_node_os10, import_node_path10, SHA, ARMS, FEEDBACK_CHARS, FAILURE_LINES2, REPLAY_BRANCH, USAGE2;
85987
86360
  var init_replay = __esm({
85988
86361
  "src/commands/replay.ts"() {
@@ -85998,7 +86371,7 @@ var init_replay = __esm({
85998
86371
  FEEDBACK_CHARS = 6e3;
85999
86372
  FAILURE_LINES2 = 8;
86000
86373
  REPLAY_BRANCH = "replay";
86001
- USAGE2 = `Usage: haansi replay run --family <id> --agent-cmd "<cmd>" --test-cmd "<cmd>"
86374
+ USAGE2 = `Usage: haansi replay run [--family <id>] --agent-cmd "<cmd>" --test-cmd "<cmd>"
86002
86375
  [--setup-cmd "<cmd>"] [--attempts 3] [--arms baseline,haansi] [--limit n]
86003
86376
  [--cutoff <date>] [--timeout-min 30]
86004
86377
  [--agent-name <name>] [--model <name>] [--run-ref <url>] [--dry-run]
@@ -86006,12 +86379,17 @@ var init_replay = __esm({
86006
86379
 
86007
86380
  haansi replay calibrate --test-cmd "<cmd>" [--setup-cmd "<cmd>"] (see --help)
86008
86381
 
86382
+ Without --family, runs this repository's stored replay tasks: calibrates them
86383
+ first, then replays the valid ones with their task card as the prompt, graded
86384
+ by the target's tests and where the change landed. With --family, replays that
86385
+ problem family's tasks with and without its playbook.
86386
+
86009
86387
  --repo-commands uses the setup and test commands an org admin set for the
86010
86388
  repository in Haansi (Settings \u2192 Data sources) for any not given here.
86011
86389
 
86012
86390
  Run from a clone of the repository. Example (GitHub Actions):
86013
86391
  haansi replay run --family "$FAMILY_ID" --setup-cmd 'npm ci' \\
86014
- --agent-cmd 'claude -p "$HAANSI_REPLAY_PROMPT" --permission-mode acceptEdits' \\
86392
+ --agent-cmd 'claude -p "$HAANSI_REPLAY_PROMPT" --permission-mode acceptEdits --mcp-config "$HAANSI_REPLAY_MCP_CONFIG"' \\
86015
86393
  --test-cmd 'npx vitest run $HAANSI_REPLAY_TEST_FILES' \\
86016
86394
  --agent-name claude-code --model claude-sonnet-5-5`;
86017
86395
  }
@@ -86231,9 +86609,10 @@ Commands:
86231
86609
  digest. [--date YYYY-MM-DD] [--workstream <id>]
86232
86610
  digest [--week|--month|--since YYYY-MM-DD]
86233
86611
  Your weekly digest (default: the latest week)
86234
- replay run --family <id> --agent-cmd "<cmd>" --test-cmd "<cmd>"
86235
- Replay a problem family's tasks in your CI, with and
86236
- without its playbook, and report the results
86612
+ replay run --agent-cmd "<cmd>" --test-cmd "<cmd>" [--family <id>]
86613
+ Replay this repository's tasks in your CI (or a
86614
+ problem family's, with and without its playbook)
86615
+ and report the results
86237
86616
  replay calibrate --test-cmd "<cmd>"
86238
86617
  Check this repository's replay tasks are valid: their
86239
86618
  tests fail at the start and pass at the fix
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haansi",
3
- "version": "0.1.55",
3
+ "version": "0.1.57",
4
4
  "description": "Haansi CLI - Session collector and MCP server for Claude Code & OpenAI Codex",
5
5
  "bin": {
6
6
  "haansi": "./dist/haansi.js"