harnery 0.5.0 → 0.7.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.
- package/README.md +16 -6
- package/dist/commander.d.ts +29 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +4 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +94 -33
- package/dist/commands/browse-ai.js +1 -1
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +41 -9
- package/dist/commands/cookies.js +1 -1
- package/dist/commands/decision.d.ts +4 -0
- package/dist/commands/decision.d.ts.map +1 -0
- package/dist/commands/decision.js +354 -0
- package/dist/commands/deinit.d.ts.map +1 -1
- package/dist/commands/deinit.js +4 -0
- package/dist/commands/devtools.d.ts +4 -0
- package/dist/commands/devtools.d.ts.map +1 -0
- package/dist/commands/devtools.js +239 -0
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +74 -2
- package/dist/commands/doctor.js +12 -4
- package/dist/commands/env.d.ts.map +1 -1
- package/dist/commands/env.js +3 -63
- package/dist/commands/fetch.js +1 -1
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +54 -14
- package/dist/commands/scratch.js +1 -1
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +273 -62
- package/dist/commands/web-fetch.js +1 -1
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +32 -8
- package/dist/core/agents/events/consume.d.ts +25 -2
- package/dist/core/agents/events/consume.d.ts.map +1 -1
- package/dist/core/agents/events/consume.js +55 -7
- package/dist/core/agents/events/emit.d.ts +2 -1
- package/dist/core/agents/events/emit.d.ts.map +1 -1
- package/dist/core/agents/events/emit.js +6 -1
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +16 -5
- package/dist/core/agents/state/scratch.d.ts +1 -1
- package/dist/core/agents/state/scratch.js +2 -2
- package/dist/core/config.d.ts +10 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +13 -0
- package/dist/core/hooks/cli.js +3 -3
- package/dist/core/hooks/effects/index.d.ts +11 -7
- package/dist/core/hooks/effects/index.d.ts.map +1 -1
- package/dist/core/hooks/effects/index.js +15 -17
- package/dist/core/hooks/events/emit.d.ts.map +1 -1
- package/dist/core/hooks/events/emit.js +4 -0
- package/dist/core/hooks/events/rotate.d.ts +43 -0
- package/dist/core/hooks/events/rotate.d.ts.map +1 -0
- package/dist/core/hooks/events/rotate.js +142 -0
- package/dist/core/hooks/harness/events.d.ts +11 -1
- package/dist/core/hooks/harness/events.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.js +22 -3
- package/dist/core/hooks/harness/wiring.d.ts +8 -0
- package/dist/core/hooks/harness/wiring.d.ts.map +1 -1
- package/dist/core/hooks/harness/wiring.js +34 -5
- package/dist/core/scratch/index.d.ts.map +1 -0
- package/dist/{lib → core}/scratch/index.js +2 -2
- package/dist/lib/agent-browser/client.js +1 -1
- package/dist/lib/browser/client.d.ts +14 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +20 -0
- package/dist/lib/browser/index.d.ts +1 -0
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/runts.d.ts +44 -0
- package/dist/lib/browser/runts.d.ts.map +1 -0
- package/dist/lib/browser/runts.js +193 -0
- package/dist/lib/completion/walk.js +1 -1
- package/dist/lib/cookies/client.d.ts +1 -1
- package/dist/lib/cookies/client.d.ts.map +1 -1
- package/dist/lib/cookies/client.js +1 -1
- package/dist/lib/decision/index.d.ts +212 -0
- package/dist/lib/decision/index.d.ts.map +1 -0
- package/dist/lib/decision/index.js +523 -0
- package/dist/lib/devtools.d.ts +178 -0
- package/dist/lib/devtools.d.ts.map +1 -0
- package/dist/lib/devtools.js +1328 -0
- package/dist/lib/docs-frontmatter-migrate.d.ts +33 -0
- package/dist/lib/docs-frontmatter-migrate.d.ts.map +1 -0
- package/dist/lib/docs-frontmatter-migrate.js +364 -0
- package/dist/lib/docs-frontmatter.d.ts +33 -0
- package/dist/lib/docs-frontmatter.d.ts.map +1 -0
- package/dist/lib/docs-frontmatter.js +130 -0
- package/dist/lib/docs-index.d.ts +1 -0
- package/dist/lib/docs-index.d.ts.map +1 -1
- package/dist/lib/docs-index.js +4 -5
- package/dist/lib/docs-lint.d.ts +3 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +67 -12
- package/dist/lib/docs-meta.d.ts +14 -0
- package/dist/lib/docs-meta.d.ts.map +1 -0
- package/dist/lib/docs-meta.js +34 -0
- package/dist/lib/docs-sweep.d.ts +12 -0
- package/dist/lib/docs-sweep.d.ts.map +1 -1
- package/dist/lib/docs-sweep.js +98 -103
- package/dist/lib/format.js +2 -2
- package/dist/lib/http/index.d.ts +1 -0
- package/dist/lib/http/index.d.ts.map +1 -1
- package/dist/lib/http/index.js +1 -0
- package/dist/lib/http/request.d.ts +77 -0
- package/dist/lib/http/request.d.ts.map +1 -0
- package/dist/lib/http/request.js +105 -0
- package/dist/lib/instructions/apply.d.ts +63 -0
- package/dist/lib/instructions/apply.d.ts.map +1 -0
- package/dist/lib/instructions/apply.js +255 -0
- package/dist/lib/instructions/splice.d.ts +73 -0
- package/dist/lib/instructions/splice.d.ts.map +1 -0
- package/dist/lib/instructions/splice.js +118 -0
- package/dist/lib/instructions/templates.d.ts +45 -0
- package/dist/lib/instructions/templates.d.ts.map +1 -0
- package/dist/lib/instructions/templates.js +258 -0
- package/dist/lib/tunnel/gate.d.ts +1 -0
- package/dist/lib/tunnel/gate.d.ts.map +1 -1
- package/dist/lib/tunnel/gate.js +15 -10
- package/dist/lib/tunnel/state.d.ts +11 -1
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +8 -3
- package/package.json +9 -6
- package/src/commander.ts +35 -0
- package/src/commands/agents.ts +97 -29
- package/src/commands/browse-ai.ts +1 -1
- package/src/commands/browse.ts +63 -8
- package/src/commands/cookies.ts +1 -1
- package/src/commands/decision.ts +438 -0
- package/src/commands/deinit.ts +5 -0
- package/src/commands/devtools.ts +284 -0
- package/src/commands/docs.ts +86 -2
- package/src/commands/doctor.ts +13 -4
- package/src/commands/env.ts +11 -77
- package/src/commands/fetch.ts +1 -1
- package/src/commands/init.ts +66 -15
- package/src/commands/scratch.ts +1 -1
- package/src/commands/tunnel.ts +316 -65
- package/src/commands/web-fetch.ts +1 -1
- package/src/core/agents/coord-client.ts +34 -7
- package/src/core/agents/events/consume.ts +65 -7
- package/src/core/agents/events/emit.ts +7 -1
- package/src/core/agents/rules/claim-conflict.ts +17 -6
- package/src/core/agents/state/scratch.ts +2 -2
- package/src/core/config.ts +15 -1
- package/src/core/hooks/cli.ts +3 -3
- package/src/core/hooks/effects/index.ts +23 -16
- package/src/core/hooks/events/emit.ts +5 -0
- package/src/core/hooks/events/rotate.ts +151 -0
- package/src/core/hooks/harness/events.ts +30 -3
- package/src/core/hooks/harness/wiring.ts +46 -5
- package/src/{lib → core}/scratch/index.ts +2 -2
- package/src/lib/agent-browser/client.ts +1 -1
- package/src/lib/browser/client.ts +28 -0
- package/src/lib/browser/index.ts +4 -0
- package/src/lib/browser/runts.ts +218 -0
- package/src/lib/completion/walk.ts +1 -1
- package/src/lib/cookies/client.ts +2 -2
- package/src/lib/decision/index.ts +685 -0
- package/src/lib/devtools.ts +1653 -0
- package/src/lib/docs-frontmatter-migrate.ts +427 -0
- package/src/lib/docs-frontmatter.ts +151 -0
- package/src/lib/docs-index.ts +4 -5
- package/src/lib/docs-lint.ts +61 -11
- package/src/lib/docs-meta.ts +44 -0
- package/src/lib/docs-sweep.ts +104 -102
- package/src/lib/format.ts +2 -2
- package/src/lib/http/index.ts +1 -0
- package/src/lib/http/request.ts +154 -0
- package/src/lib/instructions/apply.ts +318 -0
- package/src/lib/instructions/splice.ts +148 -0
- package/src/lib/instructions/templates.ts +295 -0
- package/src/lib/tunnel/gate.ts +15 -10
- package/src/lib/tunnel/state.ts +19 -4
- package/dist/lib/scratch/index.d.ts.map +0 -1
- /package/dist/{lib → core}/scratch/index.d.ts +0 -0
package/src/commands/agents.ts
CHANGED
|
@@ -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,
|
|
@@ -35,10 +36,18 @@ import {
|
|
|
35
36
|
normalizeHarness,
|
|
36
37
|
readHeartbeat,
|
|
37
38
|
resolveOwner,
|
|
39
|
+
resolveOwnerBySessionEnv,
|
|
38
40
|
resolveOwnerWithSource,
|
|
39
41
|
} from "../core/agents/index.ts";
|
|
40
42
|
import { resolveBinName } from "../core/config.ts";
|
|
43
|
+
|
|
44
|
+
/** Cap for CLI scans of the unbounded event ledger (`trace` / `health`). Well
|
|
45
|
+
* under V8's ~512MB max string length so a `readFileSync` of the whole file can
|
|
46
|
+
* never throw; covers ample recent history for a diagnostic scan. */
|
|
47
|
+
const STREAM_SCAN_CAP_BYTES = 128 * 1024 * 1024; // 128 MiB
|
|
48
|
+
|
|
41
49
|
import { parsePsChainLine } from "../core/hooks/resolve/anchor.ts";
|
|
50
|
+
import { appendEntry, resolveOwnerByName } from "../core/scratch/index.ts";
|
|
42
51
|
import {
|
|
43
52
|
buildCouncilId,
|
|
44
53
|
buildInviteMarkdown,
|
|
@@ -73,7 +82,6 @@ import {
|
|
|
73
82
|
lookupById as lookupIdentityById,
|
|
74
83
|
lookupByName as lookupIdentityByName,
|
|
75
84
|
} from "../lib/identities/index.ts";
|
|
76
|
-
import { appendEntry, resolveOwnerByName } from "../lib/scratch/index.ts";
|
|
77
85
|
|
|
78
86
|
const FRESHNESS_SECS = 600; // 10-minute heartbeat-freshness window.
|
|
79
87
|
|
|
@@ -159,8 +167,7 @@ export function registerAgentsCommand(program: Command, emitParam: EmitContext):
|
|
|
159
167
|
cmd
|
|
160
168
|
.command("show <name>")
|
|
161
169
|
.description(
|
|
162
|
-
"Deep-dive on one peer agent: registry state (files held, last tool, task) " +
|
|
163
|
-
"plus claude-sessions history (latest title, recent prompts, recent tools, tool-usage tallies). " +
|
|
170
|
+
"Deep-dive on one peer agent: registry state (files held, last tool, task, turn summary). " +
|
|
164
171
|
"Disambiguates name → instance_id via prefix match.",
|
|
165
172
|
)
|
|
166
173
|
.option("--json", "JSON envelope output")
|
|
@@ -687,6 +694,7 @@ function runWhoami(opts: { json?: boolean }): void {
|
|
|
687
694
|
process.exit(1);
|
|
688
695
|
}
|
|
689
696
|
|
|
697
|
+
ensureCursorSession(root);
|
|
690
698
|
const resolved = resolveOwnerWithSource();
|
|
691
699
|
const myOwner = resolved.owner;
|
|
692
700
|
if (!myOwner) {
|
|
@@ -1165,6 +1173,50 @@ function formatLocalShort(iso: string): string {
|
|
|
1165
1173
|
}).format(d);
|
|
1166
1174
|
}
|
|
1167
1175
|
|
|
1176
|
+
function cursorEnvSessionId(): string | null {
|
|
1177
|
+
const raw = process.env.CURSOR_SESSION_ID?.trim() || process.env.CURSOR_CONVERSATION_ID?.trim();
|
|
1178
|
+
if (!raw) return null;
|
|
1179
|
+
return raw.startsWith("bc-") && raw.length > 3 ? raw.slice(3) : raw;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function shouldBootstrapCursorSession(): boolean {
|
|
1183
|
+
return process.env.CURSOR_AGENT === "1" && cursorEnvSessionId() !== null;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function ensureCursorSession(root: string): void {
|
|
1187
|
+
if (!shouldBootstrapCursorSession()) return;
|
|
1188
|
+
if (resolveOwnerBySessionEnv(root)) return;
|
|
1189
|
+
|
|
1190
|
+
const sessionId = cursorEnvSessionId();
|
|
1191
|
+
if (!sessionId) return;
|
|
1192
|
+
const agentHook = resolve(root, "harnery", "bin", "agent-hook");
|
|
1193
|
+
if (!existsSync(agentHook)) return;
|
|
1194
|
+
|
|
1195
|
+
const payload = JSON.stringify({
|
|
1196
|
+
conversation_id: sessionId,
|
|
1197
|
+
session_id: sessionId,
|
|
1198
|
+
hook_event_name: "sessionStart",
|
|
1199
|
+
workspace_roots: [root],
|
|
1200
|
+
cwd: root,
|
|
1201
|
+
composer_mode: "agent",
|
|
1202
|
+
is_background_agent: false,
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
spawnSync("bash", [agentHook, "session-start", "--harness", "cursor"], {
|
|
1206
|
+
input: payload,
|
|
1207
|
+
cwd: root,
|
|
1208
|
+
encoding: "utf8",
|
|
1209
|
+
timeout: 3000,
|
|
1210
|
+
env: {
|
|
1211
|
+
...process.env,
|
|
1212
|
+
HARNERY_AGENT_COORD_PLATFORM: "cursor",
|
|
1213
|
+
HARNERY_COORD_ROOT_OVERRIDE: root,
|
|
1214
|
+
CURSOR_SESSION_ID: sessionId,
|
|
1215
|
+
CURSOR_CONVERSATION_ID: sessionId,
|
|
1216
|
+
},
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1168
1220
|
function runReleaseClaim(path: string): void {
|
|
1169
1221
|
const root = monorepoRoot();
|
|
1170
1222
|
if (!root) {
|
|
@@ -1213,6 +1265,7 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
|
|
|
1213
1265
|
// Identity: prefer explicit --session-id (the ppid-walk-free escape hatch,
|
|
1214
1266
|
// mirrors `status`), fall back to the ppid walk. Cursor shell tool calls
|
|
1215
1267
|
// don't descend from a pid-map-registered anchor, so the walk can miss there.
|
|
1268
|
+
if (!opts?.sessionId) ensureCursorSession(root);
|
|
1216
1269
|
const myOwner = opts?.sessionId ?? resolveOwner();
|
|
1217
1270
|
if (!myOwner) {
|
|
1218
1271
|
emit.error({
|
|
@@ -1265,6 +1318,7 @@ function runStatus(opts: { json?: boolean; sessionId?: string }): void {
|
|
|
1265
1318
|
|
|
1266
1319
|
// Identity resolution: prefer explicit --session-id (hook-friendly), fall
|
|
1267
1320
|
// back to ppid walk for interactive shell usage.
|
|
1321
|
+
if (!opts.sessionId) ensureCursorSession(root);
|
|
1268
1322
|
const myOwner = opts.sessionId ?? resolveOwner();
|
|
1269
1323
|
if (!myOwner) {
|
|
1270
1324
|
emit.error({
|
|
@@ -2098,8 +2152,12 @@ function readStreamStats(root: string): { bytes: number; lines: number; cursor_b
|
|
|
2098
2152
|
let backlog = 0;
|
|
2099
2153
|
let seenCursor = cursor === null; // no cursor → everything is "backlog"
|
|
2100
2154
|
try {
|
|
2101
|
-
|
|
2102
|
-
|
|
2155
|
+
// Bounded tail read: the ledger grows without bound and a whole-file
|
|
2156
|
+
// readFileSync throws past V8's ~512MB string limit. lines/backlog are then
|
|
2157
|
+
// scoped to the scanned window; `bytes` (full size, via statSync above) still
|
|
2158
|
+
// reflects the true stream size for the health rollup.
|
|
2159
|
+
const { text } = readStreamTailBounded(streamPath, STREAM_SCAN_CAP_BYTES);
|
|
2160
|
+
for (const line of text.split("\n")) {
|
|
2103
2161
|
if (!line.trim()) continue;
|
|
2104
2162
|
lines++;
|
|
2105
2163
|
if (seenCursor) {
|
|
@@ -2225,24 +2283,34 @@ function runTrace(
|
|
|
2225
2283
|
const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 200);
|
|
2226
2284
|
const candidateIds = targetId.includes("\x00") ? targetId.split("\x00") : [targetId];
|
|
2227
2285
|
|
|
2228
|
-
// Scan the
|
|
2286
|
+
// Scan the stream tail once, bucket events by instance_id for the candidates.
|
|
2287
|
+
// Bounded read: the ledger grows without bound and a whole-file readFileSync
|
|
2288
|
+
// throws past V8's ~512MB string limit. A trace of an agent whose events all
|
|
2289
|
+
// predate the window is reported as truncated rather than crashing.
|
|
2229
2290
|
const streamPath = resolve(root, ".harnery", "events.ndjson");
|
|
2230
2291
|
const byId = new Map<string, CanonicalEvent[]>();
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2292
|
+
const { text: streamText, truncated: streamTruncated } = readStreamTailBounded(
|
|
2293
|
+
streamPath,
|
|
2294
|
+
STREAM_SCAN_CAP_BYTES,
|
|
2295
|
+
);
|
|
2296
|
+
for (const line of streamText.split("\n")) {
|
|
2297
|
+
if (!line) continue;
|
|
2298
|
+
try {
|
|
2299
|
+
const ev = JSON.parse(line) as CanonicalEvent;
|
|
2300
|
+
if (!ev.instance_id || !candidateIds.includes(ev.instance_id)) continue;
|
|
2301
|
+
if (sinceMs && Date.parse(ev.ts) < sinceMs) continue;
|
|
2302
|
+
const arr = byId.get(ev.instance_id) ?? [];
|
|
2303
|
+
arr.push(ev);
|
|
2304
|
+
byId.set(ev.instance_id, arr);
|
|
2305
|
+
} catch {
|
|
2306
|
+
/* skip */
|
|
2244
2307
|
}
|
|
2245
2308
|
}
|
|
2309
|
+
if (streamTruncated) {
|
|
2310
|
+
process.stderr.write(
|
|
2311
|
+
`note: event ledger exceeds ${Math.round(STREAM_SCAN_CAP_BYTES / 1024 / 1024)}MB; traced only the most recent window (older events omitted)\n`,
|
|
2312
|
+
);
|
|
2313
|
+
}
|
|
2246
2314
|
|
|
2247
2315
|
// If the name mapped to multiple instances, trace the one with the latest event.
|
|
2248
2316
|
let resolvedId = candidateIds[0]!;
|
|
@@ -2629,7 +2697,7 @@ function renderHealthBox(report: HealthReport): void {
|
|
|
2629
2697
|
const localTime = formatLocalTime(new Date(report.generated_at));
|
|
2630
2698
|
const title = `Coord Health (${localTime})`;
|
|
2631
2699
|
|
|
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 +
|
|
2700
|
+
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
2701
|
|
|
2634
2702
|
if (report.anomalies.length > 0) {
|
|
2635
2703
|
process.stdout.write("\n"); // lint-ok-emission: same chat-paste path
|
|
@@ -2686,7 +2754,7 @@ function runHarnessProbe(
|
|
|
2686
2754
|
// and `resolveOwner` here, so the probe reports exactly what the live hot
|
|
2687
2755
|
// path resolves.
|
|
2688
2756
|
const anchorTokens = new Set(["claude", "claude-code", "cursor", "codex"]);
|
|
2689
|
-
const override = process.env.
|
|
2757
|
+
const override = process.env.HARNERY_AGENT_COORD_TEST_ANCHOR_PID;
|
|
2690
2758
|
let anchorPid = override && Number(override) > 0 ? override : "";
|
|
2691
2759
|
const chainParts: string[] = [];
|
|
2692
2760
|
let walkPid = process.pid;
|
|
@@ -2795,12 +2863,12 @@ function runHarnessProbe(
|
|
|
2795
2863
|
* dispatcher in an isolated sandbox.
|
|
2796
2864
|
*
|
|
2797
2865
|
* Sandbox isolation strategy:
|
|
2798
|
-
* - mkdtempSync(tmpdir(), "
|
|
2866
|
+
* - mkdtempSync(tmpdir(), "harn-harness-probe-") creates a non-git tmp dir.
|
|
2799
2867
|
* - The dispatcher's coord-root resolution falls back to
|
|
2800
|
-
* `
|
|
2868
|
+
* `HARNERY_COORD_ROOT_OVERRIDE` when git rev-parse fails. We set it to the sandbox.
|
|
2801
2869
|
* - We rewrite the payload's `cwd` field (Cursor cds to it) to the sandbox,
|
|
2802
2870
|
* so real `.harnery/` never gets touched.
|
|
2803
|
-
* - We set `
|
|
2871
|
+
* - We set `HARNERY_AGENT_COORD_OFF=0` explicitly so any user-side off-switch in
|
|
2804
2872
|
* the environment doesn't mask adapter crashes.
|
|
2805
2873
|
*
|
|
2806
2874
|
* Sample shape: probe-meta wrapped (`_probe_meta.event` + `.payload`) OR bare
|
|
@@ -2880,7 +2948,7 @@ function replayHarnessSamples(
|
|
|
2880
2948
|
};
|
|
2881
2949
|
const harnessFlag = harness === "claude_code" ? "claude-code" : harness;
|
|
2882
2950
|
|
|
2883
|
-
const sandbox = mkdtempSync(join(tmpdir(), "
|
|
2951
|
+
const sandbox = mkdtempSync(join(tmpdir(), "harn-harness-probe-"));
|
|
2884
2952
|
const results: SampleReplayResult[] = [];
|
|
2885
2953
|
|
|
2886
2954
|
try {
|
|
@@ -2928,10 +2996,10 @@ function replayHarnessSamples(
|
|
|
2928
2996
|
timeout: 10_000,
|
|
2929
2997
|
env: {
|
|
2930
2998
|
...process.env,
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2999
|
+
HARNERY_COORD_ROOT_OVERRIDE: sandbox,
|
|
3000
|
+
HARNERY_AGENT_COORD_HARNESS: harness,
|
|
3001
|
+
HARNERY_AGENT_COORD_PLATFORM: harness,
|
|
3002
|
+
HARNERY_AGENT_COORD_OFF: "0",
|
|
2935
3003
|
},
|
|
2936
3004
|
});
|
|
2937
3005
|
|
|
@@ -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: "
|
|
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),
|
package/src/commands/browse.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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.
|
|
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.
|
|
348
|
-
const annotateOverflow = overflow && opts.
|
|
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.
|
|
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;
|