tickmarkr 1.97.0 → 2.1.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 (47) hide show
  1. package/README.md +19 -1
  2. package/dist/brand.d.ts +28 -0
  3. package/dist/brand.js +41 -0
  4. package/dist/cli/commands/compile.js +32 -1
  5. package/dist/cli/commands/doctor.d.ts +19 -0
  6. package/dist/cli/commands/doctor.js +59 -0
  7. package/dist/cli/commands/init.js +5 -2
  8. package/dist/cli/commands/resume.js +25 -8
  9. package/dist/cli/commands/run.d.ts +61 -1
  10. package/dist/cli/commands/run.js +372 -18
  11. package/dist/cli/commands/status.js +145 -28
  12. package/dist/cli/index.d.ts +1 -1
  13. package/dist/cli/index.js +1 -1
  14. package/dist/compile/collateral.d.ts +25 -0
  15. package/dist/compile/collateral.js +46 -11
  16. package/dist/compile/native.js +10 -0
  17. package/dist/config/config.d.ts +1 -0
  18. package/dist/config/config.js +2 -2
  19. package/dist/drivers/herdr.d.ts +19 -13
  20. package/dist/drivers/herdr.js +90 -26
  21. package/dist/drivers/index.d.ts +5 -1
  22. package/dist/drivers/index.js +16 -1
  23. package/dist/drivers/orca.d.ts +189 -0
  24. package/dist/drivers/orca.js +879 -0
  25. package/dist/drivers/types.d.ts +2 -0
  26. package/dist/gates/acceptance.js +17 -7
  27. package/dist/gates/llm.d.ts +19 -0
  28. package/dist/gates/llm.js +104 -6
  29. package/dist/gates/run-gates.d.ts +18 -0
  30. package/dist/gates/run-gates.js +195 -29
  31. package/dist/gates/scope.d.ts +9 -1
  32. package/dist/gates/scope.js +22 -2
  33. package/dist/graph/graph.d.ts +1 -0
  34. package/dist/graph/graph.js +19 -2
  35. package/dist/report/compare.js +17 -2
  36. package/dist/run/daemon.d.ts +1 -8
  37. package/dist/run/daemon.js +231 -246
  38. package/dist/run/environment.d.ts +18 -1
  39. package/dist/run/environment.js +19 -2
  40. package/dist/run/journal.d.ts +50 -3
  41. package/dist/run/journal.js +181 -5
  42. package/dist/run/protocol.d.ts +4 -4
  43. package/dist/run/stall.d.ts +30 -0
  44. package/dist/run/stall.js +173 -0
  45. package/dist/tui/ink/init-app.js +14 -4
  46. package/package.json +1 -1
  47. package/skills/tickmarkr-overseer/SKILL.md +27 -15
@@ -69,19 +69,19 @@ export function workerSplitDirection(paneCols, safeFloor = TRAILER_SAFE_FLOOR_CO
69
69
  }
70
70
  // The watch board's geometry, deliberately NOT the trailer floor above. `workerSplitDirection`
71
71
  // halves the caller and refuses a right split under 108+2 — that bound protects WORKER panes, which
