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.
Files changed (2) hide show
  1. package/dist/daemon.mjs +533 -43
  2. 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 resolve5 } from "node:path";
4780
- import { existsSync as existsSync12 } from "node:fs";
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((resolve6) => this.waiter = resolve6);
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((resolve6) => {
5041
+ return new Promise((resolve7) => {
5042
5042
  const p2 = spawn(this.resolveBin(), ["--version"], { stdio: "ignore" });
5043
- p2.on("error", () => resolve6(false));
5044
- p2.on("exit", (code) => resolve6(code === 0));
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((resolve6) => {
5135
+ return new Promise((resolve7) => {
5136
5136
  const p2 = spawn2(bin, ["--version"], { stdio: "ignore", shell: process.platform === "win32" });
5137
- p2.on("error", () => resolve6(false));
5138
- p2.on("exit", (code) => resolve6(code === 0));
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((resolve6) => {
5279
+ return new Promise((resolve7) => {
5280
5280
  const p2 = spawn3(cmd[0], [...cmd.slice(1), "--version"], { stdio: "ignore" });
5281
- p2.on("error", () => resolve6(false));
5282
- p2.on("exit", (code) => resolve6(code === 0));
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((resolve6) => {
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", () => resolve6(false));
5568
- p2.on("exit", (code) => resolve6(code === 0));
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((resolve6) => {
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
- resolve6(models);
5596
+ resolve7(models);
5597
5597
  } catch {
5598
- resolve6([]);
5598
+ resolve7([]);
5599
5599
  }
5600
5600
  } else {
5601
- resolve6([]);
5601
+ resolve7([]);
5602
5602
  }
5603
5603
  });
5604
- p2.on("error", () => resolve6([]));
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((resolve6) => {
5886
- pendingQuestions.set(q2.id, (ok, answer) => resolve6({ 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((resolve6, reject) => {
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(() => resolve6(port2));
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((resolve6) => setTimeout(resolve6, ms));
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((resolve6) => setTimeout(() => resolve6("timeout"), OVERALL_TIMEOUT_MS));
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(resolve6, reject) {
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
- resolve6();
12935
+ resolve7();
12936
12936
  } catch (err2) {
12937
12937
  reject(err2);
12938
12938
  }
12939
12939
  };
12940
12940
  Module2.useBackupModule = function() {
12941
- return new Promise(function(resolve7, reject2) {
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
- resolve7();
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((resolve8, reject3) => {
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
- resolve8(xhr.response);
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((resolve8, reject3) => {
39632
+ return new Promise((resolve9, reject3) => {
39633
39633
  Module3["instantiateWasm"](info, (inst, mod) => {
39634
- resolve8(receiveInstance(inst, mod));
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((resolve7, reject2) => {
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
- resolve7(xhr.response);
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((resolve7, reject2) => {
40255
+ return new Promise((resolve8, reject2) => {
40256
40256
  Module2["instantiateWasm"](info, (inst, mod) => {
40257
- resolve7(receiveInstance(inst, mod));
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
- function argValues(flag) {
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] === flag && args[i2 + 1]) out.push(args[i2 + 1]);
44716
+ if (args[i2] === flag2 && args[i2 + 1]) out.push(args[i2 + 1]);
44227
44717
  }
44228
44718
  return out;
44229
44719
  }
44230
- var argValue = (flag) => argValues(flag)[0];
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) => resolve5(w2));
44728
+ var wsArgs = argValues("--workspace").map((w2) => resolve6(w2));
44239
44729
  for (const w2 of wsArgs) {
44240
- if (!existsSync12(w2)) {
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.17",
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.16"
27
+ "offhands": "^0.1.17"
28
28
  }
29
29
  }