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.
- package/dist/haansi.js +405 -26
- 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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
{
|
|
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 --
|
|
86235
|
-
Replay
|
|
86236
|
-
|
|
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
|