72
- // print a trailer; the supervising seat + board pair does not. Applied to that pair on 2026-08-18 it
73
- // sent a 189-column tab's board BELOW the seat and the operator corrected it (QUEUE-v194 criterion 1;
74
- // skills/tickmarkr-overseer/SKILL.md: "the side placement outranks the halving floor"). So the board
75
- // is allocated its measured width FIRST and the seat keeps the remainder.
76
- export const BOARD_TARGET_COLS = 110; // §14a measured clean-render bound for the board
77
- export const BOARD_SEAT_FLOOR_COLS = 40; // the seat beside it still has to be usable
78
- /** Board-first placement beside the supervising seat: right only while the caller can fund the board
79
- * its target AND leave the seat its floor; otherwise down at full width, never a squeezed board.
80
- * An unmeasurable caller falls back to down like every other placement here (fail closed). */
81
- export function boardSplitPlan(callerCols, boardCols = BOARD_TARGET_COLS, seatFloor = BOARD_SEAT_FLOOR_COLS) {
82
- if (callerCols == null || callerCols < boardCols + seatFloor)
83
- return { direction: "down", boardCols: null };
84
- return { direction: "right", ratio: Math.round(((callerCols - boardCols) / callerCols) * 1e4) / 1e4, boardCols };
72
+ // print a trailer; the supervising seat + board pair does not, and neither does the board's own
73
+ // placement any more. Every width-derived variant of this placement has been wrong in the operator's
74
+ // tab: the halving floor sent a 189-column board below the seat (2026-08-18), and the width-first
75
+ // side split that replaced it puts the board and the narration shoulder to shoulder when the board is
76
+ // the surface the operator reads and the narration is the rail beneath it. The placement is now ONE
77
+ // record — the board stacked ABOVE the caller at full width, taking 72% of the height — and it is
78
+ // invariant: no terminal width, measured or unmeasurable, can select a different arrangement.
79
+ export const BOARD_HEIGHT_SHARE = 0.72; // board 72 / narration 28, the operator's stack
80
+ /** The single approved vertical-stack record. The caller's columns are accepted and deliberately
81
+ * ignored: this signature is where width used to decide the arrangement, and the parameter stays
82
+ * so that "the plan does not depend on it" is a property a caller (and a test) can exercise. */
83
+ export function boardSplitPlan(_callerCols) {
84
+ return { direction: "down", ratio: BOARD_HEIGHT_SHARE, swap: "above" };
85
85
  }
86
86
  /** The tab a slot belongs to: its TASK — worker, judge, review and consult panes for one task share it.
87
87
  * Returns undefined for everything else, which keeps those on the dedicated-tab path.
@@ -147,6 +147,7 @@ export class HerdrDriver {
147
147
  // the caller launched tickmarkr elsewhere (process.cwd is not run identity). The repo itself is
148
148
  // also bound for judge/review/consult slots whose cwd is the root rather than a task worktree.
149
149
  journalRoots = new Map();
150
+ narrate;
150
151
  // VIS-10: the run's workspace id, captured once at construction (the daemon inherits it from the
151
152
  // operator's env before the driver is built). Required at slot() time, never in the constructor —
152
153
  // pickDriver and its unit test construct HerdrDriver without env, so slot() is the trust gate.
@@ -176,7 +177,14 @@ export class HerdrDriver {
176
177
  if (!repoRoot) {
177
178
  throw new Error(`cannot journal dispatch-retry: slot ${slot.name} has no daemon repo binding for ${slot.cwd}`);
178
179
  }
179
- Journal.open(repoRoot, owned.runId).append("dispatch-retry", owned.taskId, data);
180
+ // Bound to the live narration sink (`narrateWith`): this Journal is the driver's own — the
181
+ // daemon never appends this event and never sees it — so an unbound handle here persists the
182
+ // recovery to the file and the pipe while the operator's rail stays silent about it.
183
+ Journal.open(repoRoot, owned.runId, this.narrate).append("dispatch-retry", owned.taskId, data);
184
+ }
185
+ /** v1.99 T2: bind this driver's own journal writes to the run's live narration sink. */
186
+ narrateWith(narrate) {
187
+ this.narrate = narrate;
180
188
  }
181
189
  serial(fn) {
182
190
  const p = this.groupSerial.then(fn, fn);
@@ -274,6 +282,20 @@ export class HerdrDriver {
274
282
  return null;
275
283
  }
276
284
  }
285
+ // Is this pane id still in the listing? FAIL CLOSED: a listing we cannot read cannot prove a pane
286
+ // gone, and the caller uses this to decide whether a pane it tried to close is really off screen.
287
+ async paneStillOpen(paneId) {
288
+ const r = await this.herdr(`pane list`);
289
+ if (r.code !== 0)
290
+ return true;
291
+ try {
292
+ const panes = JSON.parse(r.stdout).result?.panes;
293
+ return !Array.isArray(panes) || panes.some((p) => p.pane_id === paneId);
294
+ }
295
+ catch {
296
+ return true;
297
+ }
298
+ }
277
299
  // Before delivery, resolve fresh via the durable name because pane ids can compact. After delivery,
278
300
  // pin the verified target so early liveness cannot drift to a label rebound onto another pane.
279
301
  async paneId(slot) {
@@ -1059,19 +1081,18 @@ export class HerdrDriver {
1059
1081
  return p.workspace_id === this.ws && typeof p.pane_id === "string" && owned?.role === "watch" && owned.taskId === "run";
1060
1082
  }).map((p) => p.pane_id);
1061
1083
  }
