tickmarkr 2.5.4 → 2.5.5

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 (59) hide show
  1. package/dist/adapters/registry.js +6 -1
  2. package/dist/adapters/types.d.ts +3 -0
  3. package/dist/cli/commands/approve.d.ts +1 -0
  4. package/dist/cli/commands/approve.js +54 -6
  5. package/dist/cli/commands/doctor.js +44 -29
  6. package/dist/cli/commands/fleet.js +195 -33
  7. package/dist/cli/commands/init.js +196 -6
  8. package/dist/cli/commands/resume.js +4 -2
  9. package/dist/cli/commands/run.js +11 -2
  10. package/dist/cli/commands/verify.js +1 -1
  11. package/dist/cli/help.d.ts +2 -0
  12. package/dist/cli/help.js +3 -1
  13. package/dist/config/config.d.ts +14 -8
  14. package/dist/config/config.js +29 -26
  15. package/dist/config/fleet-overlay.d.ts +3 -9
  16. package/dist/config/fleet-overlay.js +68 -11
  17. package/dist/config/fleet-why.d.ts +7 -0
  18. package/dist/config/fleet-why.js +5 -0
  19. package/dist/drivers/index.d.ts +15 -1
  20. package/dist/drivers/index.js +38 -10
  21. package/dist/drivers/orca.d.ts +99 -10
  22. package/dist/drivers/orca.js +586 -97
  23. package/dist/gates/baseline.d.ts +3 -0
  24. package/dist/gates/baseline.js +2 -1
  25. package/dist/gates/cache.d.ts +100 -0
  26. package/dist/gates/cache.js +389 -0
  27. package/dist/gates/run-gates.d.ts +3 -0
  28. package/dist/gates/run-gates.js +117 -10
  29. package/dist/gates/test-manifest.d.ts +99 -0
  30. package/dist/gates/test-manifest.js +389 -0
  31. package/dist/gates/test-reporter.d.ts +4 -0
  32. package/dist/gates/test-reporter.js +49 -0
  33. package/dist/route/preference.d.ts +22 -1
  34. package/dist/route/preference.js +123 -25
  35. package/dist/route/router.js +31 -6
  36. package/dist/run/daemon.d.ts +13 -0
  37. package/dist/run/daemon.js +160 -70
  38. package/dist/run/git.d.ts +10 -1
  39. package/dist/run/git.js +43 -9
  40. package/dist/run/journal.d.ts +14 -2
  41. package/dist/run/journal.js +78 -10
  42. package/dist/run/lease.d.ts +14 -0
  43. package/dist/run/lease.js +87 -0
  44. package/dist/run/merge.d.ts +2 -0
  45. package/dist/run/merge.js +91 -3
  46. package/dist/run/operator-state.d.ts +11 -0
  47. package/dist/run/operator-state.js +17 -3
  48. package/dist/tui/cockpit/board.d.ts +96 -0
  49. package/dist/tui/cockpit/board.js +346 -0
  50. package/dist/tui/cockpit/decision-actions.js +2 -0
  51. package/dist/tui/cockpit/layout.d.ts +5 -1
  52. package/dist/tui/cockpit/layout.js +8 -3
  53. package/dist/tui/cockpit/live-runtime.js +71 -21
  54. package/dist/tui/cockpit/run-view.d.ts +7 -5
  55. package/dist/tui/cockpit/run-view.js +12 -11
  56. package/dist/tui/ink/fleet-app.d.ts +41 -27
  57. package/dist/tui/ink/fleet-app.js +204 -31
  58. package/package.json +1 -1
  59. package/skills/tickmarkr-overseer/SKILL.md +173 -113
