harnery 0.5.0 → 0.6.0

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 (68) hide show
  1. package/dist/commander.d.ts +10 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +2 -0
  4. package/dist/commands/agents.d.ts.map +1 -1
  5. package/dist/commands/agents.js +43 -29
  6. package/dist/commands/browse-ai.js +1 -1
  7. package/dist/commands/browse.d.ts.map +1 -1
  8. package/dist/commands/browse.js +41 -9
  9. package/dist/commands/cookies.js +1 -1
  10. package/dist/commands/decision.d.ts +4 -0
  11. package/dist/commands/decision.d.ts.map +1 -0
  12. package/dist/commands/decision.js +354 -0
  13. package/dist/commands/docs.d.ts.map +1 -1
  14. package/dist/commands/docs.js +5 -1
  15. package/dist/commands/fetch.js +1 -1
  16. package/dist/core/agents/events/consume.d.ts +25 -2
  17. package/dist/core/agents/events/consume.d.ts.map +1 -1
  18. package/dist/core/agents/events/consume.js +55 -7
  19. package/dist/core/agents/events/emit.d.ts +2 -1
  20. package/dist/core/agents/events/emit.d.ts.map +1 -1
  21. package/dist/core/agents/events/emit.js +2 -1
  22. package/dist/core/agents/state/scratch.d.ts +1 -1
  23. package/dist/core/agents/state/scratch.js +2 -2
  24. package/dist/core/hooks/effects/index.d.ts.map +1 -1
  25. package/dist/core/hooks/effects/index.js +2 -1
  26. package/dist/lib/agent-browser/client.js +1 -1
  27. package/dist/lib/browser/client.d.ts +14 -0
  28. package/dist/lib/browser/client.d.ts.map +1 -1
  29. package/dist/lib/browser/client.js +20 -0
  30. package/dist/lib/browser/index.d.ts +1 -0
  31. package/dist/lib/browser/index.d.ts.map +1 -1
  32. package/dist/lib/browser/runts.d.ts +44 -0
  33. package/dist/lib/browser/runts.d.ts.map +1 -0
  34. package/dist/lib/browser/runts.js +193 -0
  35. package/dist/lib/completion/walk.js +1 -1
  36. package/dist/lib/cookies/client.d.ts +1 -1
  37. package/dist/lib/cookies/client.d.ts.map +1 -1
  38. package/dist/lib/cookies/client.js +1 -1
  39. package/dist/lib/decision/index.d.ts +212 -0
  40. package/dist/lib/decision/index.d.ts.map +1 -0
  41. package/dist/lib/decision/index.js +523 -0
  42. package/dist/lib/docs-lint.d.ts +1 -0
  43. package/dist/lib/docs-lint.d.ts.map +1 -1
  44. package/dist/lib/docs-lint.js +49 -0
  45. package/dist/lib/tunnel/gate.js +1 -1
  46. package/package.json +3 -1
  47. package/src/commander.ts +12 -0
  48. package/src/commands/agents.ts +47 -26
  49. package/src/commands/browse-ai.ts +1 -1
  50. package/src/commands/browse.ts +63 -8
  51. package/src/commands/cookies.ts +1 -1
  52. package/src/commands/decision.ts +438 -0
  53. package/src/commands/docs.ts +5 -1
  54. package/src/commands/fetch.ts +1 -1
  55. package/src/core/agents/events/consume.ts +65 -7
  56. package/src/core/agents/events/emit.ts +2 -1
  57. package/src/core/agents/state/scratch.ts +2 -2
  58. package/src/core/config.ts +1 -1
  59. package/src/core/hooks/effects/index.ts +2 -1
  60. package/src/lib/agent-browser/client.ts +1 -1
  61. package/src/lib/browser/client.ts +28 -0
  62. package/src/lib/browser/index.ts +4 -0
  63. package/src/lib/browser/runts.ts +218 -0
  64. package/src/lib/completion/walk.ts +1 -1
  65. package/src/lib/cookies/client.ts +2 -2
  66. package/src/lib/decision/index.ts +685 -0
  67. package/src/lib/docs-lint.ts +44 -0
  68. package/src/lib/tunnel/gate.ts +1 -1