1062
- // T2: the watch is a sibling of the daemon's own pane, never a separate tab — beside it when the
1063
- // tab can fund the board its width, below it at full width when it cannot. Its durable owned name
1064
- // is how a later daemon RECOGNIZES the board it must retire, so a run never stacks a second one.
1084
+ // T2: the watch is a sibling of the daemon's own pane, never a separate tab — stacked ABOVE it at
1085
+ // the caller's full width, always, whatever the terminal measures. Its durable owned name is how a
1086
+ // later daemon RECOGNIZES the board it must retire, so a run never stacks a second one.
1065
1087
  async watchSlot(cwd, name) {
1066
1088
  if (!this.ws)
1067
1089
  throw new Error("herdr watch placement requires HERDR_WORKSPACE_ID — refusing unseeded pane");
1068
1090
  if (!this.callerPane)
1069
1091
  throw new Error("herdr watch placement requires HERDR_PANE_ID — refusing untargeted split");
1070
- // Board width first (boardSplitPlan), measured off the caller through the driver's own layout
1071
- // read — never an unconditional right split, and never the worker halving rule.
1072
- const plan = boardSplitPlan(await this.paneWidth(this.callerPane));
1073
- const ratio = plan.ratio == null ? "" : ` --ratio ${plan.ratio}`;
1074
- const sp = await this.herdr(`pane split ${shq(this.callerPane)} --direction ${plan.direction}${ratio} --no-focus`);
1092
+ // One invariant placement (boardSplitPlan): split the caller down, then swap the new pane above
1093
+ // it. No layout read decides this — width chose the arrangement twice and was wrong twice.
1094
+ const plan = boardSplitPlan();
1095
+ const sp = await this.herdr(`pane split ${shq(this.callerPane)} --direction ${plan.direction} --ratio ${plan.ratio} --no-focus`);
1075
1096
  if (sp.code !== 0)
1076
1097
  throw new Error(`herdr watch split failed: ${sp.stderr || sp.stdout}`);
1077
1098
  let pane;
@@ -1083,18 +1104,61 @@ export class HerdrDriver {
1083
1104
  }
1084
1105
  if (typeof pane !== "string" || !pane)
1085
1106
  throw new Error(`herdr watch split returned no pane id: ${sp.stdout}`);
1107
+ // The split leaves the board UNDER the caller; the swap is what makes the stack the requested
1108
+ // one. Verified, not assumed: a swap that failed would leave a board below the narration while
1109
+ // the daemon reported the geometry it asked for. Instead the split pane is closed and the failure
1110
+ // propagates — the daemon swallows it and runs boardless, which is honest about what is on screen.
1111
+ // `pane swap` answers a no-op with a ZERO exit and `changed:false` (herdr socket API: a swap it
1112
+ // declined is a non-error response), so an exit code alone proves nothing about the geometry —
1113
+ // that is exactly the path that would leave the board below the narration while the daemon
1114
+ // reported the stack. The documented `changed` flag is the verification; anything else — a
1115
+ // nonzero exit, `changed:false`, an unparseable result — fails closed.
1116
+ const swapped = await this.herdr(`pane swap --source-pane ${shq(pane)} --target-pane ${shq(this.callerPane)}`);
1117
+ // Flag lives at `result.swap.changed` (verbatim 0.8.0); see herdr-swap-shape.test.ts.
1118
+ let swapChanged;
1119
+ try {
1120
+ const result = JSON.parse(swapped.stdout).result;
1121
+ swapChanged = result?.swap?.changed ?? result?.changed;
1122
+ }
1123
+ catch {
1124
+ /* fail closed below */
1125
+ }
1126
+ if (swapped.code !== 0 || swapChanged !== true) {
1127
+ await this.discardSplit(pane, `herdr watch swap ${plan.swap} failed: ${swapped.code !== 0
1128
+ ? swapped.stderr || swapped.stdout
1129
+ : `herdr reported no swap took place: ${swapped.stdout || swapped.stderr}`}`);
1130
+ }
1086
1131
  const renamed = await this.herdr(`pane rename ${shq(pane)} ${shq(name)}`);
1087
1132
  if (renamed.code !== 0 || await this.namedPaneId(name) !== pane) {
1088
- await this.herdr(`pane close ${shq(pane)}`);
1089
- throw new Error(`herdr watch rename failed: ${renamed.stderr || renamed.stdout}`);
1133
+ await this.discardSplit(pane, `herdr watch rename failed: ${renamed.stderr || renamed.stdout}`);
1090
1134
  }
1091
1135
  const seed = await this.herdr(`pane run ${shq(pane)} ${shq(`cd ${shq(cwd)}; export HERDR_WORKSPACE_ID=${shq(this.ws)}; ${herdrSealShellPrefix()}`)}`, cwd);
1092
1136
  if (seed.code !== 0) {
1093
- await this.herdr(`pane close ${shq(pane)}`);
1094
- throw new Error(`herdr watch seed failed: ${seed.stderr || seed.stdout}`);
1137
+ await this.discardSplit(pane, `herdr watch seed failed: ${seed.stderr || seed.stdout}`);
1095
1138
  }
1096
1139
  return { id: pane, name, cwd };
1097
1140
  }
