offhands 0.1.17 → 0.1.19
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/daemon.mjs +533 -43
- package/package.json +2 -2
package/dist/daemon.mjs
CHANGED
|
@@ -4776,8 +4776,8 @@ var require_main = __commonJS({
|
|
|
4776
4776
|
});
|
|
4777
4777
|
|
|
4778
4778
|
// ../daemon/src/index.ts
|
|
4779
|
-
import { resolve as
|
|
4780
|
-
import { existsSync as
|
|
4779
|
+
import { resolve as resolve6 } from "node:path";
|
|
4780
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
4781
4781
|
|
|
4782
4782
|
// ../daemon/src/runners/claude-code.ts
|
|
4783
4783
|
import { spawn } from "node:child_process";
|
|
@@ -4951,7 +4951,7 @@ var AsyncEventQueue = class {
|
|
|
4951
4951
|
for (; ; ) {
|
|
4952
4952
|
while (this.buffer.length > 0) yield this.buffer.shift();
|
|
4953
4953
|
if (this.closed) return;
|
|
4954
|
-
await new Promise((
|
|
4954
|
+
await new Promise((resolve7) => this.waiter = resolve7);
|
|
4955
4955
|
this.waiter = null;
|
|
4956
4956
|
}
|
|
4957
4957
|
}
|
|
@@ -5038,10 +5038,10 @@ var ClaudeCodeRunner = class {
|
|
|
5038
5038
|
return "claude";
|
|
5039
5039
|
}
|
|
5040
5040
|
async detect() {
|
|
5041
|
-
return new Promise((
|
|
5041
|
+
return new Promise((resolve7) => {
|
|
5042
5042
|
const p2 = spawn(this.resolveBin(), ["--version"], { stdio: "ignore" });
|
|
5043
|
-
p2.on("error", () =>
|
|
5044
|
-
p2.on("exit", (code) =>
|
|
5043
|
+
p2.on("error", () => resolve7(false));
|
|
5044
|
+
p2.on("exit", (code) => resolve7(code === 0));
|
|
5045
5045
|
});
|
|
5046
5046
|
}
|
|
5047
5047
|
start(run, callbacks) {
|
|
@@ -5132,10 +5132,10 @@ function extractSessionId(value) {
|
|
|
5132
5132
|
// ../daemon/src/runners/stubs.ts
|
|
5133
5133
|
import { spawn as spawn2 } from "node:child_process";
|
|
5134
5134
|
function commandExists(bin) {
|
|
5135
|
-
return new Promise((
|
|
5135
|
+
return new Promise((resolve7) => {
|
|
5136
5136
|
const p2 = spawn2(bin, ["--version"], { stdio: "ignore", shell: process.platform === "win32" });
|
|
5137
|
-
p2.on("error", () =>
|
|
5138
|
-
p2.on("exit", (code) =>
|
|
5137
|
+
p2.on("error", () => resolve7(false));
|
|
5138
|
+
p2.on("exit", (code) => resolve7(code === 0));
|
|
5139
5139
|
});
|
|
5140
5140
|
}
|
|
5141
5141
|
function notImplemented(id) {
|
|
@@ -5276,10 +5276,10 @@ var CopilotCliRunner = class {
|
|
|
5276
5276
|
async detect() {
|
|
5277
5277
|
const cmd = this.resolveCommand();
|
|
5278
5278
|
if (!cmd) return false;
|
|
5279
|
-
return new Promise((
|
|
5279
|
+
return new Promise((resolve7) => {
|
|
5280
5280
|
const p2 = spawn3(cmd[0], [...cmd.slice(1), "--version"], { stdio: "ignore" });
|
|
5281
|
-
p2.on("error", () =>
|
|
5282
|
-
p2.on("exit", (code) =>
|
|
5281
|
+
p2.on("error", () => resolve7(false));
|
|
5282
|
+
p2.on("exit", (code) => resolve7(code === 0));
|
|
5283
5283
|
});
|
|
5284
5284
|
}
|
|
5285
5285
|
loggedIn() {
|
|
@@ -5562,17 +5562,17 @@ var OpenCodeRunner = class {
|
|
|
5562
5562
|
const cmd = this.resolveCommand();
|
|
5563
5563
|
if (!cmd) return false;
|
|
5564
5564
|
await this.fetchModels();
|
|
5565
|
-
return new Promise((
|
|
5565
|
+
return new Promise((resolve7) => {
|
|
5566
5566
|
const p2 = spawn4(cmd[0], [...cmd.slice(1), "--version"], { stdio: "ignore", shell: process.platform === "win32" });
|
|
5567
|
-
p2.on("error", () =>
|
|
5568
|
-
p2.on("exit", (code) =>
|
|
5567
|
+
p2.on("error", () => resolve7(false));
|
|
5568
|
+
p2.on("exit", (code) => resolve7(code === 0));
|
|
5569
5569
|
});
|
|
5570
5570
|
}
|
|
5571
5571
|
async fetchModels() {
|
|
5572
5572
|
if (this.modelsCache.length > 0) return this.modelsCache;
|
|
5573
5573
|
const cmd = this.resolveCommand();
|
|
5574
5574
|
if (!cmd) return [];
|
|
5575
|
-
return new Promise((
|
|
5575
|
+
return new Promise((resolve7) => {
|
|
5576
5576
|
const p2 = spawn4(cmd[0], [...cmd.slice(1), "models"], {
|
|
5577
5577
|
stdio: ["ignore", "pipe", "ignore"],
|
|
5578
5578
|
shell: process.platform === "win32"
|
|
@@ -5593,15 +5593,15 @@ var OpenCodeRunner = class {
|
|
|
5593
5593
|
writable: false,
|
|
5594
5594
|
configurable: true
|
|
5595
5595
|
});
|
|
5596
|
-
|
|
5596
|
+
resolve7(models);
|
|
5597
5597
|
} catch {
|
|
5598
|
-
|
|
5598
|
+
resolve7([]);
|
|
5599
5599
|
}
|
|
5600
5600
|
} else {
|
|
5601
|
-
|
|
5601
|
+
resolve7([]);
|
|
5602
5602
|
}
|
|
5603
5603
|
});
|
|
5604
|
-
p2.on("error", () =>
|
|
5604
|
+
p2.on("error", () => resolve7([]));
|
|
5605
5605
|
});
|
|
5606
5606
|
}
|
|
5607
5607
|
loggedIn() {
|
|
@@ -5882,8 +5882,8 @@ stderr: ${stderrTail}` : ""}`
|
|
|
5882
5882
|
preview: truncate4(preview, 600),
|
|
5883
5883
|
question: { text: first.question, options, multiSelect: first.multiple }
|
|
5884
5884
|
}]);
|
|
5885
|
-
const verdict = await new Promise((
|
|
5886
|
-
pendingQuestions.set(q2.id, (ok, answer) =>
|
|
5885
|
+
const verdict = await new Promise((resolve7) => {
|
|
5886
|
+
pendingQuestions.set(q2.id, (ok, answer) => resolve7({ ok, answer }));
|
|
5887
5887
|
});
|
|
5888
5888
|
emit([{
|
|
5889
5889
|
type: "approval-result",
|
|
@@ -5996,14 +5996,14 @@ async function httpJson(url, method, body, expectNoContent = false) {
|
|
|
5996
5996
|
return res.json();
|
|
5997
5997
|
}
|
|
5998
5998
|
function findFreePort() {
|
|
5999
|
-
return new Promise((
|
|
5999
|
+
return new Promise((resolve7, reject) => {
|
|
6000
6000
|
const srv = createServer();
|
|
6001
6001
|
srv.unref();
|
|
6002
6002
|
srv.on("error", reject);
|
|
6003
6003
|
srv.listen(0, "127.0.0.1", () => {
|
|
6004
6004
|
const address = srv.address();
|
|
6005
6005
|
const port2 = typeof address === "object" && address ? address.port : 0;
|
|
6006
|
-
srv.close(() =>
|
|
6006
|
+
srv.close(() => resolve7(port2));
|
|
6007
6007
|
});
|
|
6008
6008
|
});
|
|
6009
6009
|
}
|
|
@@ -6020,7 +6020,7 @@ async function waitForServerReady(port2, timeoutMs = 2e4) {
|
|
|
6020
6020
|
throw new Error(`opencode serve did not become ready on port ${port2}`);
|
|
6021
6021
|
}
|
|
6022
6022
|
function sleep(ms) {
|
|
6023
|
-
return new Promise((
|
|
6023
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
6024
6024
|
}
|
|
6025
6025
|
|
|
6026
6026
|
// ../daemon/src/session-manager.ts
|
|
@@ -6905,7 +6905,7 @@ async function buildContext(workspace, prompt, opts = {}) {
|
|
|
6905
6905
|
const startedAt = Date.now();
|
|
6906
6906
|
const maxChars = opts.maxChars ?? (Number(process.env.OFFHAND_INTEL_MAX_CHARS) || DEFAULT_MAX_CHARS);
|
|
6907
6907
|
const work = gatherFindings(workspace, prompt);
|
|
6908
|
-
const timeout = new Promise((
|
|
6908
|
+
const timeout = new Promise((resolve7) => setTimeout(() => resolve7("timeout"), OVERALL_TIMEOUT_MS));
|
|
6909
6909
|
const result = await Promise.race([work, timeout]);
|
|
6910
6910
|
const durationMs = Date.now() - startedAt;
|
|
6911
6911
|
if (result === "timeout") {
|
|
@@ -12920,7 +12920,7 @@ var randomValuesStandard;
|
|
|
12920
12920
|
var crypto;
|
|
12921
12921
|
var randomValueNodeJS;
|
|
12922
12922
|
var _Module = Module;
|
|
12923
|
-
Module.ready = new Promise(function(
|
|
12923
|
+
Module.ready = new Promise(function(resolve7, reject) {
|
|
12924
12924
|
var Module2 = _Module;
|
|
12925
12925
|
Module2.onAbort = reject;
|
|
12926
12926
|
Module2.print = function(what) {
|
|
@@ -12932,13 +12932,13 @@ Module.ready = new Promise(function(resolve6, reject) {
|
|
|
12932
12932
|
Module2.onRuntimeInitialized = function() {
|
|
12933
12933
|
try {
|
|
12934
12934
|
Module2._crypto_secretbox_keybytes();
|
|
12935
|
-
|
|
12935
|
+
resolve7();
|
|
12936
12936
|
} catch (err2) {
|
|
12937
12937
|
reject(err2);
|
|
12938
12938
|
}
|
|
12939
12939
|
};
|
|
12940
12940
|
Module2.useBackupModule = function() {
|
|
12941
|
-
return new Promise(function(
|
|
12941
|
+
return new Promise(function(resolve8, reject2) {
|
|
12942
12942
|
var Module3 = {};
|
|
12943
12943
|
Module3.onAbort = reject2;
|
|
12944
12944
|
Module3.getRandomValue = _Module.getRandomValue;
|
|
@@ -12951,7 +12951,7 @@ Module.ready = new Promise(function(resolve6, reject) {
|
|
|
12951
12951
|
Object.keys(Module3).forEach(function(k2) {
|
|
12952
12952
|
_Module[k2] = Module3[k2];
|
|
12953
12953
|
});
|
|
12954
|
-
|
|
12954
|
+
resolve8();
|
|
12955
12955
|
};
|
|
12956
12956
|
var Module3 = typeof Module3 != "undefined" ? Module3 : {};
|
|
12957
12957
|
var ENVIRONMENT_IS_WEB2 = !!globalThis.window;
|
|
@@ -13011,13 +13011,13 @@ Module.ready = new Promise(function(resolve6, reject) {
|
|
|
13011
13011
|
}
|
|
13012
13012
|
readAsync2 = async (url) => {
|
|
13013
13013
|
if (isFileURI2(url)) {
|
|
13014
|
-
return new Promise((
|
|
13014
|
+
return new Promise((resolve9, reject3) => {
|
|
13015
13015
|
var xhr = new XMLHttpRequest();
|
|
13016
13016
|
xhr.open("GET", url, true);
|
|
13017
13017
|
xhr.responseType = "arraybuffer";
|
|
13018
13018
|
xhr.onload = () => {
|
|
13019
13019
|
if (xhr.status == 200 || xhr.status == 0 && xhr.response) {
|
|
13020
|
-
|
|
13020
|
+
resolve9(xhr.response);
|
|
13021
13021
|
return;
|
|
13022
13022
|
}
|
|
13023
13023
|
reject3(xhr.status);
|
|
@@ -39629,9 +39629,9 @@ Module.ready = new Promise(function(resolve6, reject) {
|
|
|
39629
39629
|
}
|
|
39630
39630
|
var info = getWasmImports2();
|
|
39631
39631
|
if (Module3["instantiateWasm"]) {
|
|
39632
|
-
return new Promise((
|
|
39632
|
+
return new Promise((resolve9, reject3) => {
|
|
39633
39633
|
Module3["instantiateWasm"](info, (inst, mod) => {
|
|
39634
|
-
|
|
39634
|
+
resolve9(receiveInstance(inst, mod));
|
|
39635
39635
|
});
|
|
39636
39636
|
});
|
|
39637
39637
|
}
|
|
@@ -40132,13 +40132,13 @@ Module.ready = new Promise(function(resolve6, reject) {
|
|
|
40132
40132
|
}
|
|
40133
40133
|
readAsync = async (url) => {
|
|
40134
40134
|
if (isFileURI(url)) {
|
|
40135
|
-
return new Promise((
|
|
40135
|
+
return new Promise((resolve8, reject2) => {
|
|
40136
40136
|
var xhr = new XMLHttpRequest();
|
|
40137
40137
|
xhr.open("GET", url, true);
|
|
40138
40138
|
xhr.responseType = "arraybuffer";
|
|
40139
40139
|
xhr.onload = () => {
|
|
40140
40140
|
if (xhr.status == 200 || xhr.status == 0 && xhr.response) {
|
|
40141
|
-
|
|
40141
|
+
resolve8(xhr.response);
|
|
40142
40142
|
return;
|
|
40143
40143
|
}
|
|
40144
40144
|
reject2(xhr.status);
|
|
@@ -40252,9 +40252,9 @@ Module.ready = new Promise(function(resolve6, reject) {
|
|
|
40252
40252
|
}
|
|
40253
40253
|
var info = getWasmImports();
|
|
40254
40254
|
if (Module2["instantiateWasm"]) {
|
|
40255
|
-
return new Promise((
|
|
40255
|
+
return new Promise((resolve8, reject2) => {
|
|
40256
40256
|
Module2["instantiateWasm"](info, (inst, mod) => {
|
|
40257
|
-
|
|
40257
|
+
resolve8(receiveInstance(inst, mod));
|
|
40258
40258
|
});
|
|
40259
40259
|
});
|
|
40260
40260
|
}
|
|
@@ -44200,6 +44200,493 @@ function intelStatsCli(args2, out = console.log) {
|
|
|
44200
44200
|
return 0;
|
|
44201
44201
|
}
|
|
44202
44202
|
|
|
44203
|
+
// ../daemon/src/intelligence/ab.ts
|
|
44204
|
+
import { mkdtempSync, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as existsSync12 } from "node:fs";
|
|
44205
|
+
import { createServer as createServer3 } from "node:net";
|
|
44206
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
44207
|
+
import { join as join13, resolve as resolve5 } from "node:path";
|
|
44208
|
+
var MIN_PAIRS_FOR_CLAIM = 10;
|
|
44209
|
+
var USAGE_GRACE_MS = 2e3;
|
|
44210
|
+
var median2 = (xs) => {
|
|
44211
|
+
if (xs.length === 0) return null;
|
|
44212
|
+
const s2 = [...xs].sort((a2, b2) => a2 - b2);
|
|
44213
|
+
const mid = Math.floor(s2.length / 2);
|
|
44214
|
+
return s2.length % 2 ? s2[mid] : (s2[mid - 1] + s2[mid]) / 2;
|
|
44215
|
+
};
|
|
44216
|
+
var mean = (xs) => xs.length ? xs.reduce((a2, b2) => a2 + b2, 0) / xs.length : null;
|
|
44217
|
+
function signTestP(a2, b2) {
|
|
44218
|
+
const n3 = a2 + b2;
|
|
44219
|
+
if (n3 === 0) return 1;
|
|
44220
|
+
const k2 = Math.min(a2, b2);
|
|
44221
|
+
let cumulative = 0;
|
|
44222
|
+
let choose = 1;
|
|
44223
|
+
for (let i2 = 0; i2 <= k2; i2++) {
|
|
44224
|
+
cumulative += choose;
|
|
44225
|
+
choose = choose * (n3 - i2) / (i2 + 1);
|
|
44226
|
+
}
|
|
44227
|
+
return Math.min(1, 2 * cumulative / 2 ** n3);
|
|
44228
|
+
}
|
|
44229
|
+
function signedRankP(diffs) {
|
|
44230
|
+
const d2 = diffs.filter((x2) => x2 !== 0);
|
|
44231
|
+
const n3 = d2.length;
|
|
44232
|
+
if (n3 === 0) return null;
|
|
44233
|
+
const order = d2.map((x2, i2) => ({ a: Math.abs(x2), i: i2 })).sort((p2, q2) => p2.a - q2.a);
|
|
44234
|
+
const rank2 = new Array(n3);
|
|
44235
|
+
for (let lo = 0; lo < n3; ) {
|
|
44236
|
+
let hi = lo;
|
|
44237
|
+
while (hi + 1 < n3 && order[hi + 1].a === order[lo].a) hi++;
|
|
44238
|
+
const avgRank2 = lo + 1 + (hi + 1);
|
|
44239
|
+
for (let k2 = lo; k2 <= hi; k2++) rank2[order[k2].i] = avgRank2;
|
|
44240
|
+
lo = hi + 1;
|
|
44241
|
+
}
|
|
44242
|
+
const total2 = rank2.reduce((s2, r2) => s2 + r2, 0);
|
|
44243
|
+
const observed2 = d2.reduce((s2, x2, i2) => s2 + (x2 > 0 ? rank2[i2] : 0), 0);
|
|
44244
|
+
let counts = new Array(total2 + 1).fill(0);
|
|
44245
|
+
counts[0] = 1;
|
|
44246
|
+
for (const r2 of rank2) {
|
|
44247
|
+
const next = counts.slice();
|
|
44248
|
+
for (let s2 = 0; s2 + r2 <= total2; s2++) if (counts[s2]) next[s2 + r2] += counts[s2];
|
|
44249
|
+
counts = next;
|
|
44250
|
+
}
|
|
44251
|
+
const dev = Math.abs(2 * observed2 - total2);
|
|
44252
|
+
let extreme = 0;
|
|
44253
|
+
for (let s2 = 0; s2 <= total2; s2++) if (Math.abs(2 * s2 - total2) >= dev) extreme += counts[s2];
|
|
44254
|
+
return Math.min(1, extreme / 2 ** n3);
|
|
44255
|
+
}
|
|
44256
|
+
function recallOf(answer, expect) {
|
|
44257
|
+
if (!expect || expect.length === 0) return null;
|
|
44258
|
+
const hay = answer.toLowerCase().replace(/\\/g, "/");
|
|
44259
|
+
const hits = expect.filter((e) => hay.includes(e.toLowerCase().replace(/\\/g, "/"))).length;
|
|
44260
|
+
return hits / expect.length;
|
|
44261
|
+
}
|
|
44262
|
+
function summarize(name, pairs, pick) {
|
|
44263
|
+
const both = pairs.map((p2) => ({ c: pick(p2.context), q: pick(p2.plain) })).filter((x2) => x2.c !== null && x2.q !== null);
|
|
44264
|
+
const contextLower = both.filter((x2) => x2.c < x2.q).length;
|
|
44265
|
+
const plainLower = both.filter((x2) => x2.c > x2.q).length;
|
|
44266
|
+
return {
|
|
44267
|
+
name,
|
|
44268
|
+
n: both.length,
|
|
44269
|
+
medianContext: median2(both.map((x2) => x2.c)),
|
|
44270
|
+
medianPlain: median2(both.map((x2) => x2.q)),
|
|
44271
|
+
medianDiff: median2(both.map((x2) => x2.c - x2.q)),
|
|
44272
|
+
meanDiff: mean(both.map((x2) => x2.c - x2.q)),
|
|
44273
|
+
contextLower,
|
|
44274
|
+
plainLower,
|
|
44275
|
+
ties: both.length - contextLower - plainLower,
|
|
44276
|
+
p: both.length ? signTestP(contextLower, plainLower) : null,
|
|
44277
|
+
pSignedRank: signedRankP(both.map((x2) => x2.c - x2.q))
|
|
44278
|
+
};
|
|
44279
|
+
}
|
|
44280
|
+
function analyzePairs(pairs, prompts) {
|
|
44281
|
+
const contextNotAttached = pairs.filter((p2) => p2.context.ok && !p2.context.contextAttached).length;
|
|
44282
|
+
const errored = pairs.filter((p2) => !p2.context.ok && !p2.context.timedOut || !p2.plain.ok && !p2.plain.timedOut).length;
|
|
44283
|
+
const usable = pairs.filter(
|
|
44284
|
+
(p2) => (p2.context.ok || p2.context.timedOut) && (p2.plain.ok || p2.plain.timedOut) && p2.context.contextAttached
|
|
44285
|
+
);
|
|
44286
|
+
const timedOutRuns = usable.reduce((n3, p2) => n3 + (p2.context.timedOut ? 1 : 0) + (p2.plain.timedOut ? 1 : 0), 0);
|
|
44287
|
+
const errorCounts = /* @__PURE__ */ new Map();
|
|
44288
|
+
for (const p2 of pairs) {
|
|
44289
|
+
for (const r2 of [p2.context, p2.plain]) {
|
|
44290
|
+
if (r2.error) errorCounts.set(r2.error, (errorCounts.get(r2.error) ?? 0) + 1);
|
|
44291
|
+
}
|
|
44292
|
+
}
|
|
44293
|
+
const errorMessages = [...errorCounts].map(([message, runs]) => ({ message, runs })).sort((a2, b2) => b2.runs - a2.runs);
|
|
44294
|
+
const metrics = [
|
|
44295
|
+
summarize("Tool calls", usable, (r2) => r2.toolCalls),
|
|
44296
|
+
summarize("Duration (s)", usable, (r2) => r2.durationMs / 1e3),
|
|
44297
|
+
summarize("Cost (USD)", usable, (r2) => r2.costUsd),
|
|
44298
|
+
summarize("Context tokens", usable, (r2) => r2.contextTokens)
|
|
44299
|
+
];
|
|
44300
|
+
const rec = usable.map((p2) => {
|
|
44301
|
+
const expect = prompts[p2.promptIndex]?.expect;
|
|
44302
|
+
return { c: recallOf(p2.context.answer, expect), q: recallOf(p2.plain.answer, expect) };
|
|
44303
|
+
}).filter((x2) => x2.c !== null && x2.q !== null);
|
|
44304
|
+
const contextHigher = rec.filter((x2) => x2.c > x2.q).length;
|
|
44305
|
+
const plainHigher = rec.filter((x2) => x2.c < x2.q).length;
|
|
44306
|
+
const quality = {
|
|
44307
|
+
pairsWithGroundTruth: rec.length,
|
|
44308
|
+
meanRecallContext: mean(rec.map((x2) => x2.c)),
|
|
44309
|
+
meanRecallPlain: mean(rec.map((x2) => x2.q)),
|
|
44310
|
+
contextHigher,
|
|
44311
|
+
plainHigher,
|
|
44312
|
+
ties: rec.length - contextHigher - plainHigher,
|
|
44313
|
+
p: rec.length ? signTestP(contextHigher, plainHigher) : null
|
|
44314
|
+
};
|
|
44315
|
+
const conclusions = [];
|
|
44316
|
+
const enough = usable.length >= MIN_PAIRS_FOR_CLAIM;
|
|
44317
|
+
for (const m3 of metrics) {
|
|
44318
|
+
if (m3.n === 0) continue;
|
|
44319
|
+
const dir = (m3.meanDiff ?? 0) < 0 ? "lower" : "higher";
|
|
44320
|
+
const ps = `signed-rank p=${m3.pSignedRank === null ? "\u2014" : m3.pSignedRank.toFixed(3)}, sign-test p=${m3.p === null ? "\u2014" : m3.p.toFixed(3)}`;
|
|
44321
|
+
if (enough && m3.n >= MIN_PAIRS_FOR_CLAIM && m3.pSignedRank !== null && m3.pSignedRank < 0.05) {
|
|
44322
|
+
conclusions.push(`${m3.name}: ${dir} with context (mean difference ${(m3.meanDiff ?? 0).toFixed(2)}; ${ps}; ${m3.contextLower} lower / ${m3.plainLower} higher / ${m3.ties} tied) \u2014 a measurable difference in this sample.`);
|
|
44323
|
+
} else {
|
|
44324
|
+
conclusions.push(`${m3.name}: no reliable difference detected (${m3.contextLower} lower with context, ${m3.plainLower} lower without, ${m3.ties} tied; ${ps}).`);
|
|
44325
|
+
}
|
|
44326
|
+
}
|
|
44327
|
+
if (usable.length === 0 && errorMessages.length > 0) {
|
|
44328
|
+
conclusions.unshift(`No run completed, so nothing can be concluded. Most common error: ${errorMessages[0].message}`);
|
|
44329
|
+
}
|
|
44330
|
+
if (!enough) {
|
|
44331
|
+
conclusions.push(`Only ${usable.length} usable pair(s); at least ${MIN_PAIRS_FOR_CLAIM} are needed before any difference is reported as measurable.`);
|
|
44332
|
+
}
|
|
44333
|
+
if (quality.meanRecallContext !== null && quality.meanRecallPlain !== null && quality.meanRecallContext < quality.meanRecallPlain - 0.1) {
|
|
44334
|
+
conclusions.push("Warning: answers WITH context were less complete against the expected files \u2014 any saving is not a win if the answer is worse.");
|
|
44335
|
+
}
|
|
44336
|
+
return {
|
|
44337
|
+
pairsTotal: pairs.length,
|
|
44338
|
+
pairsAnalysed: usable.length,
|
|
44339
|
+
excluded: { contextNotAttached, errored },
|
|
44340
|
+
errorMessages,
|
|
44341
|
+
timedOutRuns,
|
|
44342
|
+
metrics,
|
|
44343
|
+
quality,
|
|
44344
|
+
conclusions
|
|
44345
|
+
};
|
|
44346
|
+
}
|
|
44347
|
+
var fmt = (x2, digits = 1) => x2 === null ? "\u2014" : x2.toFixed(digits);
|
|
44348
|
+
function formatAbReport(r2, meta) {
|
|
44349
|
+
const L2 = [];
|
|
44350
|
+
L2.push("Offhand Intelligence \u2014 paired A/B measurement");
|
|
44351
|
+
L2.push("");
|
|
44352
|
+
L2.push(`Workspace: ${meta.workspace}`);
|
|
44353
|
+
L2.push(`Runner: ${meta.runner}${meta.model ? ` (${meta.model})` : ""} \xB7 ${meta.prompts} prompt(s) \xD7 ${meta.repeats} repeat(s) = ${r2.pairsTotal} pair(s)`);
|
|
44354
|
+
L2.push(
|
|
44355
|
+
`Analysed: ${r2.pairsAnalysed} of ${r2.pairsTotal} pair(s)` + (r2.excluded.contextNotAttached ? ` \xB7 ${r2.excluded.contextNotAttached} excluded (retrieval found nothing to attach)` : "") + (r2.excluded.errored ? ` \xB7 ${r2.excluded.errored} excluded (a run errored)` : "")
|
|
44356
|
+
);
|
|
44357
|
+
if (r2.timedOutRuns) L2.push(`Warning: ${r2.timedOutRuns} run(s) hit the time limit \u2014 their numbers are lower bounds.`);
|
|
44358
|
+
if (meta.pairsPlanned !== void 0 && r2.pairsTotal < meta.pairsPlanned) {
|
|
44359
|
+
L2.push(`Stopped early: ran ${r2.pairsTotal} of ${meta.pairsPlanned} planned pair(s), because consecutive pairs failed entirely.`);
|
|
44360
|
+
}
|
|
44361
|
+
if (r2.errorMessages.length) {
|
|
44362
|
+
L2.push("Errors:");
|
|
44363
|
+
for (const m3 of r2.errorMessages.slice(0, 3)) L2.push(` ${m3.runs} run(s): ${m3.message}`);
|
|
44364
|
+
}
|
|
44365
|
+
L2.push("");
|
|
44366
|
+
L2.push("Metric with ctx plain median diff mean diff ctx lower / plain lower / tie signed-rank p sign-test p");
|
|
44367
|
+
for (const m3 of r2.metrics) {
|
|
44368
|
+
const digits = m3.name.startsWith("Cost") ? 4 : m3.name.startsWith("Duration") ? 0 : 1;
|
|
44369
|
+
L2.push(
|
|
44370
|
+
`${m3.name.padEnd(19)} ${fmt(m3.medianContext, digits).padEnd(10)} ${fmt(m3.medianPlain, digits).padEnd(9)} ${fmt(m3.medianDiff, digits).padEnd(12)} ${fmt(m3.meanDiff, digits).padEnd(11)} ${`${m3.contextLower} / ${m3.plainLower} / ${m3.ties}`.padEnd(30)} ${(m3.pSignedRank === null ? "\u2014" : m3.pSignedRank.toFixed(3)).padEnd(15)} ${m3.p === null ? "\u2014" : m3.p.toFixed(3)} (n=${m3.n})`
|
|
44371
|
+
);
|
|
44372
|
+
}
|
|
44373
|
+
const q2 = r2.quality;
|
|
44374
|
+
if (q2.pairsWithGroundTruth > 0) {
|
|
44375
|
+
L2.push(
|
|
44376
|
+
`${"Answer recall".padEnd(19)} ${fmt(q2.meanRecallContext, 2).padEnd(10)} ${fmt(q2.meanRecallPlain, 2).padEnd(9)} ${"".padEnd(12)} ${"".padEnd(11)} ${`${q2.contextHigher} / ${q2.plainHigher} / ${q2.ties}`.padEnd(30)} ${"".padEnd(15)} ${q2.p === null ? "\u2014" : q2.p.toFixed(3)} (n=${q2.pairsWithGroundTruth}; higher is better)`
|
|
44377
|
+
);
|
|
44378
|
+
} else {
|
|
44379
|
+
L2.push("Answer recall not measured (no prompt listed expected files)");
|
|
44380
|
+
}
|
|
44381
|
+
L2.push("");
|
|
44382
|
+
for (const c2 of r2.conclusions) L2.push(`\u2022 ${c2}`);
|
|
44383
|
+
L2.push("");
|
|
44384
|
+
L2.push(
|
|
44385
|
+
"One model, one repository, this prompt set. Exact sign test and signed-rank test on paired runs; no savings figure is computed or implied, and none should be quoted from this."
|
|
44386
|
+
);
|
|
44387
|
+
return L2.join("\n");
|
|
44388
|
+
}
|
|
44389
|
+
function parsePromptFile(text) {
|
|
44390
|
+
let raw;
|
|
44391
|
+
try {
|
|
44392
|
+
raw = JSON.parse(text);
|
|
44393
|
+
} catch (e) {
|
|
44394
|
+
throw new Error(`prompts file is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
44395
|
+
}
|
|
44396
|
+
if (!Array.isArray(raw) || raw.length === 0) throw new Error("prompts file must be a non-empty JSON array");
|
|
44397
|
+
return raw.map((item, i2) => {
|
|
44398
|
+
if (typeof item === "string" && item.trim()) return { prompt: item };
|
|
44399
|
+
if (item && typeof item === "object" && typeof item.prompt === "string" && item.prompt.trim()) {
|
|
44400
|
+
const expect = item.expect;
|
|
44401
|
+
if (expect !== void 0 && !(Array.isArray(expect) && expect.every((e) => typeof e === "string"))) {
|
|
44402
|
+
throw new Error(`prompt #${i2 + 1}: "expect" must be an array of strings`);
|
|
44403
|
+
}
|
|
44404
|
+
return { prompt: item.prompt, ...expect ? { expect } : {} };
|
|
44405
|
+
}
|
|
44406
|
+
throw new Error(`prompt #${i2 + 1} must be a non-empty string or { "prompt": "...", "expect": [...] }`);
|
|
44407
|
+
});
|
|
44408
|
+
}
|
|
44409
|
+
function mulberry32(seed) {
|
|
44410
|
+
let a2 = seed >>> 0;
|
|
44411
|
+
return () => {
|
|
44412
|
+
a2 = a2 + 1831565813 >>> 0;
|
|
44413
|
+
let t2 = a2;
|
|
44414
|
+
t2 = Math.imul(t2 ^ t2 >>> 15, t2 | 1);
|
|
44415
|
+
t2 ^= t2 + Math.imul(t2 ^ t2 >>> 7, t2 | 61);
|
|
44416
|
+
return ((t2 ^ t2 >>> 14) >>> 0) / 4294967296;
|
|
44417
|
+
};
|
|
44418
|
+
}
|
|
44419
|
+
async function runAb(cfg, harness) {
|
|
44420
|
+
const rng = mulberry32(cfg.seed ?? Date.now());
|
|
44421
|
+
const total = cfg.prompts.length * cfg.repeats;
|
|
44422
|
+
const abortAfter = cfg.abortAfterFailedPairs ?? 2;
|
|
44423
|
+
const pairs = [];
|
|
44424
|
+
let done = 0;
|
|
44425
|
+
let consecutiveFailed = 0;
|
|
44426
|
+
outer: for (let rep = 0; rep < cfg.repeats; rep++) {
|
|
44427
|
+
for (let promptIndex = 0; promptIndex < cfg.prompts.length; promptIndex++) {
|
|
44428
|
+
const order = rng() < 0.5 ? ["context", "plain"] : ["plain", "context"];
|
|
44429
|
+
const prompt = cfg.prompts[promptIndex];
|
|
44430
|
+
const results = {};
|
|
44431
|
+
if (cfg.parallel) {
|
|
44432
|
+
const [first, second] = await Promise.all(order.map((arm) => harness.runArm(arm, prompt)));
|
|
44433
|
+
results[order[0]] = first;
|
|
44434
|
+
results[order[1]] = second;
|
|
44435
|
+
} else {
|
|
44436
|
+
for (const arm of order) results[arm] = await harness.runArm(arm, prompt);
|
|
44437
|
+
}
|
|
44438
|
+
pairs.push({ promptIndex, rep, order, context: results.context, plain: results.plain });
|
|
44439
|
+
done++;
|
|
44440
|
+
cfg.onProgress?.(
|
|
44441
|
+
`[${done}/${total}] prompt ${promptIndex + 1} rep ${rep + 1}: context ${results.context.toolCalls} tools/${(results.context.durationMs / 1e3).toFixed(0)}s${results.context.contextAttached ? "" : " (no context attached)"} \xB7 plain ${results.plain.toolCalls} tools/${(results.plain.durationMs / 1e3).toFixed(0)}s`
|
|
44442
|
+
);
|
|
44443
|
+
const failed = (r2) => !r2.ok && !r2.timedOut;
|
|
44444
|
+
consecutiveFailed = failed(results.context) && failed(results.plain) ? consecutiveFailed + 1 : 0;
|
|
44445
|
+
if (abortAfter > 0 && consecutiveFailed >= abortAfter) {
|
|
44446
|
+
cfg.onProgress?.(
|
|
44447
|
+
`Stopping early: ${consecutiveFailed} consecutive pairs failed entirely (${results.context.error ?? results.plain.error ?? "unknown error"}). More runs would not help \u2014 fix that first.`
|
|
44448
|
+
);
|
|
44449
|
+
break outer;
|
|
44450
|
+
}
|
|
44451
|
+
}
|
|
44452
|
+
}
|
|
44453
|
+
return pairs;
|
|
44454
|
+
}
|
|
44455
|
+
async function freePort() {
|
|
44456
|
+
return new Promise((resolvePort, reject) => {
|
|
44457
|
+
const srv = createServer3();
|
|
44458
|
+
srv.once("error", reject);
|
|
44459
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
44460
|
+
const addr = srv.address();
|
|
44461
|
+
const port2 = typeof addr === "object" && addr ? addr.port : 0;
|
|
44462
|
+
srv.close(() => resolvePort(port2));
|
|
44463
|
+
});
|
|
44464
|
+
});
|
|
44465
|
+
}
|
|
44466
|
+
async function createAbHarness(cfg) {
|
|
44467
|
+
const dir = mkdtempSync(join13(tmpdir4(), "offhand-ab-"));
|
|
44468
|
+
const store2 = new Store(dir);
|
|
44469
|
+
store2.upsertWorkspace(cfg.workspace);
|
|
44470
|
+
store2.setWorkspaceIntelligenceMode(cfg.workspace, "automatic");
|
|
44471
|
+
const broker2 = new ApprovalBroker(3e4);
|
|
44472
|
+
const port2 = cfg.serve ? await freePort() : 0;
|
|
44473
|
+
const manager2 = new SessionManager(store2, cfg.makeRunners(broker2, port2));
|
|
44474
|
+
await manager2.init();
|
|
44475
|
+
broker2.policyProvider = () => manager2.currentPolicy();
|
|
44476
|
+
if (cfg.serve) new LocalSessionServer(manager2, port2, broker2);
|
|
44477
|
+
const send = (msg, reply) => manager2.handle(parseClientMessage(JSON.stringify(msg)), reply);
|
|
44478
|
+
return {
|
|
44479
|
+
runArm(arm, prompt) {
|
|
44480
|
+
const session = store2.createSession(cfg.workspace, cfg.runnerId, cfg.model, `ab-${arm}`);
|
|
44481
|
+
store2.updateSession(session.id, { permissionMode: "plan" });
|
|
44482
|
+
const t0 = Date.now();
|
|
44483
|
+
let toolCalls = 0;
|
|
44484
|
+
let answer = "";
|
|
44485
|
+
let costUsd = null;
|
|
44486
|
+
let contextTokens = null;
|
|
44487
|
+
let contextAttached = false;
|
|
44488
|
+
let filesSelected = null;
|
|
44489
|
+
let error;
|
|
44490
|
+
let timedOut = false;
|
|
44491
|
+
return new Promise((resolveArm) => {
|
|
44492
|
+
let settled = false;
|
|
44493
|
+
let detach = () => {
|
|
44494
|
+
};
|
|
44495
|
+
let timer;
|
|
44496
|
+
let doneAt;
|
|
44497
|
+
const finish = () => {
|
|
44498
|
+
if (settled) return;
|
|
44499
|
+
settled = true;
|
|
44500
|
+
if (timer) clearTimeout(timer);
|
|
44501
|
+
detach();
|
|
44502
|
+
resolveArm({
|
|
44503
|
+
arm,
|
|
44504
|
+
ok: !error && !timedOut,
|
|
44505
|
+
timedOut,
|
|
44506
|
+
...error ? { error } : {},
|
|
44507
|
+
toolCalls,
|
|
44508
|
+
// Measured to the agent's own `done`, not to when we stopped listening.
|
|
44509
|
+
durationMs: (doneAt ?? Date.now()) - t0,
|
|
44510
|
+
costUsd,
|
|
44511
|
+
contextTokens,
|
|
44512
|
+
contextAttached,
|
|
44513
|
+
filesSelected,
|
|
44514
|
+
answer: answer.slice(0, 2e4)
|
|
44515
|
+
});
|
|
44516
|
+
};
|
|
44517
|
+
const sink = (m3) => {
|
|
44518
|
+
if (m3.sessionId !== session.id) return;
|
|
44519
|
+
if (m3.type === "receipt" && doneAt !== void 0) {
|
|
44520
|
+
finish();
|
|
44521
|
+
return;
|
|
44522
|
+
}
|
|
44523
|
+
if (m3.type === "intelligence-review") {
|
|
44524
|
+
contextAttached = true;
|
|
44525
|
+
filesSelected = m3.filesSelected;
|
|
44526
|
+
send({ type: "intelligence-response", sessionId: session.id, reviewId: m3.reviewId, useContext: true }, sink);
|
|
44527
|
+
return;
|
|
44528
|
+
}
|
|
44529
|
+
if (m3.type !== "run-event") return;
|
|
44530
|
+
const e = m3.event;
|
|
44531
|
+
if (e.type === "text") answer += e.chunk;
|
|
44532
|
+
else if (e.type === "tool") {
|
|
44533
|
+
if (e.name !== "auto-approved") toolCalls++;
|
|
44534
|
+
} else if (e.type === "usage") {
|
|
44535
|
+
if (typeof e.costUsd === "number") costUsd = e.costUsd;
|
|
44536
|
+
if (typeof e.contextTokens === "number") contextTokens = e.contextTokens;
|
|
44537
|
+
} else if (e.type === "approval") {
|
|
44538
|
+
send({ type: "approval-response", approvalId: e.id, approve: false }, sink);
|
|
44539
|
+
} else if (e.type === "error") {
|
|
44540
|
+
error = String(e.message ?? "error");
|
|
44541
|
+
finish();
|
|
44542
|
+
} else if (e.type === "done") {
|
|
44543
|
+
doneAt = Date.now();
|
|
44544
|
+
setTimeout(finish, USAGE_GRACE_MS);
|
|
44545
|
+
}
|
|
44546
|
+
};
|
|
44547
|
+
detach = manager2.attach(sink);
|
|
44548
|
+
timer = setTimeout(() => {
|
|
44549
|
+
timedOut = true;
|
|
44550
|
+
send({ type: "cancel", sessionId: session.id }, sink);
|
|
44551
|
+
setTimeout(finish, 3e3);
|
|
44552
|
+
}, cfg.timeoutMs);
|
|
44553
|
+
send({ type: arm === "context" ? "intelligence-prepare" : "prompt", sessionId: session.id, prompt: prompt.prompt }, sink);
|
|
44554
|
+
});
|
|
44555
|
+
},
|
|
44556
|
+
close() {
|
|
44557
|
+
try {
|
|
44558
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
44559
|
+
} catch {
|
|
44560
|
+
}
|
|
44561
|
+
}
|
|
44562
|
+
};
|
|
44563
|
+
}
|
|
44564
|
+
function flag(args2, name) {
|
|
44565
|
+
const i2 = args2.indexOf(name);
|
|
44566
|
+
return i2 >= 0 ? args2[i2 + 1] : void 0;
|
|
44567
|
+
}
|
|
44568
|
+
function intelAbMerge(args2, mergeAt, out) {
|
|
44569
|
+
const files = args2.slice(mergeAt + 1).filter((a2) => !a2.startsWith("--"));
|
|
44570
|
+
if (files.length === 0) {
|
|
44571
|
+
out("usage: offhands intel-ab --merge <result.json> [<result.json> ...] [--json]");
|
|
44572
|
+
return 1;
|
|
44573
|
+
}
|
|
44574
|
+
const prompts = [];
|
|
44575
|
+
const pairs = [];
|
|
44576
|
+
const workspaces = [];
|
|
44577
|
+
let runner = "";
|
|
44578
|
+
let model;
|
|
44579
|
+
for (const f2 of files) {
|
|
44580
|
+
let saved;
|
|
44581
|
+
try {
|
|
44582
|
+
saved = JSON.parse(readFileSync6(resolve5(f2), "utf8"));
|
|
44583
|
+
} catch (e) {
|
|
44584
|
+
out(`could not read ${f2}: ${e instanceof Error ? e.message : String(e)}`);
|
|
44585
|
+
return 1;
|
|
44586
|
+
}
|
|
44587
|
+
if (!Array.isArray(saved?.prompts) || !Array.isArray(saved?.pairs)) {
|
|
44588
|
+
out(`${f2} is not an intel-ab --out result (needs "prompts" and "pairs")`);
|
|
44589
|
+
return 1;
|
|
44590
|
+
}
|
|
44591
|
+
const offset = prompts.length;
|
|
44592
|
+
prompts.push(...saved.prompts);
|
|
44593
|
+
for (const p2 of saved.pairs) pairs.push({ ...p2, promptIndex: p2.promptIndex + offset });
|
|
44594
|
+
workspaces.push(String(saved.workspace ?? f2));
|
|
44595
|
+
runner = runner || String(saved.runnerId ?? "");
|
|
44596
|
+
model = model ?? saved.model;
|
|
44597
|
+
}
|
|
44598
|
+
const report = analyzePairs(pairs, prompts);
|
|
44599
|
+
out(
|
|
44600
|
+
args2.includes("--json") ? JSON.stringify(report, null, 2) : formatAbReport(report, {
|
|
44601
|
+
workspace: workspaces.join(" + "),
|
|
44602
|
+
runner: runner || "unknown",
|
|
44603
|
+
...model ? { model } : {},
|
|
44604
|
+
prompts: prompts.length,
|
|
44605
|
+
repeats: Math.max(1, Math.round(pairs.length / Math.max(prompts.length, 1)))
|
|
44606
|
+
})
|
|
44607
|
+
);
|
|
44608
|
+
return 0;
|
|
44609
|
+
}
|
|
44610
|
+
async function intelAbCli(args2, out = console.log, deps = {}) {
|
|
44611
|
+
const mergeAt = args2.indexOf("--merge");
|
|
44612
|
+
if (mergeAt >= 0) return intelAbMerge(args2, mergeAt, out);
|
|
44613
|
+
const workspaceArg = flag(args2, "--workspace");
|
|
44614
|
+
const promptsArg = flag(args2, "--prompts");
|
|
44615
|
+
const runnerId = flag(args2, "--runner") ?? "claude-code";
|
|
44616
|
+
const repeats = Number(flag(args2, "--repeats") ?? 2);
|
|
44617
|
+
const timeoutS = Number(flag(args2, "--timeout") ?? 240);
|
|
44618
|
+
const maxRuns = Number(flag(args2, "--max-runs") ?? 60);
|
|
44619
|
+
const seedArg = flag(args2, "--seed");
|
|
44620
|
+
const model = flag(args2, "--model") ?? (runnerId === "claude-code" ? "haiku" : void 0);
|
|
44621
|
+
const outFile = flag(args2, "--out");
|
|
44622
|
+
const json = args2.includes("--json");
|
|
44623
|
+
const parallel = !args2.includes("--sequential");
|
|
44624
|
+
if (!workspaceArg || !promptsArg) {
|
|
44625
|
+
out("usage: offhands intel-ab --workspace <path> --prompts <file.json> [--runner claude-code|opencode] [--model m] [--repeats 2] [--timeout 240] [--sequential] [--max-runs 60] [--seed n] [--json] [--out file] [--dry-run] --yes");
|
|
44626
|
+
return 1;
|
|
44627
|
+
}
|
|
44628
|
+
if (runnerId !== "claude-code" && runnerId !== "opencode") {
|
|
44629
|
+
out(`unsupported runner "${runnerId}" \u2014 use claude-code or opencode`);
|
|
44630
|
+
return 1;
|
|
44631
|
+
}
|
|
44632
|
+
if (!Number.isInteger(repeats) || repeats < 1 || !(timeoutS > 0)) {
|
|
44633
|
+
out("--repeats must be a positive integer and --timeout a positive number of seconds");
|
|
44634
|
+
return 1;
|
|
44635
|
+
}
|
|
44636
|
+
const workspace = resolve5(workspaceArg);
|
|
44637
|
+
if (!existsSync12(workspace)) {
|
|
44638
|
+
out(`workspace does not exist: ${workspace}`);
|
|
44639
|
+
return 1;
|
|
44640
|
+
}
|
|
44641
|
+
let prompts;
|
|
44642
|
+
try {
|
|
44643
|
+
prompts = parsePromptFile(readFileSync6(resolve5(promptsArg), "utf8"));
|
|
44644
|
+
} catch (e) {
|
|
44645
|
+
out(`could not use the prompts file: ${e instanceof Error ? e.message : String(e)}`);
|
|
44646
|
+
return 1;
|
|
44647
|
+
}
|
|
44648
|
+
const pairsPlanned = prompts.length * repeats;
|
|
44649
|
+
const runsPlanned = pairsPlanned * 2;
|
|
44650
|
+
if (runsPlanned > maxRuns) {
|
|
44651
|
+
out(`that plan is ${runsPlanned} agent runs, above --max-runs ${maxRuns}. Lower --repeats or the prompt count, or raise --max-runs deliberately.`);
|
|
44652
|
+
return 1;
|
|
44653
|
+
}
|
|
44654
|
+
out(
|
|
44655
|
+
`Plan: ${prompts.length} prompt(s) \xD7 ${repeats} repeat(s) \xD7 2 arms = ${runsPlanned} real ${runnerId} runs${model ? ` (model ${model})` : ""} in read-only plan mode, ${parallel ? "each pair in parallel" : "sequential"}, up to ${timeoutS}s each.`
|
|
44656
|
+
);
|
|
44657
|
+
out(`Workspace: ${workspace}`);
|
|
44658
|
+
if (args2.includes("--dry-run") || !args2.includes("--yes")) {
|
|
44659
|
+
out("");
|
|
44660
|
+
out(args2.includes("--dry-run") ? "Dry run \u2014 nothing was executed." : "This spends real agent tokens. Re-run with --yes to proceed (or --dry-run to only see the plan).");
|
|
44661
|
+
return args2.includes("--dry-run") ? 0 : 2;
|
|
44662
|
+
}
|
|
44663
|
+
if (runnerId === "claude-code") {
|
|
44664
|
+
delete process.env.CLAUDECODE;
|
|
44665
|
+
delete process.env.CLAUDE_CODE_ENTRYPOINT;
|
|
44666
|
+
}
|
|
44667
|
+
const createHarness = deps.createHarness ?? createAbHarness;
|
|
44668
|
+
const harness = await createHarness({
|
|
44669
|
+
workspace,
|
|
44670
|
+
runnerId,
|
|
44671
|
+
...model ? { model } : {},
|
|
44672
|
+
timeoutMs: timeoutS * 1e3,
|
|
44673
|
+
serve: !deps.createHarness,
|
|
44674
|
+
makeRunners: (broker2, port2) => [
|
|
44675
|
+
runnerId === "opencode" ? new OpenCodeRunner(broker2, `http://127.0.0.1:${port2}/status`) : new ClaudeCodeRunner(broker2, `http://127.0.0.1:${port2}/approval`)
|
|
44676
|
+
]
|
|
44677
|
+
});
|
|
44678
|
+
try {
|
|
44679
|
+
const pairs = await runAb({ prompts, repeats, parallel, ...seedArg ? { seed: Number(seedArg) } : {}, onProgress: out }, harness);
|
|
44680
|
+
const report = analyzePairs(pairs, prompts);
|
|
44681
|
+
if (outFile) writeFileSync5(resolve5(outFile), JSON.stringify({ workspace, runnerId, model, prompts, pairs, report }, null, 2));
|
|
44682
|
+
out("");
|
|
44683
|
+
out(json ? JSON.stringify(report, null, 2) : formatAbReport(report, { workspace, runner: runnerId, ...model ? { model } : {}, prompts: prompts.length, repeats, pairsPlanned }));
|
|
44684
|
+
return 0;
|
|
44685
|
+
} finally {
|
|
44686
|
+
harness.close();
|
|
44687
|
+
}
|
|
44688
|
+
}
|
|
44689
|
+
|
|
44203
44690
|
// ../daemon/src/index.ts
|
|
44204
44691
|
var args = process.argv.slice(2);
|
|
44205
44692
|
if (args[0] === "drop") {
|
|
@@ -44220,14 +44707,17 @@ if (args[0] === "drop") {
|
|
|
44220
44707
|
if (args[0] === "intel-stats") {
|
|
44221
44708
|
process.exit(intelStatsCli(args.slice(1)));
|
|
44222
44709
|
}
|
|
44223
|
-
|
|
44710
|
+
if (args[0] === "intel-ab") {
|
|
44711
|
+
process.exit(await intelAbCli(args.slice(1)));
|
|
44712
|
+
}
|
|
44713
|
+
function argValues(flag2) {
|
|
44224
44714
|
const out = [];
|
|
44225
44715
|
for (let i2 = 0; i2 < args.length; i2++) {
|
|
44226
|
-
if (args[i2] ===
|
|
44716
|
+
if (args[i2] === flag2 && args[i2 + 1]) out.push(args[i2 + 1]);
|
|
44227
44717
|
}
|
|
44228
44718
|
return out;
|
|
44229
44719
|
}
|
|
44230
|
-
var argValue = (
|
|
44720
|
+
var argValue = (flag2) => argValues(flag2)[0];
|
|
44231
44721
|
var port = Number(argValue("--port") ?? 4317);
|
|
44232
44722
|
var relayUrl = argValue("--relay")?.replace(/^ws(s?):\/\//, "http$1://");
|
|
44233
44723
|
var forceRepair = args.includes("--repair");
|
|
@@ -44235,9 +44725,9 @@ var approvalTimeoutMs = Number(argValue("--approval-timeout") ?? 300) * 1e3;
|
|
|
44235
44725
|
var webUrl = argValue("--web-url") ?? "https://offhand-web.onrender.com";
|
|
44236
44726
|
var devUrl = argValue("--dev-url");
|
|
44237
44727
|
var store = new Store();
|
|
44238
|
-
var wsArgs = argValues("--workspace").map((w2) =>
|
|
44728
|
+
var wsArgs = argValues("--workspace").map((w2) => resolve6(w2));
|
|
44239
44729
|
for (const w2 of wsArgs) {
|
|
44240
|
-
if (!
|
|
44730
|
+
if (!existsSync13(w2)) {
|
|
44241
44731
|
console.error(`workspace does not exist: ${w2}`);
|
|
44242
44732
|
process.exit(1);
|
|
44243
44733
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "offhands",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "Phone → coding-agent relay. Daemon runs on your laptop, PWA on your phone. E2E encrypted.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -24,6 +24,6 @@
|
|
|
24
24
|
"esbuild": "^0.24.0"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"offhands": "^0.1.
|
|
27
|
+
"offhands": "^0.1.17"
|
|
28
28
|
}
|
|
29
29
|
}
|