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