1141
+ // A board that could not be placed costs the OPERATOR a stray pane unless the split is really
1142
+ // taken back, so every close is followed by a pane-list verification rather than issued and
1143
+ // forgotten. A nonzero close and a success that frees nothing are both diagnosed from that same
1144
+ // observation. The daemon swallows it either way and runs boardless — but never silently keeps a
1145
+ // split the geometry it asked for does not include.
1146
+ async discardSplit(pane, why) {
1147
+ const closed = await this.herdr(`pane close ${shq(pane)}`);
1148
+ const stillOpen = await this.paneStillOpen(pane);
1149
+ const orphan = stillOpen
1150
+ ? closed.code !== 0
1151
+ ? closed.stderr || closed.stdout || `exit ${closed.code}`
1152
+ : "close reported success but the pane could not be proven absent from pane list"
1153
+ : null;
1154
+ if (orphan !== null) {
1155
+ // The daemon swallows narrator failures whole (visibility is never a gate), so the thrown
1156
+ // error dies in its catch. This line is the operator's only notice that a pane they did not
1157
+ // ask for is still on their screen and that no process will take it back.
1158
+ console.error(`tickmarkr: the watch split ${pane} survived its close (${orphan}) — close it by hand; the run continues boardless`);
1159
+ }
1160
+ throw new Error(orphan === null ? why : `${why} — and the split pane ${pane} survived its close (${orphan})`);
1161
+ }
1098
1162
  // T6 narrator: the run's single live status surface, RUNNING THE COMMAND THIS CALL SUPPLIED. Only
1099
1163
  // a board this driver instance itself opened is reused (this.watches); any other surviving board —
1100
1164
  // a prior run's, or one already carrying this run's canonical name after a resume — is retired and
@@ -1,3 +1,7 @@
1
1
  import type { TickmarkrConfig } from "../config/config.js";
2
2
  import type { ExecutorDriver } from "./types.js";
3
- export declare function pickDriver(cfg: TickmarkrConfig, override?: "auto" | "herdr" | "subprocess"): ExecutorDriver;
3
+ export declare const DRIVER_CHOICES: readonly ["auto", "herdr", "subprocess", "orca"];
4
+ export type DriverChoice = (typeof DRIVER_CHOICES)[number];
5
+ /** Validate argv at the CLI boundary rather than casting an arbitrary string into a driver choice. */
6
+ export declare function parseDriverOverride(override?: string): DriverChoice | undefined;
7
+ export declare function pickDriver(cfg: TickmarkrConfig, override?: string): ExecutorDriver;
@@ -1,7 +1,18 @@
1
1
  import { HerdrDriver } from "./herdr.js";
2
+ import { OrcaDriver } from "./orca.js";
2
3
  import { SubprocessDriver } from "./subprocess.js";
