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