harnery 0.31.6 → 0.32.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/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +2 -0
- package/dist/commands/agents.d.ts +0 -9
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +70 -95
- package/dist/commands/browse-session.d.ts +21 -0
- package/dist/commands/browse-session.d.ts.map +1 -0
- package/dist/commands/browse-session.js +157 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +205 -37
- package/dist/commands/checkpoint.js +1 -1
- package/dist/commands/deinit.d.ts.map +1 -1
- package/dist/commands/deinit.js +4 -0
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +40 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +100 -17
- package/dist/commands/init.d.ts +1 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +16 -5
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +2 -3
- package/dist/core/agents/canonical-emit.d.ts +1 -2
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +1 -2
- package/dist/core/agents/cli.js +123 -88
- package/dist/core/agents/coord-client.d.ts +7 -3
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +5 -4
- package/dist/core/agents/finalization.d.ts +68 -0
- package/dist/core/agents/finalization.d.ts.map +1 -0
- package/dist/core/agents/finalization.js +443 -0
- package/dist/core/agents/git-hook.d.ts +51 -0
- package/dist/core/agents/git-hook.d.ts.map +1 -0
- package/dist/core/agents/git-hook.js +118 -0
- package/dist/core/agents/render/prompt-context.d.ts +6 -5
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +26 -13
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +15 -2
- package/dist/core/agents/rules/claim-conflict.js +3 -3
- package/dist/core/agents/rules/commit-conflict.d.ts +15 -6
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +21 -5
- package/dist/core/agents/rules/stop-hook.d.ts +3 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +59 -23
- package/dist/core/agents/session-events.d.ts +8 -16
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +12 -26
- package/dist/core/agents/state/heartbeat-projector.d.ts +3 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +44 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +32 -5
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +54 -17
- package/dist/core/agents/state/names.d.ts +22 -1
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +40 -2
- package/dist/core/config.d.ts +30 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +74 -11
- package/dist/core/governor/planning.d.ts.map +1 -1
- package/dist/core/governor/planning.js +12 -13
- package/dist/core/hooks/adapter/detect.d.ts +2 -4
- package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
- package/dist/core/hooks/adapter/detect.js +4 -14
- package/dist/core/hooks/adapter/output.d.ts +1 -1
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +5 -4
- package/dist/core/hooks/adapter/wiring.d.ts +23 -0
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +32 -0
- package/dist/core/hooks/cli.d.ts +1 -2
- package/dist/core/hooks/cli.d.ts.map +1 -1
- package/dist/core/hooks/cli.js +169 -27
- package/dist/core/hooks/codex-wsl-bridge.d.ts +46 -0
- package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -0
- package/dist/core/hooks/codex-wsl-bridge.js +137 -0
- package/dist/core/hooks/effects/index.d.ts +1 -1
- package/dist/core/hooks/effects/index.js +3 -3
- package/dist/core/hooks/events/schema.d.ts +27 -0
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.d.ts +7 -1
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +15 -0
- package/dist/core/hooks/resolve/transcript.d.ts +44 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +176 -1
- package/dist/core/hooks/session-name-presence.d.ts +30 -0
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -0
- package/dist/core/hooks/session-name-presence.js +43 -0
- package/dist/core/hooks/unsafe-cross-shell.d.ts +13 -0
- package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -0
- package/dist/core/hooks/unsafe-cross-shell.js +151 -0
- package/dist/core/work/state.d.ts +4 -5
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +5 -8
- package/dist/core/workflow/index.d.ts +1 -1
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/proof.d.ts +2 -2
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +2 -2
- package/dist/core/workflow/run-state.d.ts +2 -2
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +2 -2
- package/dist/core/workflow/types.d.ts +3 -3
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/execution.d.ts +2 -2
- package/dist/core/workflow/workspaces/execution.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/execution.js +5 -0
- package/dist/core/workflow/workspaces/index.d.ts +1 -1
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +1 -2
- package/dist/core/workflow/workspaces/paths.d.ts +2 -2
- package/dist/core/workflow/workspaces/types.d.ts +0 -6
- package/dist/core/workflow/workspaces/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/validate.d.ts +0 -2
- package/dist/core/workflow/workspaces/validate.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/validate.js +0 -2
- package/dist/lib/browser/client.d.ts +118 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +435 -6
- package/dist/lib/browser/geometry.d.ts.map +1 -1
- package/dist/lib/browser/geometry.js +180 -37
- package/dist/lib/browser/index.d.ts +5 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +5 -1
- package/dist/lib/browser/netscape-cookies.d.ts +6 -0
- package/dist/lib/browser/netscape-cookies.d.ts.map +1 -0
- package/dist/lib/browser/netscape-cookies.js +31 -0
- package/dist/lib/browser/proxy.d.ts +24 -0
- package/dist/lib/browser/proxy.d.ts.map +1 -0
- package/dist/lib/browser/proxy.js +84 -0
- package/dist/lib/browser/runts.d.ts +6 -0
- package/dist/lib/browser/runts.d.ts.map +1 -1
- package/dist/lib/browser/runts.js +21 -4
- package/dist/lib/browser/session-control.d.ts +112 -0
- package/dist/lib/browser/session-control.d.ts.map +1 -0
- package/dist/lib/browser/session-control.js +670 -0
- package/dist/lib/docs-links.d.ts +108 -0
- package/dist/lib/docs-links.d.ts.map +1 -0
- package/dist/lib/docs-links.js +555 -0
- package/dist/lib/exec.d.ts +1 -1
- package/dist/lib/exec.js +1 -1
- package/dist/lib/identities/assume.d.ts +4 -2
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +15 -2
- package/dist/lib/instructions/git-hooks.d.ts +75 -0
- package/dist/lib/instructions/git-hooks.d.ts.map +1 -0
- package/dist/lib/instructions/git-hooks.js +238 -0
- package/dist/lib/instructions/splice.d.ts +10 -4
- package/dist/lib/instructions/splice.d.ts.map +1 -1
- package/dist/lib/instructions/splice.js +19 -11
- package/package.json +1 -1
- package/schemas/config.schema.json +32 -0
- package/src/commander.ts +2 -0
- package/src/commands/agents.ts +96 -115
- package/src/commands/browse-session.ts +245 -0
- package/src/commands/browse.ts +287 -44
- package/src/commands/checkpoint.ts +1 -1
- package/src/commands/deinit.ts +7 -1
- package/src/commands/docs.ts +56 -0
- package/src/commands/doctor.ts +100 -18
- package/src/commands/init.ts +17 -9
- package/src/commands/tunnel.ts +2 -5
- package/src/core/agents/canonical-emit.ts +1 -2
- package/src/core/agents/cli.ts +137 -88
- package/src/core/agents/coord-client.ts +10 -5
- package/src/core/agents/finalization.ts +595 -0
- package/src/core/agents/git-hook.ts +126 -0
- package/src/core/agents/render/prompt-context.ts +28 -13
- package/src/core/agents/render/session-context.ts +15 -2
- package/src/core/agents/rules/claim-conflict.ts +3 -3
- package/src/core/agents/rules/commit-conflict.ts +35 -8
- package/src/core/agents/rules/stop-hook.ts +71 -23
- package/src/core/agents/session-events.ts +14 -41
- package/src/core/agents/state/heartbeat-projector.ts +45 -1
- package/src/core/agents/state/heartbeat-writer.ts +78 -16
- package/src/core/agents/state/names.ts +54 -2
- package/src/core/config.ts +92 -12
- package/src/core/governor/planning.ts +17 -12
- package/src/core/hooks/adapter/detect.ts +4 -13
- package/src/core/hooks/adapter/output.ts +9 -4
- package/src/core/hooks/adapter/wiring.ts +43 -0
- package/src/core/hooks/cli.ts +201 -23
- package/src/core/hooks/codex-wsl-bridge.ts +188 -0
- package/src/core/hooks/effects/index.ts +5 -5
- package/src/core/hooks/events/schema.ts +27 -0
- package/src/core/hooks/resolve/owner.ts +22 -1
- package/src/core/hooks/resolve/transcript.ts +164 -1
- package/src/core/hooks/session-name-presence.ts +52 -0
- package/src/core/hooks/unsafe-cross-shell.ts +160 -0
- package/src/core/work/state.ts +8 -12
- package/src/core/workflow/engine.ts +2 -2
- package/src/core/workflow/index.ts +0 -1
- package/src/core/workflow/proof.ts +4 -4
- package/src/core/workflow/run-state.ts +5 -5
- package/src/core/workflow/types.ts +3 -3
- package/src/core/workflow/workspaces/execution.ts +11 -4
- package/src/core/workflow/workspaces/index.ts +0 -3
- package/src/core/workflow/workspaces/local-git.ts +1 -2
- package/src/core/workflow/workspaces/paths.ts +2 -2
- package/src/core/workflow/workspaces/types.ts +0 -9
- package/src/core/workflow/workspaces/validate.ts +1 -5
- package/src/lib/browser/client.ts +528 -6
- package/src/lib/browser/geometry.ts +197 -37
- package/src/lib/browser/index.ts +39 -0
- package/src/lib/browser/netscape-cookies.ts +39 -0
- package/src/lib/browser/proxy.ts +105 -0
- package/src/lib/browser/runts.ts +27 -3
- package/src/lib/browser/session-control.ts +892 -0
- package/src/lib/docs-links.ts +674 -0
- package/src/lib/exec.ts +1 -1
- package/src/lib/identities/assume.ts +22 -1
- package/src/lib/instructions/git-hooks.ts +259 -0
- package/src/lib/instructions/splice.ts +41 -11
package/src/commands/agents.ts
CHANGED
|
@@ -30,6 +30,12 @@ import type { Command } from "commander";
|
|
|
30
30
|
import type { EmitContext, HarneryProgramContext } from "../commander.ts";
|
|
31
31
|
import { coordBinPath } from "../core/agents/coord-bin.ts";
|
|
32
32
|
import { readStreamTailBounded } from "../core/agents/events/consume.ts";
|
|
33
|
+
import {
|
|
34
|
+
checkGitFinalization,
|
|
35
|
+
formatGitFinalizationFailure,
|
|
36
|
+
type GitFinalizationResult,
|
|
37
|
+
readSessionWriteClaims,
|
|
38
|
+
} from "../core/agents/finalization.ts";
|
|
33
39
|
import {
|
|
34
40
|
emitCanonical,
|
|
35
41
|
type Heartbeat,
|
|
@@ -40,6 +46,7 @@ import {
|
|
|
40
46
|
resolveOwnerBySessionEnv,
|
|
41
47
|
resolveOwnerWithSource,
|
|
42
48
|
} from "../core/agents/index.ts";
|
|
49
|
+
import { buildSuggestedName } from "../core/agents/state/heartbeat-writer.ts";
|
|
43
50
|
import { coordFreshnessSeconds, resolveBinName } from "../core/config.ts";
|
|
44
51
|
import { type RemoteMachine, readRemoteMachines } from "../core/presence/index.ts";
|
|
45
52
|
import { registerContextCommand } from "./context.ts";
|
|
@@ -221,13 +228,17 @@ export function registerAgentsCommand(
|
|
|
221
228
|
.command("status")
|
|
222
229
|
.description("End-of-turn status box (name + session age + files held + peer count)")
|
|
223
230
|
.option("--json", "JSON output instead of the box")
|
|
231
|
+
.option(
|
|
232
|
+
"--end-turn",
|
|
233
|
+
"Treat this as the turn's closing status: issue the box only when this session's held paths are committed and their repositories are pushed",
|
|
234
|
+
)
|
|
224
235
|
.option(
|
|
225
236
|
"--session-id <id>",
|
|
226
237
|
"Lookup heartbeat by session_id directly, bypassing the ppid walk. " +
|
|
227
238
|
"Use this when calling from a hook (the hook's process tree may not lead back to Claude Code's session pid). " +
|
|
228
239
|
"The Stop hook payload includes session_id; pass it through.",
|
|
229
240
|
)
|
|
230
|
-
.action((opts: { json?: boolean; sessionId?: string }) => {
|
|
241
|
+
.action((opts: { endTurn?: boolean; json?: boolean; sessionId?: string }) => {
|
|
231
242
|
runStatus(opts);
|
|
232
243
|
});
|
|
233
244
|
|
|
@@ -343,7 +354,7 @@ export function registerAgentsCommand(
|
|
|
343
354
|
)
|
|
344
355
|
.option(
|
|
345
356
|
"--timeout <dur>",
|
|
346
|
-
"Give up after this duration; suffix s/m/h/d (e.g. 30s, 5m, 1h).
|
|
357
|
+
"Give up after this duration; suffix s/m/h/d is required (e.g. 30s, 5m, 1h). Default 60m.",
|
|
347
358
|
"60m",
|
|
348
359
|
)
|
|
349
360
|
.option("--poll-secs <n>", "Poll interval in seconds (default 5)", "5")
|
|
@@ -381,8 +392,8 @@ export function registerAgentsCommand(
|
|
|
381
392
|
cmd
|
|
382
393
|
.command("health")
|
|
383
394
|
.description(
|
|
384
|
-
"One-screen coord-layer health rollup: heal events,
|
|
385
|
-
"
|
|
395
|
+
"One-screen coord-layer health rollup: heal events, council activity, " +
|
|
396
|
+
"zombie detection, and anomalies. Designed for " +
|
|
386
397
|
"daily glance + dashboard ingestion. Reads .harnery/.",
|
|
387
398
|
)
|
|
388
399
|
.option("--since <window>", "Window (Nh | Nd). Default: 24h.", "24h")
|
|
@@ -397,7 +408,7 @@ export function registerAgentsCommand(
|
|
|
397
408
|
"Adapter wiring probe: ppid chain, comm names, pid-map anchor, sample payload paths. " +
|
|
398
409
|
"With --replay-samples, also replays every checked-in sample payload against the live " +
|
|
399
410
|
"adapter in an isolated sandbox to catch adapter / payload-shape drift. " +
|
|
400
|
-
"Complements heal-events (drift telemetry). Id:
|
|
411
|
+
"Complements heal-events (drift telemetry). Id: claude-code | cursor.",
|
|
401
412
|
)
|
|
402
413
|
.option("--json", "JSON envelope output")
|
|
403
414
|
.option(
|
|
@@ -552,12 +563,23 @@ function registerIdentityCommands(parent: Command): void {
|
|
|
552
563
|
"--session-id <id>",
|
|
553
564
|
"Bind the heartbeat with this session_id directly, bypassing the ppid walk.",
|
|
554
565
|
)
|
|
555
|
-
.
|
|
556
|
-
|
|
557
|
-
|
|
566
|
+
.option(
|
|
567
|
+
"--force-ancestor",
|
|
568
|
+
"Allow assuming a persona this session's fork lineage descends from " +
|
|
569
|
+
"(default: refused, because a fork's inherited transcript asserting that " +
|
|
570
|
+
"name is not a role handoff).",
|
|
571
|
+
)
|
|
572
|
+
.action(
|
|
573
|
+
(target: string, opts: { json?: boolean; sessionId?: string; forceAncestor?: boolean }) => {
|
|
574
|
+
runIdentityAssume(target, opts);
|
|
575
|
+
},
|
|
576
|
+
);
|
|
558
577
|
}
|
|
559
578
|
|
|
560
|
-
function runIdentityAssume(
|
|
579
|
+
function runIdentityAssume(
|
|
580
|
+
target: string,
|
|
581
|
+
opts: { json?: boolean; sessionId?: string; forceAncestor?: boolean },
|
|
582
|
+
): void {
|
|
561
583
|
if (opts.json) emit.config({ format: "json" });
|
|
562
584
|
const root = monorepoRoot();
|
|
563
585
|
if (!root) {
|
|
@@ -578,7 +600,9 @@ function runIdentityAssume(target: string, opts: { json?: boolean; sessionId?: s
|
|
|
578
600
|
process.exit(1);
|
|
579
601
|
}
|
|
580
602
|
try {
|
|
581
|
-
const result = assumeIdentity(root, owner, target
|
|
603
|
+
const result = assumeIdentity(root, owner, target, {
|
|
604
|
+
forceAncestor: opts.forceAncestor ?? false,
|
|
605
|
+
});
|
|
582
606
|
emit.data({
|
|
583
607
|
...result,
|
|
584
608
|
display_name: displayAgentName(result.name),
|
|
@@ -879,7 +903,7 @@ function runWhoami(opts: { json?: boolean }): void {
|
|
|
879
903
|
task: hb.task ?? null,
|
|
880
904
|
turn_summary: hb.turn_summary ?? null,
|
|
881
905
|
turn_summary_updated_at: hb.turn_summary_updated_at ?? null,
|
|
882
|
-
platform: hb.platform ?? "
|
|
906
|
+
platform: hb.platform ?? "claude-code",
|
|
883
907
|
};
|
|
884
908
|
|
|
885
909
|
emit.data({ ...row, resolution_source: resolved.source, note: SUBAGENT_NOTE });
|
|
@@ -977,7 +1001,7 @@ function runList(opts: { all?: boolean; stale?: boolean; json?: boolean }): void
|
|
|
977
1001
|
task: h.task ?? null,
|
|
978
1002
|
turn_summary: h.turn_summary ?? null,
|
|
979
1003
|
turn_summary_updated_at: h.turn_summary_updated_at ?? null,
|
|
980
|
-
platform: h.platform ?? "
|
|
1004
|
+
platform: h.platform ?? "claude-code",
|
|
981
1005
|
});
|
|
982
1006
|
continue;
|
|
983
1007
|
}
|
|
@@ -1002,7 +1026,7 @@ function runList(opts: { all?: boolean; stale?: boolean; json?: boolean }): void
|
|
|
1002
1026
|
task: h.task ?? null,
|
|
1003
1027
|
turn_summary: h.turn_summary ?? null,
|
|
1004
1028
|
turn_summary_updated_at: h.turn_summary_updated_at ?? null,
|
|
1005
|
-
platform: h.platform ?? "
|
|
1029
|
+
platform: h.platform ?? "claude-code",
|
|
1006
1030
|
});
|
|
1007
1031
|
}
|
|
1008
1032
|
|
|
@@ -1030,7 +1054,7 @@ function runList(opts: { all?: boolean; stale?: boolean; json?: boolean }): void
|
|
|
1030
1054
|
task: a.task ?? null,
|
|
1031
1055
|
turn_summary: a.turn_summary ?? null,
|
|
1032
1056
|
turn_summary_updated_at: null,
|
|
1033
|
-
platform: a.platform ?? "
|
|
1057
|
+
platform: a.platform ?? "claude-code",
|
|
1034
1058
|
machine: rm.machine,
|
|
1035
1059
|
});
|
|
1036
1060
|
}
|
|
@@ -1495,15 +1519,13 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
|
|
|
1495
1519
|
process.exit(1);
|
|
1496
1520
|
}
|
|
1497
1521
|
|
|
1498
|
-
// Snapshot BEFORE the mutation:
|
|
1499
|
-
//
|
|
1500
|
-
//
|
|
1501
|
-
//
|
|
1502
|
-
//
|
|
1503
|
-
//
|
|
1504
|
-
// (a deliberate clear), which carries a stamp and does NOT re-trigger.
|
|
1522
|
+
// Snapshot BEFORE the mutation: the writer stamps `suggested_session_name`
|
|
1523
|
+
// on the session's first NON-EMPTY declaration (one source of truth: the
|
|
1524
|
+
// focus you declare names the session). A bare clear never consumes the
|
|
1525
|
+
// naming window, and subagent/workflow kinds are never named — see
|
|
1526
|
+
// heartbeat-writer.setTask. This call is the naming call exactly when the
|
|
1527
|
+
// stamp appears across the mutation.
|
|
1505
1528
|
const priorHb = readHeartbeat(myOwner);
|
|
1506
|
-
const firstOfSession = !priorHb?.task_updated_at;
|
|
1507
1529
|
|
|
1508
1530
|
// Heartbeat mutation goes through agent-coord (atomic temp+rename).
|
|
1509
1531
|
const helper = agentCoordOrExit(root);
|
|
@@ -1520,46 +1542,47 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
|
|
|
1520
1542
|
}
|
|
1521
1543
|
|
|
1522
1544
|
const hb = readHeartbeat(myOwner);
|
|
1545
|
+
|
|
1546
|
+
// The naming call: the mutation just stamped `suggested_session_name` for
|
|
1547
|
+
// the first time. `first_of_session: true` means exactly "this call produced
|
|
1548
|
+
// the session name" — it is never true with a null name (an empty first
|
|
1549
|
+
// declaration or a `--session-id` relay both keep the window open instead).
|
|
1550
|
+
const suggestedName =
|
|
1551
|
+
!priorHb?.suggested_session_name && hb?.suggested_session_name
|
|
1552
|
+
? hb.suggested_session_name
|
|
1553
|
+
: null;
|
|
1554
|
+
const firstOfSession = suggestedName !== null;
|
|
1555
|
+
|
|
1523
1556
|
emitCanonical({
|
|
1524
1557
|
type: "state.task_set",
|
|
1525
1558
|
owner: myOwner,
|
|
1526
1559
|
session: hb?.session_id ?? myOwner,
|
|
1527
1560
|
adapter: normalizeAdapter(hb?.platform),
|
|
1528
|
-
data: {
|
|
1561
|
+
data: {
|
|
1562
|
+
task,
|
|
1563
|
+
cleared: !task || task.length === 0,
|
|
1564
|
+
first_of_session: firstOfSession,
|
|
1565
|
+
suggested_session_name: suggestedName,
|
|
1566
|
+
},
|
|
1529
1567
|
});
|
|
1530
1568
|
|
|
1531
|
-
// On the first focus declaration of a session, surface a suggested session
|
|
1532
|
-
// name for the operator to set as their adapter tab title. Read the result
|
|
1533
|
-
// (`suggested_session_name`) and reproduce it in a fenced code block so the
|
|
1534
|
-
// UI's Copy button hands over the exact string. null on every later call.
|
|
1535
|
-
const built =
|
|
1536
|
-
firstOfSession && !opts?.sessionId ? buildSuggestedName(hb?.name ?? "unknown", [task]) : null;
|
|
1537
|
-
|
|
1538
1569
|
emit.data({
|
|
1539
1570
|
instance_id: myOwner,
|
|
1540
1571
|
name: hb?.name ?? null,
|
|
1541
1572
|
task: hb?.task ?? null,
|
|
1542
1573
|
cleared: !task || task.length === 0,
|
|
1543
1574
|
first_of_session: firstOfSession,
|
|
1544
|
-
suggested_session_name:
|
|
1575
|
+
suggested_session_name: suggestedName,
|
|
1576
|
+
// Right-time instruction: the UserPromptSubmit nudge fires before the name
|
|
1577
|
+
// exists; this result is the moment the model holds the string.
|
|
1578
|
+
...(suggestedName
|
|
1579
|
+
? {
|
|
1580
|
+
note: "Reproduce suggested_session_name verbatim, by itself, in a fenced code block at the top of your reply — the operator copies it as the session/tab title.",
|
|
1581
|
+
}
|
|
1582
|
+
: {}),
|
|
1545
1583
|
});
|
|
1546
1584
|
}
|
|
1547
1585
|
|
|
1548
|
-
/**
|
|
1549
|
-
* Build the copy-pasteable session name from the coord identity + the agent's
|
|
1550
|
-
* description parts. Pure (no coord-state reads) so it's unit-testable; collapses
|
|
1551
|
-
* internal whitespace and trims. Returns null when the description is empty.
|
|
1552
|
-
*/
|
|
1553
|
-
export function buildSuggestedName(
|
|
1554
|
-
agentName: string,
|
|
1555
|
-
descriptionParts: string[],
|
|
1556
|
-
): { suggestedName: string; description: string } | null {
|
|
1557
|
-
const description = descriptionParts.join(" ").replace(/\s+/g, " ").trim();
|
|
1558
|
-
if (!description) return null;
|
|
1559
|
-
const name = agentName?.trim() || "unknown";
|
|
1560
|
-
return { suggestedName: `Agent ${name} - ${description}`, description };
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
1586
|
function runSuggestName(
|
|
1564
1587
|
descriptionParts: string[],
|
|
1565
1588
|
opts: { json?: boolean; sessionId?: string },
|
|
@@ -1624,7 +1647,7 @@ function runSuggestName(
|
|
|
1624
1647
|
process.stdout.write(`${suggestedName}\n`); // lint-ok-emission: chat-paste path
|
|
1625
1648
|
}
|
|
1626
1649
|
|
|
1627
|
-
function runStatus(opts: { json?: boolean; sessionId?: string }): void {
|
|
1650
|
+
function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string }): void {
|
|
1628
1651
|
const root = monorepoRoot();
|
|
1629
1652
|
if (!root) {
|
|
1630
1653
|
emit.error({
|
|
@@ -1656,20 +1679,23 @@ function runStatus(opts: { json?: boolean; sessionId?: string }): void {
|
|
|
1656
1679
|
process.exit(1);
|
|
1657
1680
|
}
|
|
1658
1681
|
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
spawnSync(helper, ["stamp-status-call", myOwner], {
|
|
1666
|
-
encoding: "utf8",
|
|
1667
|
-
timeout: 2000,
|
|
1668
|
-
...coordHelperOpts(root),
|
|
1682
|
+
let finalization: GitFinalizationResult | null = null;
|
|
1683
|
+
if (opts.endTurn) {
|
|
1684
|
+
const history = readSessionWriteClaims(root, hb.instance_id, hb.session_id ?? myOwner);
|
|
1685
|
+
const touchedPaths = [...new Set([...(hb.files_touched ?? []), ...history.paths])];
|
|
1686
|
+
finalization = checkGitFinalization(root, touchedPaths, {
|
|
1687
|
+
claimHistoryComplete: history.complete,
|
|
1669
1688
|
});
|
|
1670
|
-
|
|
1671
|
-
|
|
1689
|
+
if (!finalization.ok) {
|
|
1690
|
+
emit.error({
|
|
1691
|
+
code: "git_not_finalized",
|
|
1692
|
+
message: formatGitFinalizationFailure(finalization, resolveBinName(root)),
|
|
1693
|
+
});
|
|
1694
|
+
process.exitCode = 1;
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1672
1697
|
}
|
|
1698
|
+
|
|
1673
1699
|
emitCanonical({
|
|
1674
1700
|
type: "state.status_checked",
|
|
1675
1701
|
owner: myOwner,
|
|
@@ -1677,6 +1703,7 @@ function runStatus(opts: { json?: boolean; sessionId?: string }): void {
|
|
|
1677
1703
|
adapter: normalizeAdapter(hb.platform),
|
|
1678
1704
|
data: {
|
|
1679
1705
|
format: opts.json ? "json" : "box",
|
|
1706
|
+
git_finalization_checked: opts.endTurn === true,
|
|
1680
1707
|
agent_count: 0, // computed below, not yet available here; Phase 5 verdict reads owner-scope only
|
|
1681
1708
|
included_self: true,
|
|
1682
1709
|
},
|
|
@@ -1784,6 +1811,7 @@ function runStatus(opts: { json?: boolean; sessionId?: string }): void {
|
|
|
1784
1811
|
context_window: ctxUsage?.window ?? null,
|
|
1785
1812
|
timestamp_iso: new Date().toISOString(),
|
|
1786
1813
|
timestamp_local: timeStr,
|
|
1814
|
+
...(finalization ? { finalization } : {}),
|
|
1787
1815
|
};
|
|
1788
1816
|
|
|
1789
1817
|
if (opts.json) {
|
|
@@ -2154,13 +2182,12 @@ function readCanonicalEventsInWindow(root: string, cutoffMs: number): CanonicalE
|
|
|
2154
2182
|
return out;
|
|
2155
2183
|
}
|
|
2156
2184
|
|
|
2157
|
-
/** adapter
|
|
2158
|
-
* existing formatPlatformLabel rendering keeps working unchanged. */
|
|
2185
|
+
/** Normalize adapter event data into the heartbeat platform value. */
|
|
2159
2186
|
function adapterToPlatform(adapter: string | undefined): string {
|
|
2160
|
-
if (adapter === "claude-code") return "
|
|
2187
|
+
if (adapter === "claude-code") return "claude-code";
|
|
2161
2188
|
if (adapter === "cursor") return "cursor";
|
|
2162
2189
|
if (adapter === "codex") return "codex";
|
|
2163
|
-
return "
|
|
2190
|
+
return "claude-code";
|
|
2164
2191
|
}
|
|
2165
2192
|
|
|
2166
2193
|
/** Project a canonical health.* event into the HealEvent shape the aggregators
|
|
@@ -2387,14 +2414,6 @@ interface HealthReport {
|
|
|
2387
2414
|
by_platform: Record<string, number>;
|
|
2388
2415
|
top_agents: Array<{ agent: string; count: number }>;
|
|
2389
2416
|
};
|
|
2390
|
-
schema_invalid: { count: number; samples: string[] };
|
|
2391
|
-
commit_guards: {
|
|
2392
|
-
blocked: number;
|
|
2393
|
-
bypassed: number;
|
|
2394
|
-
suppressed: number;
|
|
2395
|
-
edit_blocked: number;
|
|
2396
|
-
shell_candidates: number;
|
|
2397
|
-
};
|
|
2398
2417
|
councils: {
|
|
2399
2418
|
active: number;
|
|
2400
2419
|
archived_in_window: number;
|
|
@@ -2736,18 +2755,7 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
2736
2755
|
|
|
2737
2756
|
// Coordination telemetry reads from the canonical events.ndjson stream.
|
|
2738
2757
|
// Heals come from health.*; council window-activity from council.*. The
|
|
2739
|
-
// commit-guard + schema-invalid counters below have NO canonical equivalent
|
|
2740
|
-
// yet. Their future home is the decision.* events (defined in schema.ts, not
|
|
2741
|
-
// yet wired). Until then they report 0 (fields kept for output-shape
|
|
2742
|
-
// compatibility).
|
|
2743
2758
|
const heal: HealEvent[] = [];
|
|
2744
|
-
const schemaInvalid = 0;
|
|
2745
|
-
const schemaSamples: string[] = [];
|
|
2746
|
-
const commitBlocked = 0;
|
|
2747
|
-
const commitBypassed = 0;
|
|
2748
|
-
const commitSuppressed = 0;
|
|
2749
|
-
const editBlocked = 0;
|
|
2750
|
-
const shellCandidates = 0;
|
|
2751
2759
|
let councilAdvanced = 0;
|
|
2752
2760
|
let councilClosed = 0;
|
|
2753
2761
|
let councilArchived = 0;
|
|
@@ -2865,12 +2873,6 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
2865
2873
|
|
|
2866
2874
|
// Anomaly detection.
|
|
2867
2875
|
const anomalies: string[] = [];
|
|
2868
|
-
if (schemaInvalid > 0) {
|
|
2869
|
-
const sampleList = schemaSamples.slice(0, 3).join(", ");
|
|
2870
|
-
anomalies.push(
|
|
2871
|
-
`HEARTBEAT_SCHEMA_INVALID fired ${schemaInvalid}x; heartbeat shape failed validation${sampleList ? ` (samples: ${sampleList})` : ""}`,
|
|
2872
|
-
);
|
|
2873
|
-
}
|
|
2874
2876
|
for (const { agent, count } of healTopAgents) {
|
|
2875
2877
|
if (count >= 5) {
|
|
2876
2878
|
anomalies.push(
|
|
@@ -2937,14 +2939,6 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
2937
2939
|
by_platform: healByPlatform,
|
|
2938
2940
|
top_agents: healTopAgents,
|
|
2939
2941
|
},
|
|
2940
|
-
schema_invalid: { count: schemaInvalid, samples: schemaSamples },
|
|
2941
|
-
commit_guards: {
|
|
2942
|
-
blocked: commitBlocked,
|
|
2943
|
-
bypassed: commitBypassed,
|
|
2944
|
-
suppressed: commitSuppressed,
|
|
2945
|
-
edit_blocked: editBlocked,
|
|
2946
|
-
shell_candidates: shellCandidates,
|
|
2947
|
-
},
|
|
2948
2942
|
councils: {
|
|
2949
2943
|
active: activeCouncils,
|
|
2950
2944
|
archived_in_window: councilArchived,
|
|
@@ -2988,17 +2982,6 @@ function renderHealthBox(report: HealthReport): void {
|
|
|
2988
2982
|
const topHealer = report.heal_events.top_agents[0];
|
|
2989
2983
|
const topHealerStr = topHealer ? `${topHealer.agent} x${topHealer.count}` : "(none)";
|
|
2990
2984
|
|
|
2991
|
-
const guardParts: string[] = [];
|
|
2992
|
-
if (report.commit_guards.blocked > 0) guardParts.push(`blocked ${report.commit_guards.blocked}`);
|
|
2993
|
-
if (report.commit_guards.bypassed > 0)
|
|
2994
|
-
guardParts.push(`bypassed ${report.commit_guards.bypassed}`);
|
|
2995
|
-
if (report.commit_guards.suppressed > 0)
|
|
2996
|
-
guardParts.push(`suppressed ${report.commit_guards.suppressed}`);
|
|
2997
|
-
if (report.commit_guards.edit_blocked > 0)
|
|
2998
|
-
guardParts.push(`edit-blocked ${report.commit_guards.edit_blocked}`);
|
|
2999
|
-
if (report.commit_guards.shell_candidates > 0)
|
|
3000
|
-
guardParts.push(`shell ${report.commit_guards.shell_candidates}`);
|
|
3001
|
-
|
|
3002
2985
|
const councilParts: string[] = [`${report.councils.active} active`];
|
|
3003
2986
|
if (report.councils.advanced_in_window > 0)
|
|
3004
2987
|
councilParts.push(`${report.councils.advanced_in_window} advanced`);
|
|
@@ -3035,8 +3018,6 @@ function renderHealthBox(report: HealthReport): void {
|
|
|
3035
3018
|
? "0"
|
|
3036
3019
|
: `${report.zombies.count} (${report.zombies.samples.join(", ")})`,
|
|
3037
3020
|
],
|
|
3038
|
-
["schema invalid", String(report.schema_invalid.count)],
|
|
3039
|
-
["commit guards", guardParts.length ? guardParts.join(", ") : "0"],
|
|
3040
3021
|
["councils", councilParts.join(", ")],
|
|
3041
3022
|
["anomalies", report.anomalies.length === 0 ? "(clean)" : `${report.anomalies.length} flagged`],
|
|
3042
3023
|
];
|
|
@@ -3070,10 +3051,10 @@ function runAdapterProbe(
|
|
|
3070
3051
|
if (opts.json) emit.config({ format: "json" });
|
|
3071
3052
|
|
|
3072
3053
|
const adapter = id.trim();
|
|
3073
|
-
if (adapter !== "
|
|
3054
|
+
if (adapter !== "claude-code" && adapter !== "cursor") {
|
|
3074
3055
|
emit.error({
|
|
3075
3056
|
code: "bad_adapter",
|
|
3076
|
-
message: "adapter id must be
|
|
3057
|
+
message: "adapter id must be claude-code or cursor",
|
|
3077
3058
|
});
|
|
3078
3059
|
process.exit(1);
|
|
3079
3060
|
}
|
|
@@ -3293,7 +3274,7 @@ function replayAdapterSamples(
|
|
|
3293
3274
|
stopFailure: "stop-failure",
|
|
3294
3275
|
StopFailure: "stop-failure",
|
|
3295
3276
|
};
|
|
3296
|
-
const adapterFlag = adapter
|
|
3277
|
+
const adapterFlag = adapter;
|
|
3297
3278
|
|
|
3298
3279
|
const sandbox = mkdtempSync(join(tmpdir(), "harn-adapter-probe-"));
|
|
3299
3280
|
const results: SampleReplayResult[] = [];
|
|
@@ -3430,13 +3411,13 @@ function collectPath(value: string, prev: string[]): string[] {
|
|
|
3430
3411
|
return [...prev, value];
|
|
3431
3412
|
}
|
|
3432
3413
|
|
|
3433
|
-
/** Parse "
|
|
3414
|
+
/** Parse "30s", "5m", "1h", "2d" into milliseconds. */
|
|
3434
3415
|
function parseDurationToMs(input: string): number | null {
|
|
3435
|
-
const match = input.trim().match(/^(\d+)([smhd]
|
|
3416
|
+
const match = input.trim().match(/^(\d+)([smhd])$/i);
|
|
3436
3417
|
if (!match) return null;
|
|
3437
3418
|
const n = Number.parseInt(match[1], 10);
|
|
3438
3419
|
if (!Number.isFinite(n) || n <= 0) return null;
|
|
3439
|
-
const unit =
|
|
3420
|
+
const unit = match[2].toLowerCase();
|
|
3440
3421
|
const mult: Record<string, number> = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 };
|
|
3441
3422
|
return n * mult[unit];
|
|
3442
3423
|
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import type { Readable } from "node:stream";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
import type { EmitContext } from "../commander.ts";
|
|
4
|
+
import type { BrowserSessionLocator } from "../lib/browser/client.ts";
|
|
5
|
+
import {
|
|
6
|
+
BROWSER_SESSION_MAX_FILL_BYTES,
|
|
7
|
+
BrowserSessionError,
|
|
8
|
+
sendBrowserSessionRequest,
|
|
9
|
+
} from "../lib/browser/session-control.ts";
|
|
10
|
+
|
|
11
|
+
interface ControlFileOpts {
|
|
12
|
+
controlFile: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface LocatorOpts extends ControlFileOpts {
|
|
16
|
+
selector?: string;
|
|
17
|
+
role?: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
label?: string;
|
|
20
|
+
text?: string;
|
|
21
|
+
partial?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function registerBrowseSessionCommand(program: Command, emit: EmitContext): void {
|
|
25
|
+
const session = program
|
|
26
|
+
.command("browse-session")
|
|
27
|
+
.description("Control an opted-in headed browse session through an owner-only descriptor");
|
|
28
|
+
|
|
29
|
+
session
|
|
30
|
+
.command("status")
|
|
31
|
+
.requiredOption("--control-file <path>", "Owner-only session descriptor")
|
|
32
|
+
.action((opts: ControlFileOpts) => runAction(emit, opts.controlFile, "status", {}));
|
|
33
|
+
|
|
34
|
+
addLocatorOptions(
|
|
35
|
+
session
|
|
36
|
+
.command("inspect")
|
|
37
|
+
.description("Inspect sanitized text, controls, focus, and active-tab state")
|
|
38
|
+
.requiredOption("--control-file <path>", "Owner-only session descriptor"),
|
|
39
|
+
).action((opts: LocatorOpts) => {
|
|
40
|
+
const locator = parseLocatorOptions(opts, false);
|
|
41
|
+
return runAction(emit, opts.controlFile, "inspect", locator ? { locator } : {});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
session
|
|
45
|
+
.command("screenshot")
|
|
46
|
+
.requiredOption("--control-file <path>", "Owner-only session descriptor")
|
|
47
|
+
.requiredOption("--out <path>", "New PNG path in an existing directory")
|
|
48
|
+
.action((opts: ControlFileOpts & { out: string }) =>
|
|
49
|
+
runAction(emit, opts.controlFile, "screenshot", { out: opts.out }),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
session
|
|
53
|
+
.command("tabs")
|
|
54
|
+
.requiredOption("--control-file <path>", "Owner-only session descriptor")
|
|
55
|
+
.action((opts: ControlFileOpts) => runAction(emit, opts.controlFile, "tabs", {}));
|
|
56
|
+
|
|
57
|
+
session
|
|
58
|
+
.command("select-tab")
|
|
59
|
+
.requiredOption("--control-file <path>", "Owner-only session descriptor")
|
|
60
|
+
.requiredOption("--index <n>", "Stable session-local tab index", parseIndex)
|
|
61
|
+
.action((opts: ControlFileOpts & { index: number }) =>
|
|
62
|
+
runAction(emit, opts.controlFile, "select_tab", { index: opts.index }),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
session
|
|
66
|
+
.command("open-tab <url>")
|
|
67
|
+
.requiredOption("--control-file <path>", "Owner-only session descriptor")
|
|
68
|
+
.action((url: string, opts: ControlFileOpts) =>
|
|
69
|
+
runAction(emit, opts.controlFile, "open_tab", { url }),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
session
|
|
73
|
+
.command("close-tab")
|
|
74
|
+
.requiredOption("--control-file <path>", "Owner-only session descriptor")
|
|
75
|
+
.requiredOption("--index <n>", "Stable session-local tab index", parseIndex)
|
|
76
|
+
.action((opts: ControlFileOpts & { index: number }) =>
|
|
77
|
+
runAction(emit, opts.controlFile, "close_tab", { index: opts.index }),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
session
|
|
81
|
+
.command("goto <url>")
|
|
82
|
+
.requiredOption("--control-file <path>", "Owner-only session descriptor")
|
|
83
|
+
.action((url: string, opts: ControlFileOpts) =>
|
|
84
|
+
runAction(emit, opts.controlFile, "goto", { url }),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
session
|
|
88
|
+
.command("reload")
|
|
89
|
+
.requiredOption("--control-file <path>", "Owner-only session descriptor")
|
|
90
|
+
.action((opts: ControlFileOpts) => runAction(emit, opts.controlFile, "reload", {}));
|
|
91
|
+
|
|
92
|
+
addLocatorOptions(
|
|
93
|
+
session
|
|
94
|
+
.command("click")
|
|
95
|
+
.requiredOption("--control-file <path>", "Owner-only session descriptor"),
|
|
96
|
+
).action((opts: LocatorOpts) =>
|
|
97
|
+
runAction(emit, opts.controlFile, "click", { locator: parseLocatorOptions(opts, true) }),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
addLocatorOptions(
|
|
101
|
+
session
|
|
102
|
+
.command("fill")
|
|
103
|
+
.description("Fill one strict locator with a value read once from standard input")
|
|
104
|
+
.requiredOption("--control-file <path>", "Owner-only session descriptor"),
|
|
105
|
+
).action(async (opts: LocatorOpts) => {
|
|
106
|
+
const value = await readFillValue(process.stdin);
|
|
107
|
+
return runAction(emit, opts.controlFile, "fill", {
|
|
108
|
+
locator: parseLocatorOptions(opts, true),
|
|
109
|
+
value,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
session
|
|
114
|
+
.command("press <key>")
|
|
115
|
+
.requiredOption("--control-file <path>", "Owner-only session descriptor")
|
|
116
|
+
.action((key: string, opts: ControlFileOpts) =>
|
|
117
|
+
runAction(emit, opts.controlFile, "press", { key }),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
addLocatorOptions(
|
|
121
|
+
session
|
|
122
|
+
.command("wait")
|
|
123
|
+
.requiredOption("--control-file <path>", "Owner-only session descriptor"),
|
|
124
|
+
).action((opts: LocatorOpts) =>
|
|
125
|
+
runAction(emit, opts.controlFile, "wait", { locator: parseLocatorOptions(opts, true) }),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
session
|
|
129
|
+
.command("close")
|
|
130
|
+
.requiredOption("--control-file <path>", "Owner-only session descriptor")
|
|
131
|
+
.action((opts: ControlFileOpts) => runAction(emit, opts.controlFile, "close", {}));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function addLocatorOptions(command: Command): Command {
|
|
135
|
+
return command
|
|
136
|
+
.option("--selector <css>", "CSS locator")
|
|
137
|
+
.option("--role <role>", "Accessible role locator")
|
|
138
|
+
.option("--name <name>", "Accessible name used with --role")
|
|
139
|
+
.option("--label <text>", "Associated label locator")
|
|
140
|
+
.option("--text <text>", "Visible text locator")
|
|
141
|
+
.option("--partial", "Allow partial accessible-name, label, or text matching");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function parseLocatorOptions(opts: LocatorOpts, required: true): BrowserSessionLocator;
|
|
145
|
+
export function parseLocatorOptions(
|
|
146
|
+
opts: LocatorOpts,
|
|
147
|
+
required: false,
|
|
148
|
+
): BrowserSessionLocator | undefined;
|
|
149
|
+
export function parseLocatorOptions(
|
|
150
|
+
opts: LocatorOpts,
|
|
151
|
+
required: boolean,
|
|
152
|
+
): BrowserSessionLocator | undefined {
|
|
153
|
+
const forms = [opts.selector, opts.role, opts.label, opts.text].filter(
|
|
154
|
+
(value): value is string => value !== undefined,
|
|
155
|
+
);
|
|
156
|
+
if (forms.length === 0) {
|
|
157
|
+
if (!required) {
|
|
158
|
+
if (opts.name !== undefined || opts.partial) {
|
|
159
|
+
throw new BrowserSessionError(
|
|
160
|
+
"invalid_locator",
|
|
161
|
+
"--name and --partial require a locator form.",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
throw new BrowserSessionError(
|
|
167
|
+
"invalid_locator",
|
|
168
|
+
"Choose exactly one locator: --selector, --role, --label, or --text.",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
if (forms.length !== 1) {
|
|
172
|
+
throw new BrowserSessionError(
|
|
173
|
+
"invalid_locator",
|
|
174
|
+
"Choose exactly one locator: --selector, --role, --label, or --text.",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (opts.name !== undefined && opts.role === undefined) {
|
|
178
|
+
throw new BrowserSessionError("invalid_locator", "--name is valid only with --role.");
|
|
179
|
+
}
|
|
180
|
+
if (opts.selector !== undefined) {
|
|
181
|
+
if (opts.partial) {
|
|
182
|
+
throw new BrowserSessionError("invalid_locator", "--partial is not valid with --selector.");
|
|
183
|
+
}
|
|
184
|
+
return { kind: "selector", value: opts.selector, partial: false };
|
|
185
|
+
}
|
|
186
|
+
if (opts.role !== undefined) {
|
|
187
|
+
return {
|
|
188
|
+
kind: "role",
|
|
189
|
+
value: opts.role,
|
|
190
|
+
...(opts.name === undefined ? {} : { name: opts.name }),
|
|
191
|
+
partial: Boolean(opts.partial),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (opts.label !== undefined) {
|
|
195
|
+
return { kind: "label", value: opts.label, partial: Boolean(opts.partial) };
|
|
196
|
+
}
|
|
197
|
+
return { kind: "text", value: opts.text!, partial: Boolean(opts.partial) };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function readFillValue(input: Readable): Promise<string> {
|
|
201
|
+
const chunks: Buffer[] = [];
|
|
202
|
+
let size = 0;
|
|
203
|
+
for await (const chunk of input) {
|
|
204
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
205
|
+
size += bytes.length;
|
|
206
|
+
if (size > BROWSER_SESSION_MAX_FILL_BYTES) {
|
|
207
|
+
throw new BrowserSessionError("fill_too_large", "Fill input exceeds the 64 KiB limit.");
|
|
208
|
+
}
|
|
209
|
+
chunks.push(bytes);
|
|
210
|
+
}
|
|
211
|
+
const value = Buffer.concat(chunks)
|
|
212
|
+
.toString("utf8")
|
|
213
|
+
.replace(/\r?\n$/, "");
|
|
214
|
+
if (value.length === 0) {
|
|
215
|
+
throw new BrowserSessionError("fill_empty", "Fill input from standard input was empty.");
|
|
216
|
+
}
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function runAction(
|
|
221
|
+
emit: EmitContext,
|
|
222
|
+
controlFile: string,
|
|
223
|
+
action: Parameters<typeof sendBrowserSessionRequest>[1],
|
|
224
|
+
args: Record<string, unknown>,
|
|
225
|
+
): Promise<void> {
|
|
226
|
+
try {
|
|
227
|
+
emit.data(await sendBrowserSessionRequest(controlFile, action, args));
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (error instanceof BrowserSessionError) {
|
|
230
|
+
emit.error({ code: error.code, message: error.message });
|
|
231
|
+
emit.setExitCode(1);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
emit.error({ code: "browser_session_failed", message: "Browser session command failed." });
|
|
235
|
+
emit.setExitCode(1);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function parseIndex(value: string): number {
|
|
240
|
+
const parsed = Number(value);
|
|
241
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) {
|
|
242
|
+
throw new BrowserSessionError("invalid_index", "Tab index must be a non-negative integer.");
|
|
243
|
+
}
|
|
244
|
+
return parsed;
|
|
245
|
+
}
|