4
+ export const DRIVER_CHOICES = ["auto", "herdr", "subprocess", "orca"];
5
+ /** Validate argv at the CLI boundary rather than casting an arbitrary string into a driver choice. */
6
+ export function parseDriverOverride(override) {
7
+ if (override === undefined)
8
+ return undefined;
9
+ for (const choice of DRIVER_CHOICES)
10
+ if (override === choice)
11
+ return choice;
12
+ throw new Error(`usage: --driver must be one of ${DRIVER_CHOICES.join(" | ")} (got ${override})`);
13
+ }
3
14
  export function pickDriver(cfg, override) {
4
- const want = override ?? cfg.driver;
15
+ const want = parseDriverOverride(override) ?? cfg.driver;
5
16
  // VIS-09 item 2: plumb the per-tab cap into the HerdrDriver — the driver takes it as a constructor
6
17
  // param and never imports config (cfg is the only seam). Guaranteed present: DEFAULT_CONFIG seeds
7
18
  // workersPerTab:3 and deepMerge overlays on top, so a missing overlay key still resolves.
@@ -9,5 +20,9 @@ export function pickDriver(cfg, override) {
9
20
  return new HerdrDriver("herdr", cfg.visibility.workersPerTab);
10
21
  if (want === "subprocess")
11
22
  return new SubprocessDriver();
23
+ // Orca is an operator-selected execution surface. Its runtime failure stays on Orca; selection
24
+ // must never substitute a hidden subprocess worker after this explicit choice.
25
+ if (want === "orca")
26
+ return new OrcaDriver();
12
27
  return HerdrDriver.available() ? new HerdrDriver("herdr", cfg.visibility.workersPerTab) : new SubprocessDriver();
13
28
  }
@@ -0,0 +1,189 @@
1
+ import { type ShResult } from "../run/git.js";
2
+ import { type ExecutorDriver, type NotifyOpts, type Slot, type SlotOpts } from "./types.js";
3
+ /** The response families the ONE shared envelope parser serves. There is no second JSON seam. */
4
+ export declare const ORCA_RESPONSE_FAMILIES: readonly ["status", "create", "list", "read", "send", "wait", "show", "close"];
5
+ export type OrcaFamily = (typeof ORCA_RESPONSE_FAMILIES)[number];
6
+ export declare const STALE_HANDLE_CODE = "terminal_handle_stale";
7
+ export declare const NOT_WRITABLE_CODE = "terminal_not_writable";
8
+ /** The ONLY terminal status that licenses reading a terminal's bytes or its agent state. */
9
+ export declare const RUNNING_STATUS = "running";
10
+ export declare const STATUS_GOVERNED_METHODS: readonly ["read", "waitOutput", "status", "waitAgentStatus"];
11
+ export interface OrcaExec {
12
+ (args: string[], cwd: string, timeoutMs?: number): Promise<ShResult>;
13
+ }
14
+ export interface OrcaTimeSource {
15
+ now: () => number;
16
+ sleep: (ms: number) => Promise<void>;
17
+ }
18
+ /** Every failure this driver produces is explicit and carries the raw bytes that produced it. */
19
+ export declare class OrcaError extends Error {
20
+ readonly family: string;
21
+ readonly reason: string;
22
+ readonly raw: string;
23
+ readonly code?: string;
24
+ readonly runtimeId?: string;
25
+ constructor(family: string, reason: string, raw: string, opts?: {
26
+ code?: string;
27
+ runtimeId?: string;
28
+ });
29
+ }
30
+ /** The slot cannot be addressed: dead/unknown terminal record, or a handle that cannot be recovered
31
+ * to exactly one owned terminal in the slot's own worktree. Never a silent false or empty string. */
32
+ export declare class OrcaUnavailableError extends OrcaError {
33
+ readonly terminalStatus?: string | undefined;
34
+ constructor(family: string, reason: string, raw: string, terminalStatus?: string | undefined);
35
+ }
36
+ export interface OrcaEnvelope {
37
+ result: Record<string, unknown>;
38
+ runtimeId: string;
39
+ raw: string;
40
+ }
41
+ /**
42
+ * The one JSON seam. Fails CLOSED on every degenerate response — empty, unparseable (a truncated
43
+ * body lands here), non-object, no boolean `ok`, `ok:false`, `ok:true` with no result object, or a
44
+ * successful response without a usable `_meta.runtimeId` —
45
+ * and preserves the raw bytes on the thrown error for diagnostics. Callers never see a partial
46
+ * envelope, so no caller can reinterpret a parse failure as empty output, an unknown-but-successful
47
+ * status, or a successful close.
48
+ */
49
+ export declare function parseEnvelope(family: OrcaFamily, stdout: string, raw?: string): OrcaEnvelope;
50
+ /** The worktree a terminal record binds to. The spike pinned the record's shape but not this key's
51
+ * spelling, so the known aliases are accepted and nothing else — a record with none is unbound,
52
+ * which fails every identity comparison below rather than passing one by default. */
53
+ export declare function terminalWorktree(term: Record<string, unknown>): string | undefined;
54
+ /**
55
+ * The same checkout under two spellings. git hands tickmarkr one (`/tmp/...` on darwin, or anything
56
+ * below a symlinked parent) while Orca answers the canonicalized one (`/private/tmp/...`), and
57
+ * `resolve()` collapses `..` but never a symlink — so string equality on resolved paths reports two
58
+ * different checkouts and leaves a perfectly valid slot unreacquirable after a runtime restart.
59
+ * Identity is FILESYSTEM identity. A path that does not exist has no filesystem identity to read, so
60
+ * it keeps its resolved spelling: deterministic, and still comparable to another spelling of itself.
61
+ */
62
+ export declare function canonicalWorktreePath(path: string): string;
63
+ /** Conservative agent-state mapping over orca's ACTUAL surfaces: `blocked` only when the show
64
+ * record reports agentWait:true, `idle` only when the `terminal wait --for tui-idle` condition is
65
+ * satisfied. The recorded 1.4.186 show response carries NO agent field at all — an absent signal
66
+ * is "unknown", never a fabricated definite status. */
67
+ export declare function mapAgentState(term: Record<string, unknown>, tuiIdle: boolean): string;
68
+ /**
69
+ * The renderer hard-wraps long lines, paints margin chrome, and a cursor page boundary splits a
70
+ * marker exactly like a wrap does. `parseWorkerResult` (src/adapters/prompt.ts) already de-wraps
71
+ * trailers this way, so marker matching gets the same joined view beside the raw one.
72
+ * ponytail: joining every line can in principle glue two unrelated lines into a marker — the same
73
+ * tolerance parseWorkerResult has carried since v1.2; raw is matched first, so an unwrapped hit
74
+ * never depends on this.
75
+ */
76
+ export declare function joinWrapped(raw: string): string;
77
+ export interface OrcaDriverOpts {
78
+ bin?: string;
79
+ exec?: OrcaExec;
80
+ time?: OrcaTimeSource;
81
+ pageLines?: number;
82
+ pollMs?: number;
83
+ /** Bounded, seam-adjustable staleness window for runtime probes before mutations. */
84
+ probeStalenessMs?: number;
85
+ }
86
+ export declare class OrcaDriver implements ExecutorDriver {
87
+ id: string;
88
+ interactive: boolean;
89
+ private slots;
90
+ private n;
91
+ private bin;
92
+ private exec;
93
+ private time;
94
+ private pageLines;
95
+ private pollMs;
96
+ private probeStalenessMs;
97
+ constructor(opts?: OrcaDriverOpts);
98
+ private call;
99
+ /** The live runtime's identity, or an explicit failure. A missing or unreachable runtime is a
100
+ * driver-level failure carrying the raw refusal — never a reachable-looking default. */
101
+ private runtimeEnv;
102
+ /** Explicit runtime probe. Also T3's doctor probe. */
103
+ probeRuntime(cwd?: string): Promise<string>;
104
+ slot(cwd: string, name: string, opts?: SlotOpts): Promise<Slot>;
105
+ /** Where to invoke the CLI for this slot's calls (see OrcaSlotState.dir). */
106
+ private cliCwd;
107
+ private state;
108
+ private latched;
109
+ private assertAvailable;
110
+ run(slot: Slot, cmd: string): Promise<void>;
111
+ private create;
112
+ /**
113
+ * Every terminal-addressed call — read AND write — goes through here, and the runtime identity is
114
+ * established BEFORE the runtime-scoped handle goes on the wire. Discarding a lookalike's answer
115
+ * after reading it is still having addressed it, so the probe comes first; the post-call check
116
+ * only closes the narrow race of a restart landing between probe and call. Same for an explicit
117
+ * `terminal_handle_stale`. Either way the driver relists the slot's exact worktree and replaces
118
+ * the handle exactly once, then re-issues the operation against the replacement.
119
+ */
120
+ private terminalOp;
121
+ private recover;
122
+ /** Validated READ terminal record, or an explicit unavailable failure. Called BEFORE any caller
123
+ * looks at tail bytes — on every page, on every read-governed method. Read records are the one
124
+ * place orca reports a literal `status` (recorded: "running" live, "exited" on the dead record). */
125
+ private validated;
126
+ /** Validated SHOW terminal record, or an explicit unavailable failure. The recorded 1.4.186 show
127
+ * response reports liveness through connected/orphaned and carries NO status and NO agent field,
128
+ * so this is the status discipline's show leg: a terminal that cannot prove connected-and-not-
129
+ * orphaned is unavailable for state questions, exactly as a non-running read record is for bytes. */
130
+ private liveShowTerm;
131
+ private tailText;
132
+ private readPage;
133
+ /** A single UNPAGED tail read — exactly what the caller asked for and nothing more. Markers split
134
+ * across cursor pages are not reassembled here; that is waitOutput's job. */
135
+ read(slot: Slot, lines: number): Promise<string>;
136
+ /**
137
+ * Bounded cursor-paged sweep into the slot's accumulated buffer. The first read of a slot carries
138
+ * no cursor: it is the ANCHOR, whose `oldestCursor` says where the retained buffer starts (its own
139
+ * tail is the newest lines, not the oldest, so it is not appended). Every page after it appends,
140
+ * and every one of them — anchor included — is status-validated before a single byte is matched.
141
+ */
142
+ private sweep;
143
+ waitOutput(slot: Slot, pattern: string, timeoutMs: number, opts?: {
144
+ regex?: boolean;
145
+ }): Promise<boolean>;
146
+ status(slot: Slot): Promise<string>;
147
+ /** One `terminal wait` through the full identity machinery. The recorded 1.4.186 elapsed answer
148
+ * is rc 1 + ok:true + {handle, condition, satisfied:false, status:"running"}; it is "not yet"
149
+ * only after this method validates all four fields. Any malformed/refused wait remains explicit. */
150
+ private waitCondition;
151
+ waitAgentStatus(slot: Slot, status: string, timeoutMs: number): Promise<boolean>;
152
+ notify(msg: string, opts?: NotifyOpts): Promise<void>;
153
+ close(slot: Slot): Promise<void>;
154
+ /**
155
+ * The one destructive call in this driver, for a slot's own terminal AND for a reconcile candidate
156
+ * alike. It goes through terminalOp deliberately: the live runtime identity is proven immediately
157
+ * BEFORE the handle goes on the wire, and a runtime that changed does not merely fail the close —
158
+ * the handle is DISCARDED and re-derived from the owned tab title in that exact checkout, where a
159
+ * handle value the new runtime happened to reissue to somebody else's terminal is refused by
160
+ * construction (recover()). Checking identity on the receipt afterwards could not undo a close.
161
+ */
162
+ private closeTerminal;
163
+ /**
164
+ * The WHOLE terminal table. `terminal list` caps rows at its own default and says so through
165
+ * `truncated`/`totalCount`; a capped listing is not an ownership snapshot, because the row it
166
+ * dropped is precisely the older run's leftover no later sweep would ever see again.
167
+ * ponytail: two asks, not a paging loop — `--limit` takes the whole table in one go, and a runtime
168
+ * that still reports truncated at totalCount rows is a listing this sweep declines to judge on.
169
+ */
170
+ private listAll;
171
+ /**
172
+ * Sweep tickmarkr-owned terminals down to `desired`. Ownership is decided ONLY by parseOwnedName
173
+ * over the owned TAB title, through the same panesToClose fold herdr uses (drivers/types.ts): an
174
+ * owned-and-undesired terminal closes whichever run — and whichever daemon — created it, and a
175
+ * title that does not parse is never a candidate however much it resembles one.
176
+ *
177
+ * The listing is UNSCOPED and layout-bearing. Unscoped because an older run's leftover sits in a
178
+ * checkout this run never knew, so a `--worktree`-filtered sweep is exactly how such a leftover
179
+ * survives forever. Layout-bearing because the owned title survives at TAB identity only — a list
180
+ * row's `title` is the shell-controlled pane title, and closing on that is how a foreign pane that
181
+ * happens to be running an owned-looking command gets killed.
182
+ *
183
+ * Cosmetic by contract: every failure is swallowed, per candidate and overall.
184
+ */
185
+ reconcile(desired: Set<string>, runId: string, opts?: {
186
+ spareLiveLlm?: boolean;
187
+ }): Promise<void>;
188
+ worktree(repo: string, branch: string, baseRef: string): Promise<string>;
189
+ }