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.
- package/README.md +19 -1
- package/dist/brand.d.ts +28 -0
- package/dist/brand.js +41 -0
- package/dist/cli/commands/compile.js +32 -1
- package/dist/cli/commands/doctor.d.ts +19 -0
- package/dist/cli/commands/doctor.js +59 -0
- package/dist/cli/commands/init.js +5 -2
- package/dist/cli/commands/resume.js +25 -8
- package/dist/cli/commands/run.d.ts +61 -1
- package/dist/cli/commands/run.js +372 -18
- package/dist/cli/commands/status.js +145 -28
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/compile/collateral.d.ts +25 -0
- package/dist/compile/collateral.js +46 -11
- package/dist/compile/native.js +10 -0
- package/dist/config/config.d.ts +1 -0
- package/dist/config/config.js +2 -2
- package/dist/drivers/herdr.d.ts +19 -13
- package/dist/drivers/herdr.js +90 -26
- package/dist/drivers/index.d.ts +5 -1
- package/dist/drivers/index.js +16 -1
- package/dist/drivers/orca.d.ts +189 -0
- package/dist/drivers/orca.js +879 -0
- package/dist/drivers/types.d.ts +2 -0
- package/dist/gates/acceptance.js +17 -7
- package/dist/gates/llm.d.ts +19 -0
- package/dist/gates/llm.js +104 -6
- package/dist/gates/run-gates.d.ts +18 -0
- package/dist/gates/run-gates.js +195 -29
- package/dist/gates/scope.d.ts +9 -1
- package/dist/gates/scope.js +22 -2
- package/dist/graph/graph.d.ts +1 -0
- package/dist/graph/graph.js +19 -2
- package/dist/report/compare.js +17 -2
- package/dist/run/daemon.d.ts +1 -8
- package/dist/run/daemon.js +231 -246
- package/dist/run/environment.d.ts +18 -1
- package/dist/run/environment.js +19 -2
- package/dist/run/journal.d.ts +50 -3
- package/dist/run/journal.js +181 -5
- package/dist/run/protocol.d.ts +4 -4
- package/dist/run/stall.d.ts +30 -0
- package/dist/run/stall.js +173 -0
- package/dist/tui/ink/init-app.js +14 -4
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +27 -15
package/dist/drivers/herdr.js
CHANGED
|
@@ -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
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return { direction: "
|
|
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
|
-
|
|
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 —
|
|
1063
|
-
//
|
|
1064
|
-
//
|
|
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
|
-
//
|
|
1071
|
-
// read —
|
|
1072
|
-
const plan = boardSplitPlan(
|
|
1073
|
-
const
|
|
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.
|
|
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.
|
|
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
|
package/dist/drivers/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import type { TickmarkrConfig } from "../config/config.js";
|
|
2
2
|
import type { ExecutorDriver } from "./types.js";
|
|
3
|
-
export declare
|
|
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;
|
package/dist/drivers/index.js
CHANGED
|
@@ -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
|
+
}
|