@@ -1,8 +1,11 @@
1
- import { realpathSync } from "node:fs";
2
- import { resolve } from "node:path";
1
+ import { existsSync, linkSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { dirname, join, resolve } from "node:path";
3
4
  import { shq } from "../adapters/types.js";
4
5
  import { createWorktree, sh } from "../run/git.js";
5
- import { Journal } from "../run/journal.js";
6
+ import { Journal, parseRunId } from "../run/journal.js";
7
+ import { stateDirName } from "../graph/graph.js";
8
+ import { readWatchBoard, requestWatchBoardStop, stopWatchBoard, WATCH_OWNER_ENV } from "../run/supervision.js";
6
9
  import { MAX_BUF } from "./subprocess.js";
7
10
  import { formatOwnedName, panesToClose, parseOwnedName } from "./types.js";
8
11
  // Orca (onorca.dev) as a third execution surface beside herdr and subprocess. tickmarkr keeps
@@ -30,11 +33,16 @@ import { formatOwnedName, panesToClose, parseOwnedName } from "./types.js";
30
33
  // focused — a driver that lets the app pick has given away the isolation the worktree exists
31
34
  // for, so every call this driver makes names `path:` and verifies what came back (T2);
32
35
  // - `terminal list`'s `--worktree` is OPTIONAL (same help): the reconcile sweep omits it, because
33
- // an older run's leftover sits in a checkout this run never knew (T2).
36
+ // an older run's leftover sits in a checkout this run never knew (T2);
37
+ // - Orca tracks only the worktrees it created or the operator opened (1.4.200, OBS-1004): a task
38
+ // checkout the daemon adds under the clone is not a selector, `worktree current` from inside it
39
+ // answers the enclosing clone, and there is no adopt verb — so every terminal is created ON the
40
+ // tracked worktree with its command cd'ed into the checkout, and `worktree set` names the
41
+ // tracked worktree; identity stays the handle plus the owned tab title.
34
42
  /** The response families the ONE shared envelope parser serves. There is no second JSON seam. */
35
43
  export const ORCA_RESPONSE_FAMILIES = [
36
44
  "status", "create", "list", "read", "send", "wait", "show", "close",
37
- "worktree-current", "worktree-set", "hooks-status",
45
+ "worktree-current", "worktree-set", "hooks-status", "split",
38
46
  ];
39
47
  export const ORCA_FIXTURE_VERSION = "1.4.195";
40
48
  export const ORCA_CLI_COMMAND_ENV = "ORCA_CLI_COMMAND";
@@ -54,9 +62,6 @@ const PAGE_LINES = 500; // per-page ask; orca caps server-side and reports `limi
54
62
  const LIST_LIMIT = 10000; // well past orca's own row default; `truncated` still decides (listAll)
55
63
  const MAX_PAGES = 400; // runaway guard: a cursor that stops advancing ends the sweep, never loops
56
64
  const POLL_MS = 200;
57
- export const WORKTREE_ADOPTION_TIMEOUT_MS = 60_000;
58
- const WORKTREE_ADOPTION_POLL_MS = 1_000;
59
- const WORKTREE_ADOPTION_JOURNAL_MS = 2_000;
60
65
  const NUDGE_ECHO_TIMEOUT_MS = 2_000;
61
66
  /** A missing slot gets the same bounded chance to appear as a reaped shell gets to settle. */
62
67
  export const PENDING_PROJECT_GRACE_MS = 2_000;
@@ -189,6 +194,67 @@ export function canonicalWorktreePath(path) {
189
194
  function sameWorktree(reported, canonical) {
190
195
  return reported !== undefined && canonicalWorktreePath(reported) === canonical;
191
196
  }
197
+ /** OBS-1004: a reported (tracked) worktree encloses a checkout when it IS the checkout or a parent of it. */
198
+ function enclosesCheckout(reported, checkout) {
199
+ if (reported === undefined)
200
+ return false;
201
+ const tracked = canonicalWorktreePath(reported);
202
+ return tracked === checkout || checkout.startsWith(`${tracked}/`);
203
+ }
204
+ /** The proof line a worker terminal prints first: recovery and focus read it back (FX-N01). */
205
+ export const CHECKOUT_MARK = "TICKMARKR_CHECKOUT";
206
+ // FX-N05: the proof is a FRAMED value — `TICKMARKR_CHECKOUT <byteLength>:<utf8 bytes as hex>;` — so it
207
+ // carries no whitespace or quotes, survives renderer wrapping (hex rows re-join losslessly), and is
208
+ // either complete (length matches, terminator present) or nothing. A prefix of another checkout can
209
+ // never decode to this one; `A` versus `A B`, `…--T1` versus `…--T10` are different frames.
210
+ const CHECKOUT_FRAME_RE = /TICKMARKR_CHECKOUT (\d+):([0-9a-f]*)(;?)/g;
211
+ const PROOF_PAGES = 16; // pages read from the oldest cursor before the proof is declared absent
212
+ /** The exact bytes the create command prints as its first line. */
213
+ export function checkoutProofLine(checkout) {
214
+ const bytes = Buffer.from(checkout, "utf8");
215
+ return `${CHECKOUT_MARK} ${bytes.length}:${bytes.toString("hex")};`;
216
+ }
217
+ /** Every complete frame in a scrollback, decoded and canonicalized; whether an incomplete one was seen. */
218
+ export function checkoutFrames(text) {
219
+ const complete = [];
220
+ let incomplete = false;
221
+ // Rows are re-joined first: a wrapped frame is whole again, and joining can never complete a
222
+ // frame that was not printed whole — the declared length and the terminator decide.
223
+ for (const m of joinWrapped(text).matchAll(CHECKOUT_FRAME_RE)) {
224
+ const length = Number(m[1]);
225
+ const hex = m[2];
226
+ if (m[3] !== ";" || hex.length !== length * 2 || !Number.isInteger(length)) {
227
+ incomplete = true;
228
+ continue;
229
+ }
230
+ complete.push(canonicalWorktreePath(Buffer.from(hex, "hex").toString("utf8")));
231
+ }
232
+ return { complete, incomplete };
233
+ }
234
+ /** Does a scrollback prove exactly `checkout`: at least one complete frame equals it, no complete
235
+ * frame names anything else, and no frame is incomplete. Full-path equality after canonicalization —
236
+ * never a prefix, a substring, or a whitespace-terminated fragment. */
237
+ export function provesCheckout(text, checkout) {
238
+ const { complete, incomplete } = checkoutFrames(text);
239
+ return !incomplete && complete.includes(checkout) && complete.every((c) => c === checkout);
240
+ }
241
+ /**
242
+ * Everything a terminal on the tracked worktree runs before the payload: enter the checkout (a
243
+ * failed cd stops the whole line — nothing of the payload ever runs in the enclosing path), print
244
+ * the proof line, then hand the WHOLE payload to one `sh -c` so a background list, a `;` list or a
245
+ * subshell inside it all start in the checkout and its exit status is the payload's (FX-N02).
246
+ */
247
+ export function checkoutPrefix(checkout) {
248
+ return `cd ${shq(checkout)} && printf '%s\\n' ${shq(checkoutProofLine(checkout))} && sh -c `;
249
+ }
250
+ /** The command a terminal on the tracked worktree runs so that it executes INSIDE the checkout. */
251
+ export function inCheckout(checkout, cmd) {
252
+ return `${checkoutPrefix(checkout)}${shq(cmd)}`;
253
+ }
254
+ /** Every checkout the complete proof frames in a scrollback name, in order of appearance. */
255
+ export function checkoutsNamed(text) {
256
+ return checkoutFrames(text).complete;
257
+ }
192
258
  // Orca has ONE terminal space — no workspace dimension for a terminal to be outside of — so every
193
259
  // reconcile candidate takes panesToClose's in-workspace branch: owned-and-undesired closes whichever
194
260
  // run (or which daemon) created it, and an unparseable title is never a candidate anywhere.
@@ -204,12 +270,152 @@ export function mapAgentState(term, tuiIdle) {
204
270
  return "idle";
205
271
  return "unknown"; // absent fields: unknown, never blocked/idle
206
272
  }
273
+ function boardState(r) {
274
+ if (r.retired === true)
275
+ return "retired";
276
+ if (typeof r.pid !== "number" || typeof r.armId !== "string")
277
+ return "reserved";
278
+ return r.pane === "" ? "claimed" : "bound";
279
+ }
280
+ /** The record's exact bytes and their parse; undefined only when no record exists. A torn or foreign
281
+ * file throws — it is indeterminate, never absent. */
282
+ function readBoard(path, repo, runId) {
283
+ let raw;
284
+ try {
285
+ raw = readFileSync(path, "utf8");
286
+ }
287
+ catch (error) {
288
+ if (error.code === "ENOENT")
289
+ return undefined;
290
+ throw error;
291
+ }
292
+ const record = JSON.parse(raw);
293
+ if (record.repo !== realpathSync(repo) || record.runId !== runId || typeof record.token !== "string" || typeof record.pane !== "string") {
294
+ throw new Error(`watch owner record ${path} does not describe run ${runId} in ${repo}`);
295
+ }
296
+ return { raw, record };
297
+ }
298
+ /**
299
+ * Whole-record compare-and-swap. The canonical path stays readable until commit: a mkdir lock
300
+ * excludes other writers, then create-only `link`s the new inode (fails if anything exists) or
301
+ * `rename`s the new file over the live path (POSIX atomic replace — readers see old or new, never
302
+ * absence). A crash that leaves a `.tmp` or `.lock` does not drop the previous record.
303
+ * ponytail: observeNamedRun (supervision.ts) renames without CAS. It cannot interleave with a swap
304
+ * because the narrator writes nothing between reserve and claim, and no transition here swaps one.
305
+ */
306
+ export function casBoard(family, path, expected, next) {
307
+ const raw = JSON.stringify(next) + "\n";
308
+ const refused = () => new OrcaError(family, `watch owner record ${path} changed underneath; swap refused and the current record kept`, "");
309
+ mkdirSync(dirname(path), { recursive: true });
310
+ const lock = `${path}.lock`;
311
+ const deadline = Date.now() + 2_000;
312
+ for (;;) {
313
+ try {
314
+ mkdirSync(lock);
315
+ break;
316
+ }
317
+ catch (error) {
318
+ if (error.code !== "EEXIST")
319
+ throw error;
320
+ if (Date.now() >= deadline)
321
+ throw new OrcaError(family, `watch owner record ${path} lock not acquired; current record kept`, "");
322
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
323
+ }
324
+ }
325
+ try {
326
+ let current;
327
+ try {
328
+ current = readFileSync(path, "utf8");
329
+ }
330
+ catch (error) {
331
+ if (error.code !== "ENOENT")
332
+ throw error;
333
+ }
334
+ if (current !== expected)
335
+ throw refused();
336
+ const tmp = `${path}.${randomUUID()}.tmp`;
337
+ writeFileSync(tmp, raw);
338
+ try {
339
+ if (expected === undefined) {
340
+ try {
341
+ linkSync(tmp, path);
342
+ }
343
+ catch {
344
+ throw refused();
345
+ }
346
+ }
347
+ else {
348
+ renameSync(tmp, path);
349
+ }
350
+ }
351
+ finally {
352
+ rmSync(tmp, { force: true });
353
+ }
354
+ return raw;
355
+ }
356
+ finally {
357
+ rmSync(lock, { recursive: true, force: true });
358
+ }
359
+ }
360
+ function pidLive(pid) {
361
+ try {
362
+ process.kill(pid, 0);
363
+ return true;
364
+ }
365
+ catch (error) {
366
+ return error.code === "EPERM";
367
+ }
368
+ }
369
+ function terminalRows(env) {
370
+ const rows = env.result.terminals;
371
+ if (!Array.isArray(rows))
372
+ throw new OrcaError("list", "list response carries no terminals array", env.raw);
373
+ return rows.filter((r) => typeof r === "object" && r !== null);
374
+ }
375
+ function listsHandle(env, handle) {
376
+ return terminalRows(env).some((r) => str(r.handle) === handle);
377
+ }
378
+ /** A handle listed by a different runtime is not the recorded pane — Orca handles are runtime-scoped. */
379
+ function listedOnRuntime(env, handle, runtimeId) {
380
+ return typeof runtimeId === "string" && env.runtimeId === runtimeId && listsHandle(env, handle);
381
+ }
382
+ function terminalTabId(env, handle) {
383
+ for (const row of terminalRows(env)) {
384
+ if (str(row.handle) === handle)
385
+ return str(row.tabId);
386
+ }
387
+ return undefined;
388
+ }
389
+ /**
390
+ * Orca 1.4.200 split contract: child at `result.split.handle`, parent tab at `result.split.tabId`.
391
+ * A receipt that only happens to contain a handle is malformed — it does not establish that the
392
+ * child belongs to the launching terminal's tab.
393
+ */
394
+ function splitReceipt(env) {
395
+ const receipt = env.result.split;
396
+ if (typeof receipt !== "object" || receipt === null || Array.isArray(receipt)) {
397
+ throw new OrcaError("split", "split receipt is unknown, malformed or handle-less", env.raw);
398
+ }
399
+ const rec = receipt;
400
+ const handle = str(rec.handle);
401
+ const tabId = str(rec.tabId);
402
+ if (!handle || !tabId) {
403
+ throw new OrcaError("split", "split receipt is unknown, malformed or handle-less", env.raw);
404
+ }
405
+ return { handle, tabId };
406
+ }
407
+ function watchSlot(cwd, name, handle) {
408
+ return { id: handle, name, cwd: canonicalWorktreePath(cwd) };
409
+ }
410
+ function errorText(error) {
411
+ return error instanceof Error ? error.message : String(error);
412
+ }
207
413
  /** Terminal-pane handles under a layout tab node: a pane object, an array of them, or a nested
208
414
  * group/split carrying `panes`, `first`, `second`. Only `type:"terminal"` leaves count — anything else is chrome. */
209
- function collectPaneHandles(node, out) {
415
+ function collectLeaves(node, out) {
210
416
  if (Array.isArray(node)) {
211
417
  for (const n of node)
212
- collectPaneHandles(n, out);
418
+ collectLeaves(n, out);
213
419
  return;
214
420
  }
215
421
  if (typeof node !== "object" || node === null)
@@ -218,12 +424,18 @@ function collectPaneHandles(node, out) {
218
424
  if (o.type === "terminal") {
219
425
  const h = str(o.handle);
220
426
  if (h)
221
- out.push(h);
427
+ out.push({ handle: h, title: str(o.title) });
222
428
  return;
223
429
  }
224
- collectPaneHandles(o.panes, out);
225
- collectPaneHandles(o.first, out);
226
- collectPaneHandles(o.second, out);
430
+ collectLeaves(o.panes, out);
431
+ collectLeaves(o.first, out);
432
+ collectLeaves(o.second, out);
433
+ }
434
+ function collectPaneHandles(node, out) {
435
+ const leaves = [];
436
+ collectLeaves(node, leaves);
437
+ for (const l of leaves)
438
+ out.push(l.handle);
227
439
  }
228
440
  function collectTabs(node, out) {
229
441
  if (Array.isArray(node)) {
@@ -266,8 +478,19 @@ export class OrcaDriver {
266
478
  narrate;
267
479
  hookCoverage;
268
480
  taskWorktrees = new Map();
481
+ trackedByCheckout = new Map(); // OBS-1004: checkout → the tracked worktree enclosing it
269
482
  pendingProjects = new Map();
483
+ env;
484
+ launchingHandle;
485
+ serialQueue = Promise.resolve();
486
+ serial(fn) {
487
+ const next = this.serialQueue.then(fn, fn);
488
+ this.serialQueue = next.then(() => { }, () => { });
489
+ return next;
490
+ }
270
491
  constructor(opts = {}) {
492
+ this.env = opts.env ?? process.env;
493
+ this.launchingHandle = opts.launchingHandle ?? this.env.ORCA_TERMINAL_HANDLE;
271
494
  this.bin = opts.bin ?? resolveOrcaCliBinary(process.cwd(), { env: opts.env, platform: opts.platform }) ?? "orca";
272
495
  // Config values flow into a shell here: every argv element is quoted, always.
273
496
  this.exec = opts.exec ?? ((args, cwd, timeoutMs) => {
@@ -437,20 +660,23 @@ export class OrcaDriver {
437
660
  }
438
661
  async create(st, cmd) {
439
662
  await this.probeRuntime(this.cliCwd(st));
440
- await this.awaitWorktreeAdoption(st);
441
- // The selector names THIS slot's checkout outright, and the CLI child is bound to it too, so
442
- // neither the UI's active worktree (`active`/`current`) nor the daemon's cwd can place it.
663
+ const tracked = await this.trackedWorktree(st.cwd);
664
+ st.tracked = tracked;
665
+ // The selector names the TRACKED worktree enclosing this slot's checkout outright — never the
666
+ // UI's active worktree (`active`/`current`) nor the daemon's cwd — and the command itself moves
667
+ // into the checkout, because Orca cannot be asked to place a terminal in a path it does not
668
+ // track (OBS-1004). `--command` runs inside Orca's wrapper shell, so a `cd` prefix is honoured.
443
669
  const env = await this.call("create", [
444
- "terminal", "create", "--worktree", `path:${st.cwd}`, "--title", st.title, "--command", cmd,
670
+ "terminal", "create", "--worktree", `path:${tracked}`, "--title", st.title, "--command", inCheckout(st.cwd, cmd),
445
671
  ], this.cliCwd(st));
446
672
  const term = requireTerminal("create", env);
447
673
  const handle = str(term.handle);
448
674
  if (!handle)
449
675
  throw new OrcaError("create", "create receipt carries no terminal handle", env.raw);
450
- // Asking is not getting: the receipt says which checkout the runtime actually resolved, and a
451
- // terminal in the wrong one has already lost the isolation this run is built on.
676
+ // Asking is not getting: the receipt says which worktree the runtime actually resolved, and a
677
+ // terminal bound elsewhere has already lost the isolation this run is built on.
452
678
  const worktree = terminalWorktree(term);
453
- if (!sameWorktree(worktree, st.cwd)) {
679
+ if (!sameWorktree(worktree, tracked)) {
454
680
  // `terminal create` has ALREADY launched the command in that wrong checkout, so refusing the
455
681
  // receipt is not yet fail-closed: the agent keeps mutating it. Close the exact handle this
456
682
  // receipt named, under the runtime that answered it (closeTerminal re-proves that identity
@@ -461,7 +687,7 @@ export class OrcaDriver {
461
687
  await this.closeTerminal({ ...st, handle, runtimeId: env.runtimeId });
462
688
  }
463
689
  catch { /* already gone, or no longer provably ours — never a blind retry */ }
464
- throw this.latched("create", st, `create receipt bound to ${worktree ?? "no worktree"}, not the slot's ${st.cwd}`, env.raw);
690
+ throw this.latched("create", st, `create receipt bound to ${worktree ?? "no worktree"}, not the tracked ${tracked} enclosing ${st.cwd}`, env.raw);
465
691
  }
466
692
  st.handle = handle;
467
693
  // The handle is bound to the runtime identity that ANSWERED its create.
@@ -473,62 +699,30 @@ export class OrcaDriver {
473
699
  }
474
700
  }
475
701
  /**
476
- * A freshly-created git checkout does not become a valid Orca selector atomically. Ask
477
- * `worktree current` FROM that checkout until Orca itself resolves the exact filesystem identity;
478
- * an enclosing checkout is still not adoption. Only selector_not_found is a retryable refusal —
479
- * malformed envelopes and every other refusal remain explicit driver failures.
702
+ * OBS-1004: the tracked worktree that encloses a checkout, asked ONCE of `worktree current` from
703
+ * inside that checkout. Orca answers the exact path when it tracks the checkout itself, the
704
+ * enclosing tracked clone for a git worktree the daemon added beneath it (1.4.200, verified from
705
+ * `.tickmarkr/worktrees.noindex/<task>`), and selector_not_found when nothing it tracks encloses
706
+ * the cwd — which is a driver failure, not something to wait out: Orca has no adopt verb.
480
707
  */
481
- async awaitWorktreeAdoption(st) {
482
- const started = this.time.now();
483
- const deadline = started + WORKTREE_ADOPTION_TIMEOUT_MS;
484
- let lastReported;
485
- for (;;) {
486
- const left = deadline - this.time.now();
487
- if (left < 0)
488
- break;
489
- try {
490
- const env = await this.call("worktree-current", ["worktree", "current"], st.cwd, Math.max(1, left));
491
- const worktree = env.result.worktree;
492
- if (typeof worktree !== "object" || worktree === null || Array.isArray(worktree)) {
493
- throw new OrcaError("worktree-current", "response carries no worktree record", env.raw, { runtimeId: env.runtimeId });
494
- }
495
- const reported = terminalWorktree(worktree);
496
- if (!reported) {
497
- throw new OrcaError("worktree-current", "worktree record carries no path", env.raw, { runtimeId: env.runtimeId });
498
- }
499
- lastReported = canonicalWorktreePath(reported);
500
- if (lastReported === st.cwd) {
501
- const waitedMs = this.time.now() - started;
502
- if (waitedMs > WORKTREE_ADOPTION_JOURNAL_MS)
503
- this.appendAdoptionWait(st, waitedMs);
504
- return;
505
- }
506
- }
507
- catch (error) {
508
- if (!(error instanceof OrcaError) || error.code !== "selector_not_found")
509
- throw error;
510
- lastReported = undefined;
511
- }
512
- const remaining = deadline - this.time.now();
513
- if (remaining <= 0)
514
- break;
515
- await this.time.sleep(Math.min(WORKTREE_ADOPTION_POLL_MS, remaining));
708
+ async trackedWorktree(checkout) {
709
+ const cached = this.trackedByCheckout.get(checkout);
710
+ if (cached)
711
+ return cached;
712
+ const env = await this.call("worktree-current", ["worktree", "current"], checkout);
713
+ const worktree = env.result.worktree;
714
+ if (typeof worktree !== "object" || worktree === null || Array.isArray(worktree)) {
715
+ throw new OrcaError("worktree-current", "response carries no worktree record", env.raw, { runtimeId: env.runtimeId });
516
716
  }
517
- const waitedMs = this.time.now() - started;
518
- throw new OrcaUnavailableError("worktree-current", `Orca did not adopt ${st.cwd} within ${WORKTREE_ADOPTION_TIMEOUT_MS}ms${lastReported ? ` (last answered ${lastReported})` : ""}`, `waitedMs=${waitedMs}`);
519
- }
520
- /** Same repo/run/narration path Herdr uses for its driver-owned dispatch-retry row. */
521
- appendAdoptionWait(st, waitedMs) {
522
- const owned = parseOwnedName(st.title);
523
- if (!owned)
524
- throw new Error(`cannot journal worktree-adoption-wait: slot ${st.title} carries no run identity`);
525
- const repoRoot = this.journalRoots.get(st.cwd);
526
- if (!repoRoot) {
527
- throw new Error(`cannot journal worktree-adoption-wait: slot ${st.title} has no daemon repo binding for ${st.cwd}`);
717
+ const reported = terminalWorktree(worktree);
718
+ if (!reported)
719
+ throw new OrcaError("worktree-current", "worktree record carries no path", env.raw, { runtimeId: env.runtimeId });
720
+ const tracked = canonicalWorktreePath(reported);
721
+ if (tracked !== checkout && !checkout.startsWith(`${tracked}/`)) {
722
+ throw new OrcaError("worktree-current", `Orca answered ${tracked}, which does not enclose ${checkout}`, env.raw, { runtimeId: env.runtimeId });
528
723
  }
529
- Journal.open(repoRoot, owned.runId, this.narrate).append("worktree-adoption-wait", owned.taskId, {
530
- milliseconds: waitedMs,
531
- });
724
+ this.trackedByCheckout.set(checkout, tracked);
725
+ return tracked;
532
726
  }
533
727
  // ---- handle identity and restart recovery ----------------------------------------------------
534
728
  /**
@@ -605,6 +799,65 @@ export class OrcaDriver {
605
799
  return env;
606
800
  }
607
801
  }
802
+ /**
803
+ * FX-N01/N05/N06: under a shared enclosing worktree every task terminal lists with the same
804
+ * worktreePath, so the tracked path + owned title cannot tell two nested checkouts apart. The
805
+ * runtime's own proof is the terminal's earliest scrollback, where the create command printed a
806
+ * framed `TICKMARKR_CHECKOUT` line before its payload (checkoutProofLine). READ-only calls: the
807
+ * anchor (for `oldestCursor`), then pages from the oldest cursor until a frame is complete or the
808
+ * bound is hit. Every page is evidence only when the response's own identity is the candidate's:
809
+ * the terminal record must name `handle` and `_meta.runtimeId` must be `runtimeId` — the runtime
810
+ * that supplied the ownership listing — else another terminal's or another runtime's bytes were
811
+ * answered and nothing is proven. Proven means provesCheckout: exact canonical full-path equality
812
+ * of a complete frame, no other checkout named, no incomplete frame.
813
+ */
814
+ async checkoutProven(handle, checkout, from, runtimeId) {
815
+ const page = async (cursor) => {
816
+ const env = await this.call("read", [
817
+ "terminal", "read", "--terminal", handle, ...(cursor === undefined ? [] : ["--cursor", cursor]), "--limit", String(this.pageLines),
818
+ ], from);
819
+ if (env.runtimeId !== runtimeId) {
820
+ throw new OrcaError("read", `proof page answered by runtime ${env.runtimeId}, not the listing's ${runtimeId}`, env.raw);
821
+ }
822
+ const term = requireTerminal("read", env);
823
+ if (str(term.handle) !== handle) {
824
+ throw new OrcaError("read", `proof page names terminal ${str(term.handle) ?? "none"}, not the candidate ${handle}`, env.raw);
825
+ }
826
+ return term;
827
+ };
828
+ const lines = (term) => Array.isArray(term.tail) ? term.tail.filter((l) => typeof l === "string") : [];
829
+ try {
830
+ const anchor = await page();
831
+ let cursor = str(anchor.oldestCursor);
832
+ let text = cursor === undefined ? lines(anchor).join("\n") : "";
833
+ for (let n = 0; cursor !== undefined && n < PROOF_PAGES; n++) {
834
+ const term = await page(cursor);
835
+ text += `${lines(term).join("\n")}\n`;
836
+ const frames = checkoutFrames(text);
837
+ if (frames.complete.length > 0 && !frames.incomplete)
838
+ break; // whole frames, nothing dangling
839
+ const next = str(term.nextCursor);
840
+ if (term.limited !== true || next === undefined || next === cursor)
841
+ break;
842
+ cursor = next;
843
+ }
844
+ const frames = checkoutFrames(text);
845
+ // Reconcile has no task-checkout path after a daemon restart, but the proof itself remains
846
+ // an ownership record: exactly one complete, unambiguous checkout frame can only have been
847
+ // written by tickmarkr's create command. Slot recovery additionally requires its exact path.
848
+ if (checkout === undefined
849
+ ? !frames.incomplete && frames.complete.length > 0 && new Set(frames.complete).size === 1
850
+ : provesCheckout(text, checkout))
851
+ return { proven: true, reason: "" };
852
+ return {
853
+ proven: false,
854
+ reason: `its scrollback ${frames.complete.length ? `names ${[...new Set(frames.complete)].join(", ")}` : "names no checkout"}${frames.incomplete ? " and carries an incomplete proof frame" : ""}`,
855
+ };
856
+ }
857
+ catch (error) {
858
+ return { proven: false, reason: error instanceof Error ? error.message : String(error) };
859
+ }
860
+ }
608
861
  async recover(family, st, newRuntimeId, raw) {
609
862
  if (st.recovering)
610
863
  throw this.latched(family, st, "handle recovery re-entered", raw);
@@ -613,7 +866,8 @@ export class OrcaDriver {
613
866
  const old = st.handle;
614
867
  // visualLayouts is required: the owned title survives at TAB identity only, and rows carry
615
868
  // just the shell-controlled pane title (recorded: "…probe…" at create → "bash" on the row).
616
- const env = await this.call("list", ["terminal", "list", "--worktree", `path:${st.cwd}`, "--include-visual-layouts", "--limit", String(LIST_LIMIT)], this.cliCwd(st));
869
+ const home = st.tracked ?? st.cwd; // the worktree Orca placed this terminal in (OBS-1004)
870
+ const env = await this.call("list", ["terminal", "list", "--worktree", `path:${home}`, "--include-visual-layouts", "--limit", String(LIST_LIMIT)], this.cliCwd(st));
617
871
  const listed = env.result.terminals;
618
872
  if (!Array.isArray(listed))
619
873
  throw new OrcaError("list", "list response carries no terminals array", env.raw);
@@ -625,7 +879,7 @@ export class OrcaDriver {
625
879
  // The worktree-authoritative rows of THIS worktree: a handle is adoptable only if a row in
626
880
  // the slot's exact worktree backs it. A same-titled tab in another worktree is a lookalike.
627
881
  const inWorktree = new Set(listed
628
- .filter((t) => typeof t === "object" && t !== null && sameWorktree(terminalWorktree(t), st.cwd))
882
+ .filter((t) => typeof t === "object" && t !== null && sameWorktree(terminalWorktree(t), home))
629
883
  .map((t) => str(t.handle))
630
884
  .filter((h) => h !== undefined));
631
885
  // Exactly one tab carrying the FULL owned title, resolving to exactly one terminal pane.
@@ -636,7 +890,7 @@ export class OrcaDriver {
636
890
  if (typeof layout !== "object" || layout === null)
637
891
  continue;
638
892
  const lo = layout;
639
- if (!sameWorktree(terminalWorktree(lo), st.cwd))
893
+ if (!sameWorktree(terminalWorktree(lo), home))
640
894
  continue; // another worktree's tabs are never candidates
641
895
  const tabs = [];
642
896
  collectTabs(lo.root, tabs);
@@ -649,14 +903,14 @@ export class OrcaDriver {
649
903
  }
650
904
  }
651
905
  if (ownedTabs.length === 0) {
652
- throw this.latched(family, st, `no tab in ${st.cwd} carries the owned title ${st.title} (row titles are shell-controlled and are never ownership keys)`, env.raw);
906
+ throw this.latched(family, st, `no tab in ${home} carries the owned title ${st.title} (row titles are shell-controlled and are never ownership keys)`, env.raw);
653
907
  }
654
908
  if (ownedTabs.length > 1) {
655
- throw this.latched(family, st, `${ownedTabs.length} tabs in ${st.cwd} carry the owned title ${st.title} — ambiguous`, env.raw);
909
+ throw this.latched(family, st, `${ownedTabs.length} tabs in ${home} carry the owned title ${st.title} — ambiguous`, env.raw);
656
910
  }
657
911
  const panes = ownedTabs[0].filter((h) => inWorktree.has(h));
658
912
  if (panes.length !== 1) {
659
- throw this.latched(family, st, `the owned tab resolves to ${panes.length} terminals in ${st.cwd}`, env.raw);
913
+ throw this.latched(family, st, `the owned tab resolves to ${panes.length} terminals in ${home}`, env.raw);
660
914
  }
661
915
  const handle = panes[0];
662
916
  if (handle === old) {
@@ -664,6 +918,14 @@ export class OrcaDriver {
664
918
  // which terminal it addresses, so it is never adopted.
665
919
  throw this.latched(family, st, `replacement handle ${handle} is the old handle value reused by runtime ${newRuntimeId ?? env.runtimeId ?? "unknown"}`, env.raw);
666
920
  }
921
+ if (st.tracked !== undefined && st.tracked !== st.cwd) {
922
+ // FX-N01: a nested checkout shares its worktreePath with every sibling task's terminal, so
923
+ // the candidate must prove the checkout itself (read-only) before it is addressed as ours.
924
+ const proof = await this.checkoutProven(handle, st.cwd, this.cliCwd(st), env.runtimeId);
925
+ if (!proof.proven) {
926
+ throw this.latched(family, st, `candidate ${handle} does not prove checkout ${st.cwd} (${proof.reason}); the sole same-titled tab on ${home} is not adopted`, env.raw);
927
+ }
928
+ }
667
929
  st.handle = handle;
668
930
  // The list response is the identity proof for the replacement. A stale refusal may have been
669
931
  // produced by either side of a restart; its metadata never overrides the runtime that relisted.
@@ -997,12 +1259,181 @@ export class OrcaDriver {
997
1259
  return false;
998
1260
  }
999
1261
  }
1000
- async narrator(_cwd, _command, runId) {
1262
+ async narrator(cwd, command, runId) {
1001
1263
  if (!runId)
1002
- throw new OrcaError("create", "Orca narrator requires a run identity", "");
1003
- // The recorded Orca API can create a tab but provides no right/no-focus
1004
- // placement receipt. Creating one would advertise a board we did not place.
1005
- throw new OrcaError("create", "Orca narrator placement unsupported: right/no-focus board placement is not available", "");
1264
+ throw new OrcaError("split", "Orca narrator requires a run identity", "");
1265
+ const launchingHandle = (this.launchingHandle ?? this.env.ORCA_TERMINAL_HANDLE)?.trim();
1266
+ if (!launchingHandle)
1267
+ throw new OrcaError("split", "Orca narrator requires ORCA_TERMINAL_HANDLE", "");
1268
+ return this.serial(async () => {
1269
+ const name = formatOwnedName({ role: "watch", taskId: "run", attempt: 0, runId });
1270
+ const path = this.boardPath(cwd, runId);
1271
+ const kept = `indeterminate cleanup refused — owner record ${path} kept`;
1272
+ let current = readBoard(path, cwd, runId);
1273
+ if (current && (current.record.driver !== this.id || current.record.name !== name)) {
1274
+ // Never overwrite a record this driver did not create, unless its observer is provably gone.
1275
+ const { driver, pid } = current.record;
1276
+ if (typeof pid !== "number" || pidLive(pid)) {
1277
+ throw new OrcaError("split", `Orca narrator placement refused for ${name}: the record is held by driver ${driver} with a live or unclaimed observer; ${kept}`, "");
1278
+ }
1279
+ }
1280
+ else if (current) {
1281
+ const state = boardState(current.record);
1282
+ if (state === "reserved" || state === "claimed") {
1283
+ throw new OrcaError("split", `Orca narrator placement remains unresolved for ${name} (${state}, no bound pane); ${kept}`, "");
1284
+ }
1285
+ if (state === "bound") {
1286
+ // The bound record is the durable truth of placement: the child handle PLUS the split
1287
+ // envelope's runtime identity. A handle listed by a later runtime is a different pane.
1288
+ if (listedOnRuntime(await this.listAll(cwd), current.record.pane, current.record.runtimeId)) {
1289
+ return watchSlot(cwd, name, current.record.pane);
1290
+ }
1291
+ // Lost: this runtime no longer has that pane. Tombstone before anything else.
1292
+ const retired = { ...current.record, retired: true };
1293
+ current = { raw: casBoard("split", path, current.raw, retired), record: retired };
1294
+ }
1295
+ // A tombstone can be an acknowledgement timeout, not completed observer cleanup. A live
1296
+ // observer must stop and ack on injected time before the handle-bound close; timeout keeps
1297
+ // the tombstone and does not replace it.
1298
+ try {
1299
+ if (typeof current.record.pid === "number" && pidLive(current.record.pid)) {
1300
+ await stopWatchBoard(current.record, this.time);
1301
+ }
1302
+ else {
1303
+ requestWatchBoardStop(current.record);
1304
+ }
1305
+ }
1306
+ catch (error) {
1307
+ throw new OrcaError("split", `Orca narrator placement refused for ${name}: retired board observer unacknowledged; ${kept}; ${errorText(error)}`, "");
1308
+ }
1309
+ try {
1310
+ await this.closeRecordedPane(cwd, name, current.record.pane, current.record.runtimeId);
1311
+ }
1312
+ catch (error) {
1313
+ throw new OrcaError("split", `Orca narrator placement refused for ${name}: retired board pane ${current.record.pane} not proven closed; ${kept}; ${errorText(error)}`, "");
1314
+ }
1315
+ }
1316
+ // Reserve before any command can read the token: create-only, or a swap of the exact record
1317
+ // judged replaceable above.
1318
+ const token = randomUUID();
1319
+ casBoard("split", path, current?.raw, { repo: realpathSync(cwd), runId, driver: this.id, workspace: ORCA_SPACE, pane: "", name, token });
1320
+ // Exactly one terminal split of the launching handle, horizontal, carrying token and command.
1321
+ // An unknown receipt (transport failure, refusal, unparseable output) after the verb was issued
1322
+ // is as indeterminate as a handle-less one: the pane may exist, so the reservation stays.
1323
+ const env = await this.call("split", [
1324
+ "terminal", "split",
1325
+ "--terminal", launchingHandle,
1326
+ "--direction", "horizontal",
1327
+ "--command", `${WATCH_OWNER_ENV}=${shq(token)} ${command}`,
1328
+ ], cwd).catch((error) => {
1329
+ throw new OrcaError("split", `Orca narrator placement failed for ${name}: split receipt is unknown (${error instanceof OrcaError ? error.reason : errorText(error)}); ${kept}`, error instanceof OrcaError ? error.raw : "");
1330
+ });
1331
+ let child;
1332
+ try {
1333
+ child = splitReceipt(env);
1334
+ }
1335
+ catch {
1336
+ throw new OrcaError("split", `Orca narrator placement failed for ${name}: split receipt is unknown, malformed or handle-less; ${kept}`, env.raw);
1337
+ }
1338
+ // Parent tabId must name the launching terminal's tab, and the child must actually appear
1339
+ // there. A handle-only object, a tabId for some other tab, or a handle the list does not
1340
+ // place in that tab is malformed — closing it would be a guessed handle.
1341
+ let listing;
1342
+ try {
1343
+ listing = await this.listAll(cwd);
1344
+ }
1345
+ catch (error) {
1346
+ throw new OrcaError("split", `Orca narrator placement failed for ${name}: split receipt is unknown, malformed or handle-less; ${kept}`, error instanceof OrcaError ? error.raw : env.raw);
1347
+ }
1348
+ const launchingTabId = terminalTabId(listing, launchingHandle);
1349
+ if (!launchingTabId
1350
+ || child.tabId !== launchingTabId
1351
+ || child.handle === launchingHandle
1352
+ || terminalTabId(listing, child.handle) !== launchingTabId) {
1353
+ throw new OrcaError("split", `Orca narrator placement failed for ${name}: split receipt is unknown, malformed or handle-less; ${kept}`, env.raw);
1354
+ }
1355
+ const childHandle = child.handle;
1356
+ // The narrator writes nothing until the observer's single claim is visible, so neither write
1357
+ // can erase the other; after it the observer never writes the record again.
1358
+ const claimDeadline = this.time.now() + 5000;
1359
+ let claim;
1360
+ for (;;) {
1361
+ const check = readBoard(path, cwd, runId);
1362
+ if (check?.record.token !== token)
1363
+ break;
1364
+ if (boardState(check.record) === "claimed") {
1365
+ claim = check;
1366
+ break;
1367
+ }
1368
+ if (this.time.now() > claimDeadline)
1369
+ break;
1370
+ await this.time.sleep(20);
1371
+ }
1372
+ if (!claim) {
1373
+ // The receipt's handle may be closed, but the refused placement's record stays as it is.
1374
+ let pane;
1375
+ try {
1376
+ await this.closeRecordedPane(cwd, name, childHandle, env.runtimeId);
1377
+ pane = `its pane ${childHandle} was closed`;
1378
+ }
1379
+ catch (error) {
1380
+ pane = `its pane ${childHandle} was not proven closed (${errorText(error)})`;
1381
+ }
1382
+ throw new OrcaError("split", `Orca narrator board unclaimed for ${name}: the observer never claimed the record (unclaimed board); ${kept}; ${pane}`, env.raw);
1383
+ }
1384
+ casBoard("split", path, claim.raw, { ...claim.record, pane: childHandle, runtimeId: env.runtimeId });
1385
+ return watchSlot(cwd, name, childHandle);
1386
+ });
1387
+ }
1388
+ boardPath(cwd, runId) {
1389
+ return join(cwd, stateDirName(cwd), "supervision", `watch-board.${parseRunId(runId)}.json`);
1390
+ }
1391
+ /** A recorded pane (a receipt's handle bound to the split envelope's runtime, never a guess) is
1392
+ * gone when that runtime no longer lists it, or when a handle-bound close receipt names it.
1393
+ * A handle listed by a different runtime is a different pane — not closed, treated as gone. */
1394
+ async closeRecordedPane(cwd, name, handle, runtimeId) {
1395
+ const listing = await this.listAll(cwd);
1396
+ if (!listedOnRuntime(listing, handle, runtimeId))
1397
+ return;
1398
+ await this.closeTerminal({
1399
+ title: name, cwd: canonicalWorktreePath(cwd), handle, runtimeId, buf: "", recoveries: 0, recovering: false,
1400
+ });
1401
+ }
1402
+ /** bound → retired, decided from the record alone: it must be this driver's board for exactly this
1403
+ * slot's pane. Already retired is returned as it is. */
1404
+ retireBoard(family, slot) {
1405
+ const runId = parseOwnedName(slot.name)?.runId;
1406
+ const path = runId ? this.boardPath(slot.cwd, runId) : undefined;
1407
+ const current = path && runId ? readBoard(path, slot.cwd, runId) : undefined;
1408
+ const r = current?.record;
1409
+ if (!path || !current || !r || r.driver !== this.id || r.name !== slot.name || r.pane !== slot.id || boardState(r) === "reserved" || boardState(r) === "claimed") {
1410
+ throw new OrcaError(family, `watch ownership unknown or foreign for ${slot.name}; existing board protected`, "");
1411
+ }
1412
+ if (r.retired)
1413
+ return r;
1414
+ const retired = { ...r, retired: true };
1415
+ casBoard(family, path, current.raw, retired);
1416
+ return retired;
1417
+ }
1418
+ /** bound → retired first: whatever fails below, no later call answers this board again. A live
1419
+ * observer is asked to stop and its acknowledgement awaited on injected time before the
1420
+ * handle-bound close (timeout keeps the tombstone and the pane); a dead one never acknowledges,
1421
+ * so it is only asked. */
1422
+ async retireAndClose(slot) {
1423
+ await this.serial(async () => {
1424
+ const retired = this.retireBoard("close", slot);
1425
+ if (typeof retired.pid === "number" && pidLive(retired.pid))
1426
+ await stopWatchBoard(retired, this.time);
1427
+ else
1428
+ requestWatchBoardStop(retired);
1429
+ await this.closeRecordedPane(slot.cwd, slot.name, slot.id, retired.runtimeId);
1430
+ });
1431
+ }
1432
+ /** WB-1 seam: the daemon reports this board lost. "Lost" can be a stale beat or missing presence
1433
+ * under a still-live owner pid, so it is not proof of a dead observer — retirement keeps close's
1434
+ * acknowledgement discipline (Leg-2 T9 P1). */
1435
+ async retireLostWatch(slot) {
1436
+ await this.retireAndClose(slot);
1006
1437
  }
1007
1438
  async focus(target) {
1008
1439
  const { slot, runId, taskId, attempt } = target;
@@ -1021,8 +1452,8 @@ export class OrcaDriver {
1021
1452
  if (typeof layout !== "object" || layout === null)
1022
1453
  continue;
1023
1454
  const lo = layout;
1024
- if (!sameWorktree(terminalWorktree(lo), cwd))
1025
- continue;
1455
+ if (!enclosesCheckout(terminalWorktree(lo), cwd))
1456
+ continue; // the tracked worktree Orca placed it in (OBS-1004)
1026
1457
  const tabs = [];
1027
1458
  collectTabs(lo.root, tabs);
1028
1459
  for (const tab of tabs) {
@@ -1032,9 +1463,15 @@ export class OrcaDriver {
1032
1463
  }
1033
1464
  if (handles.length !== 1)
1034
1465
  return { status: rows.length ? "foreign" : "closed", reason: "No unique owned terminal in the recorded worktree" };
1035
- const matches = rows.filter(row => typeof row === "object" && row !== null && str(row.handle) === handles[0] && sameWorktree(terminalWorktree(row), cwd));
1466
+ const matches = rows.filter(row => typeof row === "object" && row !== null && str(row.handle) === handles[0] && enclosesCheckout(terminalWorktree(row), cwd));
1036
1467
  if (matches.length !== 1)
1037
1468
  return { status: "foreign", reason: "Terminal worktree ownership is unverified" };
1469
+ if (!sameWorktree(terminalWorktree(matches[0]), cwd)) {
1470
+ // FX-N01: the row only ENCLOSES the recorded checkout — require the terminal's own proof line.
1471
+ const proof = await this.checkoutProven(handles[0], cwd, cwd, env.runtimeId);
1472
+ if (!proof.proven)
1473
+ return { status: "foreign", reason: `Terminal checkout ownership is unverified: ${proof.reason}` };
1474
+ }
1038
1475
  if (matches[0].connected === false || matches[0].orphaned === true)
1039
1476
  return { status: "closed", reason: "Recorded terminal is no longer running; open task evidence" };
1040
1477
  return { status: "unsupported", reason: "Owned terminal verified; this Orca API has no focus operation. Open task evidence with Enter" };
@@ -1053,12 +1490,15 @@ export class OrcaDriver {
1053
1490
  }
1054
1491
  await this.setWorkspaceStatus(worktree, state);
1055
1492
  }
1056
- async setWorkspaceStatus(worktree, state) {
1493
+ async setWorkspaceStatus(checkout, state) {
1494
+ // OBS-1004: the task checkout is not an Orca selector (`selector_not_found`, recorded on run
1495
+ // 0004's T5); the projection lands on the tracked worktree that encloses it.
1496
+ const tracked = await this.trackedWorktree(checkout);
1057
1497
  // UNRECORDED SHAPE: no Orca 1.4.195 `worktree set` receipt was captured. The shared envelope
1058
1498
  // parser is the complete success proof here; no result payload is assumed or fabricated.
1059
1499
  await this.call("worktree-set", [
1060
- "worktree", "set", "--worktree", `path:${worktree}`, "--workspace-status", state,
1061
- ], worktree);
1500
+ "worktree", "set", "--worktree", `path:${tracked}`, "--workspace-status", state,
1501
+ ], checkout);
1062
1502
  }
1063
1503
  async notify(msg, opts) {
1064
1504
  if (opts?.tier === "routine")
@@ -1069,11 +1509,16 @@ export class OrcaDriver {
1069
1509
  this.narrate = narrate;
1070
1510
  }
1071
1511
  async close(slot) {
1512
+ if (parseOwnedName(slot.name)?.role === "watch") {
1513
+ await this.retireAndClose(slot);
1514
+ return;
1515
+ }
1072
1516
  const st = this.slots.get(slot.id);
1073
1517
  if (!st)
1074
1518
  return;
1075
- if (st.handle)
1519
+ if (st.handle) {
1076
1520
  await this.closeTerminal(st);
1521
+ }
1077
1522
  this.slots.delete(slot.id);
1078
1523
  }
1079
1524
  /**
@@ -1204,10 +1649,24 @@ export class OrcaDriver {
1204
1649
  const title = str(t.title);
1205
1650
  if (!title)
1206
1651
  continue;
1207
- const handles = [];
1208
- collectPaneHandles(t.panes, handles); // a split tab holds more than one, and both are ours
1209
- for (const handle of handles)
1210
- candidates.set(handle, { title, worktree: canonicalWorktreePath(worktree) });
1652
+ const leaves = [];
1653
+ collectLeaves(t.panes, leaves);
1654
+ const launching = (this.launchingHandle ?? this.env.ORCA_TERMINAL_HANDLE)?.trim();
1655
+ // A tab title belongs to the tab, not to a leaf. After the operator moves the worker
1656
+ // out, a foreign shell can sit alone under that owned title — so every leaf, including
1657
+ // the only leaf of a single-leaf tab, needs the durable checkout proof. A title that
1658
+ // does not parse is never a candidate.
1659
+ if (!parseOwnedName(title))
1660
+ continue;
1661
+ for (const leaf of leaves) {
1662
+ if (launching && leaf.handle === launching)
1663
+ continue;
1664
+ if (this.isRecordedWatchHandle(leaf.handle, worktree, runId, env.runtimeId))
1665
+ continue;
1666
+ if (!await this.isRecordedWorkerHandle(leaf.handle, from, env.runtimeId))
1667
+ continue;
1668
+ candidates.set(leaf.handle, { title, worktree: canonicalWorktreePath(worktree) });
1669
+ }
1211
1670
  }
1212
1671
  }
1213
1672
  const toClose = panesToClose([...candidates].map(([paneId, c]) => ({ name: c.title, paneId, workspaceId: ORCA_SPACE })), desired, ORCA_SPACE, runId, opts);
@@ -1232,6 +1691,36 @@ export class OrcaDriver {
1232
1691
  }
1233
1692
  catch { /* cosmetic — visibility hygiene never fails the run */ }
1234
1693
  }
1694
+ isRecordedWatchHandle(handle, cwd, runId, runtimeId) {
1695
+ const matches = (record) => !!record && record.pane === handle && record.runtimeId === runtimeId;
1696
+ if (runId && matches(readWatchBoard(cwd, runId)))
1697
+ return true;
1698
+ try {
1699
+ const dir = join(cwd, stateDirName(cwd), "supervision");
1700
+ if (existsSync(dir)) {
1701
+ for (const f of readdirSync(dir)) {
1702
+ if (f.startsWith("watch-board.") && f.endsWith(".json")) {
1703
+ const content = JSON.parse(readFileSync(join(dir, f), "utf8"));
1704
+ if (content && typeof content === "object" && matches(content))
1705
+ return true;
1706
+ }
1707
+ }
1708
+ }
1709
+ }
1710
+ catch (error) {
1711
+ // Ownership uncertainty fails closed: abort this best-effort reconcile before it can treat a
1712
+ // recorded watch as an ordinary title-keyed worker. The outer reconcile boundary remains
1713
+ // cosmetic, but no close is attempted from a partial supervision-directory read.
1714
+ throw new OrcaError("list", `watch ownership unreadable in ${cwd}: ${errorText(error)}`, "");
1715
+ }
1716
+ return false;
1717
+ }
1718
+ async isRecordedWorkerHandle(handle, from, runtimeId) {
1719
+ // Do not use the driver's in-memory slots as the boundary: reconcile is also responsible for
1720
+ // terminals made before this driver process started. `create()` writes this proof before its
1721
+ // worker payload, and checkoutProven reads it from Orca rather than trusting a fixture handle.
1722
+ return (await this.checkoutProven(handle, undefined, from, runtimeId)).proven;
1723
+ }
1235
1724
  // tickmarkr's own createWorktree stays the sole checkout authority — orca never makes worktrees.
1236
1725
  async worktree(repo, branch, baseRef) {
1237
1726
  const worktree = await createWorktree(repo, branch, baseRef);