@@ -28,6 +28,7 @@ import { homedir, tmpdir } from "node:os";
28
28
  import { basename, join, resolve } from "node:path";
29
29
  import type { Command } from "commander";
30
30
  import type { EmitContext } from "../commander.ts";
31
+ import { readStreamTailBounded } from "../core/agents/events/consume.ts";
31
32
  import {
32
33
  emitCanonical,
33
34
  type Heartbeat,
@@ -38,6 +39,12 @@ import {
38
39
  resolveOwnerWithSource,
39
40
  } from "../core/agents/index.ts";
40
41
  import { resolveBinName } from "../core/config.ts";
42
+
43
+ /** Cap for CLI scans of the unbounded event ledger (`trace` / `health`). Well
44
+ * under V8's ~512MB max string length so a `readFileSync` of the whole file can
45
+ * never throw; covers ample recent history for a diagnostic scan. */
46
+ const STREAM_SCAN_CAP_BYTES = 128 * 1024 * 1024; // 128 MiB
47
+
41
48
  import { parsePsChainLine } from "../core/hooks/resolve/anchor.ts";
42
49
  import {
43
50
  buildCouncilId,
@@ -2098,8 +2105,12 @@ function readStreamStats(root: string): { bytes: number; lines: number; cursor_b
2098
2105
  let backlog = 0;
2099
2106
  let seenCursor = cursor === null; // no cursor → everything is "backlog"
2100
2107
  try {
2101
- const raw = readFileSync(streamPath, "utf8");
2102
- for (const line of raw.split("\n")) {
2108
+ // Bounded tail read: the ledger grows without bound and a whole-file
2109
+ // readFileSync throws past V8's ~512MB string limit. lines/backlog are then
2110
+ // scoped to the scanned window; `bytes` (full size, via statSync above) still
2111
+ // reflects the true stream size for the health rollup.
2112
+ const { text } = readStreamTailBounded(streamPath, STREAM_SCAN_CAP_BYTES);
2113
+ for (const line of text.split("\n")) {
2103
2114
  if (!line.trim()) continue;
2104
2115
  lines++;
2105
2116
  if (seenCursor) {
@@ -2225,24 +2236,34 @@ function runTrace(
2225
2236
  const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 200);
2226
2237
  const candidateIds = targetId.includes("\x00") ? targetId.split("\x00") : [targetId];
2227
2238
 
2228
- // Scan the full stream once, bucket events by instance_id for the candidates.
2239
+ // Scan the stream tail once, bucket events by instance_id for the candidates.
2240
+ // Bounded read: the ledger grows without bound and a whole-file readFileSync
2241
+ // throws past V8's ~512MB string limit. A trace of an agent whose events all
2242
+ // predate the window is reported as truncated rather than crashing.
2229
2243
  const streamPath = resolve(root, ".harnery", "events.ndjson");
2230
2244
  const byId = new Map<string, CanonicalEvent[]>();
2231
- if (existsSync(streamPath)) {
2232
- for (const line of readFileSync(streamPath, "utf8").split("\n")) {
2233
- if (!line) continue;
2234
- try {
2235
- const ev = JSON.parse(line) as CanonicalEvent;
2236
- if (!ev.instance_id || !candidateIds.includes(ev.instance_id)) continue;
2237
- if (sinceMs && Date.parse(ev.ts) < sinceMs) continue;
2238
- const arr = byId.get(ev.instance_id) ?? [];
2239
- arr.push(ev);
2240
- byId.set(ev.instance_id, arr);
2241
- } catch {
2242
- /* skip */
2243
- }
2245
+ const { text: streamText, truncated: streamTruncated } = readStreamTailBounded(
2246
+ streamPath,
2247
+ STREAM_SCAN_CAP_BYTES,
2248
+ );
2249
+ for (const line of streamText.split("\n")) {
2250
+ if (!line) continue;
2251
+ try {
2252
+ const ev = JSON.parse(line) as CanonicalEvent;
2253
+ if (!ev.instance_id || !candidateIds.includes(ev.instance_id)) continue;
2254
+ if (sinceMs && Date.parse(ev.ts) < sinceMs) continue;
2255
+ const arr = byId.get(ev.instance_id) ?? [];
2256
+ arr.push(ev);
2257
+ byId.set(ev.instance_id, arr);
2258
+ } catch {
2259
+ /* skip */
2244
2260
  }
2245
2261
  }
2262
+ if (streamTruncated) {
2263
+ process.stderr.write(
2264
+ `note: event ledger exceeds ${Math.round(STREAM_SCAN_CAP_BYTES / 1024 / 1024)}MB; traced only the most recent window (older events omitted)\n`,
2265
+ );
2266
+ }
2246
2267
 
2247
2268
  // If the name mapped to multiple instances, trace the one with the latest event.
2248
2269
  let resolvedId = candidateIds[0]!;
@@ -2629,7 +2650,7 @@ function renderHealthBox(report: HealthReport): void {
2629
2650
  const localTime = formatLocalTime(new Date(report.generated_at));
2630
2651
  const title = `Coord Health (${localTime})`;
2631
2652
 
2632
- process.stdout.write(`${formatBox(title, rows)}\n`); // lint-ok-emission: chat-paste path; mirrors runStatus's direct write so the box surfaces in both TTY + bp-session-teed contexts
2653
+ process.stdout.write(`${formatBox(title, rows)}\n`); // lint-ok-emission: chat-paste path; mirrors runStatus's direct write so the box surfaces in both TTY + harn-session-teed contexts
2633
2654
 
2634
2655
  if (report.anomalies.length > 0) {
2635
2656
  process.stdout.write("\n"); // lint-ok-emission: same chat-paste path
@@ -2686,7 +2707,7 @@ function runHarnessProbe(
2686
2707
  // and `resolveOwner` here, so the probe reports exactly what the live hot
2687
2708
  // path resolves.
2688
2709
  const anchorTokens = new Set(["claude", "claude-code", "cursor", "codex"]);
2689
- const override = process.env.BP_AGENT_COORD_TEST_ANCHOR_PID;
2710
+ const override = process.env.HARNERY_AGENT_COORD_TEST_ANCHOR_PID;
2690
2711
  let anchorPid = override && Number(override) > 0 ? override : "";
2691
2712
  const chainParts: string[] = [];
2692
2713
  let walkPid = process.pid;
@@ -2795,12 +2816,12 @@ function runHarnessProbe(
2795
2816
  * dispatcher in an isolated sandbox.
2796
2817
  *
2797
2818
  * Sandbox isolation strategy:
2798
- * - mkdtempSync(tmpdir(), "bp-harness-probe-") creates a non-git tmp dir.
2819
+ * - mkdtempSync(tmpdir(), "harn-harness-probe-") creates a non-git tmp dir.
2799
2820
  * - The dispatcher's coord-root resolution falls back to
2800
- * `BP_COORD_ROOT_OVERRIDE` when git rev-parse fails. We set it to the sandbox.
2821
+ * `HARNERY_COORD_ROOT_OVERRIDE` when git rev-parse fails. We set it to the sandbox.
2801
2822
  * - We rewrite the payload's `cwd` field (Cursor cds to it) to the sandbox,
2802
2823
  * so real `.harnery/` never gets touched.
2803
- * - We set `BP_AGENT_COORD_OFF=0` explicitly so any user-side off-switch in
2824
+ * - We set `HARNERY_AGENT_COORD_OFF=0` explicitly so any user-side off-switch in
2804
2825
  * the environment doesn't mask adapter crashes.
2805
2826
  *
2806
2827
  * Sample shape: probe-meta wrapped (`_probe_meta.event` + `.payload`) OR bare
@@ -2880,7 +2901,7 @@ function replayHarnessSamples(
2880
2901
  };
2881
2902
  const harnessFlag = harness === "claude_code" ? "claude-code" : harness;
2882
2903
 
2883
- const sandbox = mkdtempSync(join(tmpdir(), "bp-harness-probe-"));
2904
+ const sandbox = mkdtempSync(join(tmpdir(), "harn-harness-probe-"));
2884
2905
  const results: SampleReplayResult[] = [];
2885
2906
 
2886
2907
  try {
@@ -2928,10 +2949,10 @@ function replayHarnessSamples(
2928
2949
  timeout: 10_000,
2929
2950
  env: {
2930
2951
  ...process.env,
2931
- BP_COORD_ROOT_OVERRIDE: sandbox,
2932
- BP_AGENT_COORD_HARNESS: harness,
2933
- BP_AGENT_COORD_PLATFORM: harness,
2934
- BP_AGENT_COORD_OFF: "0",
2952
+ HARNERY_COORD_ROOT_OVERRIDE: sandbox,
2953
+ HARNERY_AGENT_COORD_HARNESS: harness,
2954
+ HARNERY_AGENT_COORD_PLATFORM: harness,
2955
+ HARNERY_AGENT_COORD_OFF: "0",
2935
2956
  },
2936
2957
  });
2937
2958
 
@@ -99,7 +99,7 @@ function runBrowseAi(url: string, opts: BrowseAiOpts): void {
99
99
  const jar =
100
100
  opts.cookies === false
101
101
  ? null
102
- : new CookieJar({ path: opts.store ?? DEFAULT_STORE, source: "bp-browse-ai" });
102
+ : new CookieJar({ path: opts.store ?? DEFAULT_STORE, source: "harn-browse-ai" });
103
103
  const ab = new AgentBrowser({
104
104
  jar,
105
105
  timeoutMs: Number.parseInt(opts.timeout, 10),
@@ -10,6 +10,7 @@ import {
10
10
  type DevOverlayResult,
11
11
  type Diagnostics,
12
12
  type OverflowResult,
13
+ type RuntsResult,
13
14
  type VisibilityResult,
14
15
  type WidthResult,
15
16
  } from "../lib/browser/index.ts";
@@ -77,16 +78,22 @@ interface BrowseOpts {
77
78
  checkVisibleThreshold?: string;
78
79
  checkVisibleFail?: boolean;
79
80
  checkVisibleSampleGrid?: string;
80
- checkVisibleNoAnnotate?: boolean;
81
+ // Commander maps `--no-check-visible-annotate` to `checkVisibleAnnotate: false`.
82
+ checkVisibleAnnotate?: boolean;
81
83
  // Width-fill check
82
84
  checkWidth?: string[];
83
85
  checkWidthThreshold?: string;
84
86
  checkWidthFail?: boolean;
85
- checkWidthNoAnnotate?: boolean;
87
+ checkWidthAnnotate?: boolean;
88
+ // `--check-runts [selector]`: true = whole body, string = scope selector.
89
+ checkRunts?: boolean | string;
90
+ checkRuntsMinChars?: string;
91
+ checkRuntsFail?: boolean;
92
+ checkRuntsAnnotate?: boolean;
86
93
  // Horizontal-overflow check
87
94
  checkOverflow?: boolean;
88
95
  checkOverflowFail?: boolean;
89
- checkOverflowNoAnnotate?: boolean;
96
+ checkOverflowAnnotate?: boolean;
90
97
  // Visual regression
91
98
  baseline?: string;
92
99
  diff?: string;
@@ -232,6 +239,24 @@ export function registerBrowseCommand(
232
239
  "--no-check-overflow-annotate",
233
240
  "Skip drawing overflow annotations on the screenshot (JSON still emitted).",
234
241
  )
242
+ .option(
243
+ "--check-runts [selector]",
244
+ "Scan text blocks for runts (a single word alone on a block's last visual line) " +
245
+ "by counting words on the last line via per-word Range rects — width thresholds " +
246
+ "miss narrow-column runts. Optional selector scopes the sweep (default: whole body). " +
247
+ "Atomic tokens (URLs, emails, phone numbers) are excluded. Reports hits in the JSON " +
248
+ "envelope under `runts` and annotates them on the screenshot.",
249
+ )
250
+ .option(
251
+ "--check-runts-min-chars <n>",
252
+ "Minimum block text length to scan (default 40; smaller labels can't meaningfully wrap).",
253
+ "40",
254
+ )
255
+ .option("--check-runts-fail", "Exit non-zero if any runt is detected.")
256
+ .option(
257
+ "--no-check-runts-annotate",
258
+ "Skip drawing runt boxes on the screenshot (JSON still emitted).",
259
+ )
235
260
  .option(
236
261
  "--baseline <name>",
237
262
  "Save the captured screenshot as a named baseline at " +
@@ -275,7 +300,7 @@ async function runBrowse(
275
300
  const jar =
276
301
  opts.cookies === false
277
302
  ? null
278
- : new CookieJar({ path: opts.store ?? DEFAULT_STORE, source: "bp-browse" });
303
+ : new CookieJar({ path: opts.store ?? DEFAULT_STORE, source: "harn-browse" });
279
304
  const headed = opts.login || opts.headed;
280
305
  const viewport = parseViewport(opts.viewport ?? "desktop");
281
306
 
@@ -330,7 +355,7 @@ async function runBrowse(
330
355
  visibility = await browser.checkVisibility(opts.checkVisible, {
331
356
  sampleGrid: Number.parseInt(opts.checkVisibleSampleGrid ?? "3", 10),
332
357
  });
333
- if (opts.checkVisibleNoAnnotate !== true) {
358
+ if (opts.checkVisibleAnnotate !== false) {
334
359
  await browser.annotateVisibility(visibility);
335
360
  }
336
361
  }
@@ -343,9 +368,16 @@ async function runBrowse(
343
368
  if (opts.checkOverflow) {
344
369
  overflow = await browser.checkOverflow();
345
370
  }
371
+ let runts: RuntsResult | undefined;
372
+ if (opts.checkRunts) {
373
+ runts = await browser.checkRunts({
374
+ scope: typeof opts.checkRunts === "string" ? opts.checkRunts : null,
375
+ minChars: Number.parseInt(opts.checkRuntsMinChars ?? "40", 10),
376
+ });
377
+ }
346
378
  const widthThreshold = Number.parseFloat(opts.checkWidthThreshold ?? "0.9");
347
- const annotateWidth = widths && opts.checkWidthNoAnnotate !== true;
348
- const annotateOverflow = overflow && opts.checkOverflowNoAnnotate !== true;
379
+ const annotateWidth = widths && opts.checkWidthAnnotate !== false;
380
+ const annotateOverflow = overflow && opts.checkOverflowAnnotate !== false;
349
381
  if (annotateWidth || annotateOverflow) {
350
382
  await browser.annotateLayout({
351
383
  widths: annotateWidth ? widths! : [],
@@ -353,6 +385,10 @@ async function runBrowse(
353
385
  widthThreshold,
354
386
  });
355
387
  }
388
+ const annotateRunts = runts && runts.runts.length > 0 && opts.checkRuntsAnnotate !== false;
389
+ if (annotateRunts) {
390
+ await browser.annotateRunts(runts!);
391
+ }
356
392
 
357
393
  if (opts.login) {
358
394
  await new Promise<void>((res) => {
@@ -385,6 +421,7 @@ async function runBrowse(
385
421
  visibility,
386
422
  widths,
387
423
  overflow,
424
+ runts,
388
425
  devOverlay,
389
426
  batchResult,
390
427
  );
@@ -397,17 +434,21 @@ async function runBrowse(
397
434
  visibility,
398
435
  widths,
399
436
  overflow,
437
+ runts,
400
438
  devOverlay,
401
439
  batchResult,
402
440
  );
403
441
  }
404
442
 
405
- if (visibility && opts.checkVisibleNoAnnotate !== true) {
443
+ if (visibility && opts.checkVisibleAnnotate !== false) {
406
444
  await browser.clearVisibilityAnnotations();
407
445
  }
408
446
  if (annotateWidth || annotateOverflow) {
409
447
  await browser.clearLayoutAnnotations();
410
448
  }
449
+ if (annotateRunts) {
450
+ await browser.clearRuntsAnnotations();
451
+ }
411
452
 
412
453
  if (opts.checkVisibleFail && visibility) {
413
454
  const threshold = Number.parseFloat(opts.checkVisibleThreshold ?? "0.9");
@@ -446,6 +487,16 @@ async function runBrowse(
446
487
  }
447
488
  }
448
489
 
490
+ if (opts.checkRuntsFail && runts && runts.runts.length > 0) {
491
+ for (const hit of runts.runts) {
492
+ emit.log(
493
+ `check-runts FAIL ${hit.block}: last line is a lone "${hit.word}" ("…${hit.snippet.slice(-40)}")`,
494
+ "warn",
495
+ );
496
+ }
497
+ process.exitCode = 2;
498
+ }
499
+
449
500
  if (opts.checkOverflowFail && overflow?.hasHorizontalOverflow) {
450
501
  const culprit = overflow.widerThanViewport[0] ?? overflow.rightOverflow[0] ?? null;
451
502
  const detail = culprit
@@ -578,6 +629,7 @@ async function runPrintMode(
578
629
  visibility: VisibilityResult[] | undefined,
579
630
  widths: WidthResult[] | undefined,
580
631
  overflow: OverflowResult | undefined,
632
+ runts: RuntsResult | undefined,
581
633
  devOverlay: DevOverlayResult | undefined,
582
634
  batchResult: BatchResult | undefined,
583
635
  ): Promise<void> {
@@ -605,6 +657,7 @@ async function runPrintMode(
605
657
  if (visibility) result.visibility = visibility;
606
658
  if (widths) result.width = widths;
607
659
  if (overflow) result.overflow = overflow;
660
+ if (runts) result.runts = runts;
608
661
  if (devOverlay) result.devOverlay = devOverlay;
609
662
  if (batchResult && batchResult.clipboardReads.length > 0) {
610
663
  result.batchClipboardReads = batchResult.clipboardReads;
@@ -634,6 +687,7 @@ async function runTrioMode(
634
687
  visibility: VisibilityResult[] | undefined,
635
688
  widths: WidthResult[] | undefined,
636
689
  overflow: OverflowResult | undefined,
690
+ runts: RuntsResult | undefined,
637
691
  devOverlay: DevOverlayResult | undefined,
638
692
  batchResult: BatchResult | undefined,
639
693
  ): Promise<void> {
@@ -675,6 +729,7 @@ async function runTrioMode(
675
729
  if (visibility) envelope.visibility = visibility;
676
730
  if (widths) envelope.width = widths;
677
731
  if (overflow) envelope.overflow = overflow;
732
+ if (runts) envelope.runts = runts;
678
733
  if (devOverlay) envelope.devOverlay = devOverlay;
679
734
  if (batchResult && batchResult.clipboardReads.length > 0) {
680
735
  envelope.batchClipboardReads = batchResult.clipboardReads;
@@ -22,7 +22,7 @@ interface CommonOpts {
22
22
  function jarFrom(opts: CommonOpts): CookieJar {
23
23
  return new CookieJar({
24
24
  path: opts.store ?? DEFAULT_STORE,
25
- source: "bp-cookies",
25
+ source: "harn-cookies",
26
26
  });
27
27
  }
28
28