tickmarkr 2.6.1 → 2.6.2

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 (85) hide show
  1. package/README.md +14 -3
  2. package/dist/adapters/catalog-remote.js +89 -47
  3. package/dist/adapters/claude-code.js +9 -6
  4. package/dist/adapters/codex.js +7 -4
  5. package/dist/adapters/prompt.d.ts +1 -0
  6. package/dist/adapters/prompt.js +14 -6
  7. package/dist/adapters/registry.js +3 -3
  8. package/dist/adapters/types.d.ts +12 -4
  9. package/dist/adapters/types.js +6 -0
  10. package/dist/cli/commands/approve.d.ts +5 -1
  11. package/dist/cli/commands/approve.js +66 -23
  12. package/dist/cli/commands/compile.js +13 -3
  13. package/dist/cli/commands/doctor.d.ts +2 -0
  14. package/dist/cli/commands/doctor.js +11 -3
  15. package/dist/cli/commands/fleet.js +45 -7
  16. package/dist/cli/commands/report.js +18 -2
  17. package/dist/cli/commands/resume.js +4 -2
  18. package/dist/cli/commands/status.js +24 -19
  19. package/dist/cli/help.d.ts +2 -0
  20. package/dist/cli/help.js +9 -2
  21. package/dist/config/config.d.ts +20 -0
  22. package/dist/config/config.js +47 -8
  23. package/dist/config/fleet-overlay.d.ts +1 -0
  24. package/dist/config/fleet-overlay.js +56 -0
  25. package/dist/drivers/orca.d.ts +26 -1
  26. package/dist/drivers/orca.js +193 -60
  27. package/dist/eval/canary.d.ts +2 -1
  28. package/dist/eval/canary.js +2 -2
  29. package/dist/eval/dispatch.js +1 -0
  30. package/dist/gates/acceptance.d.ts +2 -1
  31. package/dist/gates/acceptance.js +7 -2
  32. package/dist/gates/baseline.d.ts +12 -1
  33. package/dist/gates/baseline.js +11 -4
  34. package/dist/gates/llm.d.ts +5 -4
  35. package/dist/gates/llm.js +13 -13
  36. package/dist/gates/review.d.ts +8 -0
  37. package/dist/gates/review.js +40 -4
  38. package/dist/gates/run-gates.d.ts +2 -1
  39. package/dist/gates/run-gates.js +27 -13
  40. package/dist/gates/test-manifest.d.ts +3 -1
  41. package/dist/gates/test-manifest.js +9 -2
  42. package/dist/graph/schema.d.ts +2 -0
  43. package/dist/graph/schema.js +2 -0
  44. package/dist/plan/scope.js +2 -2
  45. package/dist/route/preference.d.ts +20 -2
  46. package/dist/route/preference.js +48 -13
  47. package/dist/route/router.js +30 -15
  48. package/dist/run/consult.d.ts +13 -1
  49. package/dist/run/consult.js +14 -5
  50. package/dist/run/daemon.d.ts +37 -2
  51. package/dist/run/daemon.js +579 -141
  52. package/dist/run/git.d.ts +8 -0
  53. package/dist/run/git.js +14 -0
  54. package/dist/run/journal.d.ts +126 -3
  55. package/dist/run/journal.js +410 -37
  56. package/dist/run/merge.d.ts +3 -1
  57. package/dist/run/merge.js +3 -2
  58. package/dist/run/operator-summary.d.ts +3 -0
  59. package/dist/run/operator-summary.js +3 -1
  60. package/dist/run/protocol.d.ts +31 -1
  61. package/dist/run/protocol.js +3 -1
  62. package/dist/run/supervision.d.ts +7 -1
  63. package/dist/run/supervision.js +5 -2
  64. package/dist/tui/cockpit/board.js +3 -3
  65. package/dist/tui/cockpit/decision-actions.d.ts +8 -5
  66. package/dist/tui/cockpit/decision-actions.js +55 -32
  67. package/dist/tui/cockpit/derive.js +13 -2
  68. package/dist/tui/cockpit/live-runtime.d.ts +10 -0
  69. package/dist/tui/cockpit/live-runtime.js +50 -3
  70. package/dist/tui/cockpit/run-cockpit.d.ts +3 -0
  71. package/dist/tui/cockpit/run-cockpit.js +26 -1
  72. package/dist/tui/cockpit/run-view.d.ts +9 -3
  73. package/dist/tui/cockpit/run-view.js +60 -7
  74. package/dist/tui/cockpit/setup-cockpit.d.ts +4 -0
  75. package/dist/tui/cockpit/setup-cockpit.js +6 -3
  76. package/dist/tui/ink/fleet-app.d.ts +15 -3
  77. package/dist/tui/ink/fleet-app.js +91 -22
  78. package/package.json +2 -1
  79. package/schema/config.schema.json +818 -0
  80. package/skills/tickmarkr-loop/SKILL.md +8 -2
  81. package/skills/tickmarkr-overseer/SKILL.md +42 -0
  82. package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +88 -0
  83. package/skills/tickmarkr-overseer/scripts/context-statusline.sh +81 -0
  84. package/skills/tickmarkr-overseer/scripts/grade-ci.sh +36 -34
  85. package/skills/tickmarkr-overseer/scripts/watch-journal.sh +6 -4
@@ -5,7 +5,7 @@ import { shq } from "../adapters/types.js";
5
5
  import { createWorktree, FORK_CAP_ENV, resolvedForkCap, sh } from "../run/git.js";
6
6
  import { Journal, parseRunId } from "../run/journal.js";
7
7
  import { stateDirName } from "../graph/graph.js";
8
- import { readWatchBoard, requestWatchBoardStop, stopWatchBoard, WATCH_OWNER_ENV } from "../run/supervision.js";
8
+ import { readWatchBoard, requestWatchBoardStop, stopWatchBoard, supervisionPresencePath, WATCH_OWNER_ENV } from "../run/supervision.js";
9
9
  import { MAX_BUF } from "./subprocess.js";
10
10
  import { formatOwnedName, panesToClose, parseOwnedName } from "./types.js";
11
11
  // Orca (onorca.dev) as a third execution surface beside herdr and subprocess. tickmarkr keeps
@@ -454,6 +454,26 @@ function lockEntryGeneration(entry) {
454
454
  const legacy = /^pid(?:\.(\d+))?$/.exec(entry);
455
455
  return owner ? Number(owner[1]) : legacy ? Number(legacy[1] ?? 0) : undefined;
456
456
  }
457
+ /** The placing claim window: how long a narrator waits for its observer to claim the reservation. */
458
+ const CLAIM_WINDOW_MS = 5000;
459
+ /** Reservation tokens THIS process held after an indeterminate split receipt (OBS-1172). Any other
460
+ * reserved or claimed record under a live placer is a placement in flight (or a crash still being
461
+ * judged) and is refused untouched; a held one is reconciled again on every call, never split over. */
462
+ const held = new Set();
463
+ /** A claimed board's observer is live when its pid answers AND the arm it claimed with is present. */
464
+ function observerLive(r) {
465
+ return typeof r.pid === "number" && pidLive(r.pid) && typeof r.armId === "string"
466
+ && existsSync(supervisionPresencePath(r.repo, "watch", r.armId));
467
+ }
468
+ /** Tombstone THIS reservation while it is still the untouched reservation; a record anyone else moved
469
+ * is left to them. Answers the tombstone, or undefined when the record was not this reservation. */
470
+ async function tombstoneReservation(path, cwd, runId, token, time, extra = {}) {
471
+ const now = readBoard(path, cwd, runId);
472
+ if (now?.record.token !== token || boardState(now.record) !== "reserved")
473
+ return undefined;
474
+ const record = { ...now.record, ...extra, retired: true };
475
+ return { raw: await casBoard("split", path, now.raw, record, time), record };
476
+ }
457
477
  function pidLive(pid) {
458
478
  try {
459
479
  process.kill(pid, 0);
@@ -701,6 +721,26 @@ export class OrcaDriver {
701
721
  }
702
722
  return { id, name: title, cwd: worktree, group: opts?.group };
703
723
  }
724
+ /**
725
+ * OBS-1109: bind THIS instance to a terminal an earlier daemon created for an OWNED slot, so a
726
+ * resumed daemon can read the interrupted attempt's pane. Binding needs ownership evidence, never
727
+ * a title or handle alone: exactly one tab on the tracked worktree carries the FULL journaled owned
728
+ * title, it resolves to exactly one terminal, and that terminal's scrollback proves the exact task
729
+ * checkout `slot.cwd` (the frame only tickmarkr's own create command prints). Zero, ambiguous or
730
+ * foreign candidates leave the slot unbound: the throw is the caller's decline. The journaled
731
+ * nonce is then checked by the caller against the trailer this binding reads.
732
+ */
733
+ async adopt(slot) {
734
+ if (!parseOwnedName(slot.name))
735
+ throw new OrcaError("list", `slot ${slot.name} carries no owned title — nothing to adopt`, "");
736
+ const cwd = canonicalWorktreePath(slot.cwd);
737
+ const st = { title: slot.name, cwd, buf: "", recoveries: 0, recovering: false };
738
+ st.tracked = await this.trackedWorktree(cwd);
739
+ await this.recover("adopt", st, undefined, "", { proveCheckout: true });
740
+ const id = `orca-${++this.n}`;
741
+ this.slots.set(id, st);
742
+ return { id, name: slot.name, cwd, ...(slot.group === undefined ? {} : { group: slot.group }) };
743
+ }
704
744
  /** Where to invoke the CLI for this slot's calls (see OrcaSlotState.dir). */
705
745
  cliCwd(st) {
706
746
  return st.dir ?? st.cwd;
@@ -979,7 +1019,7 @@ export class OrcaDriver {
979
1019
  return { proven: false, reason: error instanceof Error ? error.message : String(error) };
980
1020
  }
981
1021
  }
982
- async recover(family, st, newRuntimeId, raw) {
1022
+ async recover(family, st, newRuntimeId, raw, opts) {
983
1023
  if (st.recovering)
984
1024
  throw this.latched(family, st, "handle recovery re-entered", raw);
985
1025
  st.recovering = true;
@@ -1034,17 +1074,19 @@ export class OrcaDriver {
1034
1074
  throw this.latched(family, st, `the owned tab resolves to ${panes.length} terminals in ${home}`, env.raw);
1035
1075
  }
1036
1076
  const handle = panes[0];
1037
- if (handle === old) {
1038
- // Handles are runtime-scoped: the same VALUE under a different runtime proves nothing about
1039
- // which terminal it addresses, so it is never adopted.
1040
- throw this.latched(family, st, `replacement handle ${handle} is the old handle value reused by runtime ${newRuntimeId ?? env.runtimeId ?? "unknown"}`, env.raw);
1041
- }
1042
- if (st.tracked !== undefined && st.tracked !== st.cwd) {
1077
+ // Handles are runtime-scoped: the same VALUE under a different runtime proves nothing about
1078
+ // which terminal it addresses. OBS-1108: it is therefore reconciled exactly like any other
1079
+ // replacement plus the checkout proof — never on the handle text (a 2.5.8 latch refused a live,
1080
+ // finished worker for hours) and never adopted without that ownership evidence.
1081
+ const reused = handle === old;
1082
+ if (reused || opts?.proveCheckout || (st.tracked !== undefined && st.tracked !== st.cwd)) {
1043
1083
  // FX-N01: a nested checkout shares its worktreePath with every sibling task's terminal, so
1044
1084
  // the candidate must prove the checkout itself (read-only) before it is addressed as ours.
1045
1085
  const proof = await this.checkoutProven(handle, st.cwd, this.cliCwd(st), env.runtimeId);
1046
1086
  if (!proof.proven) {
1047
- 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);
1087
+ throw this.latched(family, st, reused
1088
+ ? `replacement handle ${handle} is the old handle value reused by runtime ${newRuntimeId ?? env.runtimeId ?? "unknown"} and does not prove checkout ${st.cwd} (${proof.reason}); a reused handle value is not adopted on its text`
1089
+ : `candidate ${handle} does not prove checkout ${st.cwd} (${proof.reason}); the sole same-titled tab on ${home} is not adopted`, env.raw);
1048
1090
  }
1049
1091
  }
1050
1092
  st.handle = handle;
@@ -1435,13 +1477,29 @@ export class OrcaDriver {
1435
1477
  }
1436
1478
  }
1437
1479
  else if (current) {
1438
- const state = boardState(current.record);
1480
+ let state = boardState(current.record);
1481
+ const { placer, token: heldToken } = current.record;
1482
+ if ((state === "reserved" || state === "claimed") && placer === process.pid && held.has(heldToken)) {
1483
+ // OBS-1172: THIS process held the placement after an indeterminate split receipt. Reconciled
1484
+ // again, never split over: adopted once proven, still held while unknown, and tombstoned
1485
+ // only once proven to have started nothing.
1486
+ const settled = await this.settleIndeterminate(cwd, path, runId, heldToken, launchingHandle, 0);
1487
+ if (settled.kind === "held") {
1488
+ throw new OrcaError("split", `Orca narrator placement remains unresolved for ${name} (${state}, held after an indeterminate split receipt): ${settled.why}; no second board is split over it; ${kept}`, "");
1489
+ }
1490
+ held.delete(heldToken);
1491
+ if (settled.kind === "adopted")
1492
+ return settled.slot;
1493
+ current = await tombstoneReservation(path, cwd, runId, heldToken, this.time);
1494
+ if (!current)
1495
+ throw new OrcaError("split", `Orca narrator placement remains unresolved for ${name}: the held reservation changed underneath; ${kept}`, "");
1496
+ state = boardState(current.record);
1497
+ }
1439
1498
  if (state === "reserved" || state === "claimed") {
1440
1499
  // A reservation or claim with no bound pane is the placing narrator's normal intermediate
1441
1500
  // state while that narrator lives (awaiting its receipt, listing, claim or bind): refused
1442
1501
  // untouched, its observer never asked to stop. Only a DEAD placer (or one unrecorded) makes
1443
1502
  // it a crash to recover.
1444
- const { placer } = current.record;
1445
1503
  if (typeof placer !== "number" || pidLive(placer)) {
1446
1504
  throw new OrcaError("split", `Orca narrator placement remains unresolved for ${name} (${state}, no bound pane) while placing narrator ${placer ?? "unrecorded"} lives; ${kept}`, "");
1447
1505
  }
@@ -1450,14 +1508,11 @@ export class OrcaDriver {
1450
1508
  // The placing driver crashed between the observer's claim and its bind. The record is
1451
1509
  // replaced only once that observer is proven gone: dead outright, or live and stopped with
1452
1510
  // its acknowledgement awaited on injected time — unacknowledged, it is refused untouched.
1453
- const pid = current.record.pid;
1454
- if (pidLive(pid)) {
1455
- try {
1456
- await stopWatchBoard(current.record, this.time);
1457
- }
1458
- catch (error) {
1459
- throw new OrcaError("split", `Orca narrator placement refused for ${name}: claimed board's live observer unacknowledged; ${kept}; ${errorText(error)}`, "");
1460
- }
1511
+ try {
1512
+ await this.stopObserver(current.record);
1513
+ }
1514
+ catch (error) {
1515
+ throw new OrcaError("split", `Orca narrator placement refused for ${name}: claimed board's live observer unacknowledged; ${kept}; ${errorText(error)}`, "");
1461
1516
  }
1462
1517
  const retired = { ...current.record, retired: true };
1463
1518
  current = { raw: await casBoard("split", path, current.raw, retired, this.time), record: retired };
@@ -1471,10 +1526,13 @@ export class OrcaDriver {
1471
1526
  if (state === "bound") {
1472
1527
  // The bound record is the durable truth of placement: the child handle PLUS the split
1473
1528
  // envelope's runtime identity. A handle listed by a later runtime is a different pane.
1474
- if (listedOnRuntime(await this.listAll(cwd), current.record.pane, current.record.runtimeId)) {
1529
+ // OBS-1131: it is adopted only while its observer lives — a resumed daemon retires the
1530
+ // board of a dead observer here (tombstone, stop request, handle-bound close) instead of
1531
+ // answering it and later spending a reopen to discover the death.
1532
+ if (observerLive(current.record) && listedOnRuntime(await this.listAll(cwd), current.record.pane, current.record.runtimeId)) {
1475
1533
  return watchSlot(cwd, name, current.record.pane);
1476
1534
  }
1477
- // Lost: this runtime no longer has that pane. Tombstone before anything else.
1535
+ // Lost or dead: tombstone before anything else.
1478
1536
  const retired = { ...current.record, retired: true };
1479
1537
  current = { raw: await casBoard("split", path, current.raw, retired, this.time), record: retired };
1480
1538
  }
@@ -1482,12 +1540,7 @@ export class OrcaDriver {
1482
1540
  // observer must stop and ack on injected time before the handle-bound close; timeout keeps
1483
1541
  // the tombstone and does not replace it.
1484
1542
  try {
1485
- if (typeof current.record.pid === "number" && pidLive(current.record.pid)) {
1486
- await stopWatchBoard(current.record, this.time);
1487
- }
1488
- else {
1489
- requestWatchBoardStop(current.record);
1490
- }
1543
+ await this.stopObserver(current.record);
1491
1544
  }
1492
1545
  catch (error) {
1493
1546
  throw new OrcaError("split", `Orca narrator placement refused for ${name}: retired board observer unacknowledged; ${kept}; ${errorText(error)}`, "");
@@ -1503,37 +1556,43 @@ export class OrcaDriver {
1503
1556
  // judged replaceable above.
1504
1557
  const token = randomUUID();
1505
1558
  await casBoard("split", path, current?.raw, { repo: realpathSync(cwd), runId, driver: this.id, workspace: ORCA_SPACE, pane: "", name, token, placer: process.pid }, this.time);
1506
- // A reservation that can never be bound — no usable receipt, or no claim — is tombstoned so the
1507
- // next narrator call splits afresh instead of refusing forever. Only THIS reservation, and only
1508
- // while it is still the untouched reservation; a record anyone else moved is left to them.
1509
- const tombstone = async (extra = {}) => {
1510
- try {
1511
- const now = readBoard(path, cwd, runId);
1512
- if (now?.record.token === token && boardState(now.record) === "reserved") {
1513
- await casBoard("split", path, now.raw, { ...now.record, ...extra, retired: true }, this.time);
1514
- }
1559
+ // OBS-1172: a split whose receipt never arrived intact — a transport failure or timeout, an
1560
+ // unparseable or handle-less body, a child the listing does not place in the launching tab —
1561
+ // may still have started the board. It is reconciled before any failure: a uniquely proven
1562
+ // live pane is adopted and bound; a split proven to have started nothing is tombstoned so the
1563
+ // next call splits afresh; anything else is HELD — the reservation stays, and no later call
1564
+ // splits a second board over it.
1565
+ const indeterminate = async (reason, raw) => {
1566
+ const settled = await this.settleIndeterminate(cwd, path, runId, token, launchingHandle, CLAIM_WINDOW_MS);
1567
+ if (settled.kind === "adopted")
1568
+ return settled.slot;
1569
+ if (settled.kind === "absent") {
1570
+ await tombstoneReservation(path, cwd, runId, token, this.time).catch(() => undefined);
1571
+ throw new OrcaError("split", `Orca narrator placement failed for ${name}: ${reason}; ${kept}`, raw);
1515
1572
  }
1516
- catch { /* the record is kept as it stands; the next narrator judges it */ }
1573
+ held.add(token);
1574
+ throw new OrcaError("split", `Orca narrator placement unresolved for ${name}: ${reason}; ${settled.why} — the placement is held and no second board is split over it; ${kept}`, raw);
1517
1575
  };
1518
1576
  // Exactly one terminal split of the launching handle, horizontal, carrying token and command.
1519
- // An unknown receipt (transport failure, refusal, unparseable output) after the verb was issued
1520
- // is as indeterminate as a handle-less one: the pane may exist, so the reservation stays.
1521
- const env = await this.call("split", [
1522
- "terminal", "split",
1523
- "--terminal", launchingHandle,
1524
- "--direction", "horizontal",
1525
- "--command", `${WATCH_OWNER_ENV}=${shq(token)} ${command}`,
1526
- ], cwd).catch(async (error) => {
1527
- await tombstone();
1528
- 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 : "");
1529
- });
1577
+ let env;
1578
+ try {
1579
+ env = await this.call("split", [
1580
+ "terminal", "split",
1581
+ "--terminal", launchingHandle,
1582
+ "--direction", "horizontal",
1583
+ "--command", `${WATCH_OWNER_ENV}=${shq(token)} ${command}`,
1584
+ ], cwd);
1585
+ }
1586
+ catch (error) {
1587
+ return await indeterminate(`split receipt is unknown (${error instanceof OrcaError ? error.reason : errorText(error)})`, error instanceof OrcaError ? error.raw : "");
1588
+ }
1589
+ const malformed = "split receipt is unknown, malformed or handle-less";
1530
1590
  let child;
1531
1591
  try {
1532
1592
  child = splitReceipt(env);
1533
1593
  }
1534
1594
  catch {
1535
- await tombstone();
1536
- throw new OrcaError("split", `Orca narrator placement failed for ${name}: split receipt is unknown, malformed or handle-less; ${kept}`, env.raw);
1595
+ return await indeterminate(malformed, env.raw);
1537
1596
  }
1538
1597
  // Parent tabId must name the launching terminal's tab, and the child must actually appear
1539
1598
  // there. A handle-only object, a tabId for some other tab, or a handle the list does not
@@ -1543,21 +1602,19 @@ export class OrcaDriver {
1543
1602
  listing = await this.listAll(cwd);
1544
1603
  }
1545
1604
  catch (error) {
1546
- await tombstone();
1547
- 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);
1605
+ return await indeterminate(malformed, error instanceof OrcaError ? error.raw : env.raw);
1548
1606
  }
1549
1607
  const launchingTabId = terminalTabId(listing, launchingHandle);
1550
1608
  if (!launchingTabId
1551
1609
  || child.tabId !== launchingTabId
1552
1610
  || child.handle === launchingHandle
1553
1611
  || terminalTabId(listing, child.handle) !== launchingTabId) {
1554
- await tombstone();
1555
- throw new OrcaError("split", `Orca narrator placement failed for ${name}: split receipt is unknown, malformed or handle-less; ${kept}`, env.raw);
1612
+ return await indeterminate(malformed, env.raw);
1556
1613
  }
1557
1614
  const childHandle = child.handle;
1558
1615
  // The narrator writes nothing until the observer's single claim is visible, so neither write
1559
1616
  // can erase the other; after it the observer never writes the record again.
1560
- const claimDeadline = this.time.now() + 5000;
1617
+ const claimDeadline = this.time.now() + CLAIM_WINDOW_MS;
1561
1618
  let claim;
1562
1619
  for (;;) {
1563
1620
  const check = readBoard(path, cwd, runId);
@@ -1574,7 +1631,7 @@ export class OrcaDriver {
1574
1631
  if (!claim) {
1575
1632
  // The receipt's handle may be closed; the reservation is tombstoned naming that pane so the
1576
1633
  // next narrator can prove it gone (or close it) and split afresh.
1577
- await tombstone({ pane: childHandle, runtimeId: env.runtimeId });
1634
+ await tombstoneReservation(path, cwd, runId, token, this.time, { pane: childHandle, runtimeId: env.runtimeId }).catch(() => undefined);
1578
1635
  let pane;
1579
1636
  try {
1580
1637
  await this.closeRecordedPane(cwd, name, childHandle, env.runtimeId);
@@ -1589,6 +1646,67 @@ export class OrcaDriver {
1589
1646
  return watchSlot(cwd, name, childHandle);
1590
1647
  });
1591
1648
  }
1649
+ /** OBS-1172: what an indeterminate split left behind, reconciled from the owner record and Orca's
1650
+ * terminal table — never from the receipt that did not arrive. The reservation's token reaches no
1651
+ * process but the split's own command, so a claim proves that split started a board; the owned
1652
+ * record (this driver, the watch title) is ours by that same token. The claimant must be live (pid
1653
+ * and arm presence), and it must name the terminal it runs in — a listed pane in the launching tab.
1654
+ * A claim that names none proves a board started, not WHICH pane hosts it: a sole neighbouring pane
1655
+ * (row titles are shell-controlled, never ownership keys) is not adopted, so the claim stays held.
1656
+ * No claim and no such pane: the split started nothing the table shows. Anything else is held. */
1657
+ async settleIndeterminate(cwd, path, runId, token, launchingHandle, waitMs) {
1658
+ const deadline = this.time.now() + waitMs;
1659
+ let claim;
1660
+ for (;;) {
1661
+ let check;
1662
+ try {
1663
+ check = readBoard(path, cwd, runId);
1664
+ }
1665
+ catch (error) {
1666
+ return { kind: "held", why: `the owner record is unreadable (${errorText(error)})` };
1667
+ }
1668
+ if (check?.record.token !== token || check.record.retired)
1669
+ return { kind: "held", why: "the owner record changed underneath" };
1670
+ if (boardState(check.record) === "claimed") {
1671
+ claim = check;
1672
+ break;
1673
+ }
1674
+ if (this.time.now() >= deadline)
1675
+ break;
1676
+ await this.time.sleep(20);
1677
+ }
1678
+ let listing;
1679
+ try {
1680
+ listing = await this.listAll(cwd);
1681
+ }
1682
+ catch (error) {
1683
+ return { kind: "held", why: `the terminal table is unreadable (${errorText(error)})` };
1684
+ }
1685
+ const tab = terminalTabId(listing, launchingHandle);
1686
+ if (!tab)
1687
+ return { kind: "held", why: `the launching terminal ${launchingHandle} is not listed` };
1688
+ const named = claim?.record.handle;
1689
+ const panes = terminalRows(listing).map((row) => str(row.handle)).filter((h) => h !== undefined && h !== launchingHandle && terminalTabId(listing, h) === tab && (named === undefined || h === named));
1690
+ if (!claim) {
1691
+ return panes.length === 0 ? { kind: "absent" }
1692
+ : { kind: "held", why: `no observer claimed the reservation and ${panes.length} pane(s) of unknown ownership share the launching tab` };
1693
+ }
1694
+ if (!observerLive(claim.record))
1695
+ return { kind: "held", why: `the observer that claimed it (pid ${claim.record.pid}) is not live` };
1696
+ if (named === undefined) {
1697
+ return { kind: "held", why: `the observer that claimed it (pid ${claim.record.pid}) reports no terminal handle, so no listed pane is proven to host it` };
1698
+ }
1699
+ if (panes.length !== 1)
1700
+ return { kind: "held", why: `its observer reports ${named}, which the launching tab does not list` };
1701
+ const pane = panes[0];
1702
+ try {
1703
+ await casBoard("split", path, claim.raw, { ...claim.record, pane, runtimeId: listing.runtimeId }, this.time);
1704
+ }
1705
+ catch (error) {
1706
+ return { kind: "held", why: `the bind was refused (${errorText(error)})` };
1707
+ }
1708
+ return { kind: "adopted", slot: watchSlot(cwd, claim.record.name, pane) };
1709
+ }
1592
1710
  boardPath(cwd, runId) {
1593
1711
  return join(cwd, stateDirName(cwd), "supervision", `watch-board.${parseRunId(runId)}.json`);
1594
1712
  }
@@ -1626,13 +1744,28 @@ export class OrcaDriver {
1626
1744
  async retireAndClose(slot) {
1627
1745
  await this.serial(async () => {
1628
1746
  const retired = await this.retireBoard("close", slot);
1629
- if (typeof retired.pid === "number" && pidLive(retired.pid))
1630
- await stopWatchBoard(retired, this.time);
1631
- else
1632
- requestWatchBoardStop(retired);
1747
+ await this.stopObserver(retired);
1633
1748
  await this.closeRecordedPane(slot.cwd, slot.name, slot.id, retired.runtimeId);
1634
1749
  });
1635
1750
  }
1751
+ /** Stop a retired board's observer before its pane is closed. A live observer acknowledges on
1752
+ * injected time; a dead one never can, so it is only asked. OBS-1131: an acknowledgement timeout
1753
+ * is not proof of death — while the pid lives the owned pane stays protected — but a pid that died
1754
+ * during the wait is, and its pane is closed instead of leaked. */
1755
+ async stopObserver(record) {
1756
+ const pid = record.pid;
1757
+ if (typeof pid !== "number" || !pidLive(pid)) {
1758
+ requestWatchBoardStop(record);
1759
+ return;
1760
+ }
1761
+ try {
1762
+ await stopWatchBoard(record, this.time);
1763
+ }
1764
+ catch (error) {
1765
+ if (pidLive(pid) || readWatchBoard(record.repo, record.runId)?.token !== record.token)
1766
+ throw error;
1767
+ }
1768
+ }
1636
1769
  /** WB-1 seam: the daemon reports this board lost. "Lost" can be a stale beat or missing presence
1637
1770
  * under a still-live owner pid, so it is not proof of a dead observer — retirement keeps close's
1638
1771
  * acknowledgement discipline (Leg-2 T9 P1). */
@@ -1,4 +1,5 @@
1
1
  import type { WorkerAdapter } from "../adapters/types.js";
2
+ import type { Effort } from "../graph/schema.js";
2
3
  import { type Fixture } from "./fixtures.js";
3
4
  export declare const CANARY_FIXTURE_ID = "canary";
4
5
  export interface CanaryJudgeResult {
@@ -35,7 +36,7 @@ export declare function ensureCanary(fixtures: Fixture[], root: string): Fixture
35
36
  * is evaluated against the resulting diff. A correct judge returns pass=false; any pass=true
36
37
  * verdict is flagged as a judge-integrity breach.
37
38
  */
38
- export declare function runCanaryJudge(fixture: Fixture, judgeAdapter: WorkerAdapter, model: string): Promise<CanaryJudgeResult>;
39
+ export declare function runCanaryJudge(fixture: Fixture, judgeAdapter: WorkerAdapter, model: string, effort?: Effort): Promise<CanaryJudgeResult>;
39
40
  /**
40
41
  * Aggregate per-channel qualification totals from fixture-channel results.
41
42
  * Results marked as canary (by the supplied predicate, defaulting to isCanaryResult) are excluded
@@ -53,7 +53,7 @@ async function applyKnownBadChange(repo) {
53
53
  * is evaluated against the resulting diff. A correct judge returns pass=false; any pass=true
54
54
  * verdict is flagged as a judge-integrity breach.
55
55
  */
56
- export async function runCanaryJudge(fixture, judgeAdapter, model) {
56
+ export async function runCanaryJudge(fixture, judgeAdapter, model, effort) {
57
57
  if (!isCanaryFixture(fixture)) {
58
58
  throw new Error(`fixture ${fixture.id} is not the canary fixture`);
59
59
  }
@@ -62,7 +62,7 @@ export async function runCanaryJudge(fixture, judgeAdapter, model) {
62
62
  const initialCommit = (await shGitOk("git rev-parse HEAD", seeded.repo)).trim();
63
63
  try {
64
64
  await applyKnownBadChange(seeded.repo);
65
- const gateResult = await acceptanceGate(task, seeded.repo, initialCommit, { adapter: judgeAdapter, model });
65
+ const gateResult = await acceptanceGate(task, seeded.repo, initialCommit, { adapter: judgeAdapter, model, effort });
66
66
  const expectedPass = false;
67
67
  const judgePass = gateResult.pass;
68
68
  const breach = judgePass === true && expectedPass === false;
@@ -157,6 +157,7 @@ async function dispatchChannel(task, channel, adapter, promptFile, nonce, fixtur
157
157
  model: channel.model,
158
158
  channel: channel.channel,
159
159
  tier: channel.tier,
160
+ ...(channel.effort ? { effort: channel.effort } : {}), // OBS-1182: measured at the effort it would run at
160
161
  };
161
162
  const invocation = adapter.invoke(task, seeded.repo, assignment, { promptFile });
162
163
  const run = await sh(invocation.command, seeded.repo, cfg.taskTimeoutMinutes * 60_000);
@@ -1,5 +1,5 @@
1
1
  import { type WorkerAdapter } from "../adapters/types.js";
2
- import { type AcceptanceItem, type Task } from "../graph/schema.js";
2
+ import { type Effort, type AcceptanceItem, type Task } from "../graph/schema.js";
3
3
  import { type LlmVia } from "./llm.js";
4
4
  import type { GateResult } from "./types.js";
5
5
  export interface EvidenceCitation {
@@ -62,4 +62,5 @@ export interface AcceptanceGateOpts {
62
62
  export declare function acceptanceGate(task: Task, worktree: string, baseRef: string, judge: {
63
63
  adapter: WorkerAdapter;
64
64
  model: string;
65
+ effort?: Effort;
65
66
  }, via?: LlmVia, opts?: AcceptanceGateOpts): Promise<GateResult>;
@@ -342,7 +342,9 @@ function citesChangedLocation(evidence, changed, diff) {
342
342
  return evidence.trim().length > 0 && diff.includes(evidence);
343
343
  return changed.get(evidence.path)?.has(evidence.line) ?? false;
344
344
  }
345
- export async function acceptanceGate(task, worktree, baseRef, judge, via, opts = {}) {
345
+ export async function acceptanceGate(task, worktree, baseRef,
346
+ // OBS-1182: the judge seat's OWN effort (absent = CLI default) — never the worker's.
347
+ judge, via, opts = {}) {
346
348
  // 1. deterministic oracles — exit code decides, fail-closed, zero LLM calls (spec §2, T2).
347
349
  // A failure returns here, before any runLlm() call: a judge can never override it.
348
350
  const passedDet = [];
@@ -439,7 +441,10 @@ Each criteria[].criterion MUST be the stable id from the rubric (c1, c2, ...) ex
439
441
  Each criteria[].evidence MUST be a structured citation {"path", "line"} whose "path" and "line" appear in the "Citable evidence lines" list above (a new-file line number inside a changed hunk); a citation outside every changed hunk or to an untouched file voids the whole verdict.
440
442
  The top-level comments array is optional. Use it only for actionable line-anchored feedback.
441
443
  `;
442
- const raw = await runLlm(judge.adapter, judge.model, prompt, worktree, via, JUDGE_TIMEOUT_MS);
444
+ // OBS-1182: an unset seat passes no effort argument at all — the CLI default, and the call shape callers pin.
445
+ const raw = await (judge.effort
446
+ ? runLlm(judge.adapter, judge.model, prompt, worktree, via, JUDGE_TIMEOUT_MS, judge.effort)
447
+ : runLlm(judge.adapter, judge.model, prompt, worktree, via, JUDGE_TIMEOUT_MS));
443
448
  const extracted = extractVerdictJson(raw, nonce);
444
449
  if (!extracted) {
445
450
  const cause = classifyVerdictCause(raw, nonce, "pass");
@@ -47,7 +47,7 @@ export declare function beginGateEvidence(cwd: string, gate: string, command: st
47
47
  subjectCommit: string | null;
48
48
  };
49
49
  termination: {
50
- kind: "unknown" | "signal" | "not-started" | "exit" | "timeout";
50
+ kind: "unknown" | "timeout" | "signal" | "not-started" | "exit";
51
51
  exitCode: number | null;
52
52
  signal: string | null;
53
53
  timedOut: boolean | null;
@@ -127,9 +127,19 @@ export interface BaselineFileDuration {
127
127
  file: string;
128
128
  durationMs: number;
129
129
  }
130
+ /**
131
+ * OBS-1123: WHICH capture a forgiveness rests on — the commit it measured and when it was published.
132
+ * Stamped by the daemon beside the measurement; absent on every baseline.json written before it, which
133
+ * readers must render as unknown provenance and never date from any other clock.
134
+ */
135
+ export interface BaselineProvenance {
136
+ baseRef: string;
137
+ capturedAt: string;
138
+ }
130
139
  export interface Baseline {
131
140
  /** No command ran because the dependency inventory could not establish isolation. */
132
141
  refusal?: string;
142
+ provenance?: BaselineProvenance;
133
143
  /** Observational capture receipts stay outside the forgiveness-bearing command entries. */
134
144
  evidenceReceipts?: Record<string, GateEvidenceReceipt>;
135
145
  commands: Record<string, BaselineCommand>;
@@ -156,6 +166,7 @@ export declare function fingerprint(output: string): string[];
156
166
  export declare function freshFailures(entry: BaselineCommand | undefined, raw: string): {
157
167
  failing: string[];
158
168
  unreadable: boolean;
169
+ forgiven: string[];
159
170
  };
160
171
  export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
161
172
  /**
@@ -428,7 +428,9 @@ export function freshFailures(entry, raw) {
428
428
  // OBS-42: diagnostic headings enrich fingerprints but cannot invalidate legacy baselines.
429
429
  const current = fingerprint(raw);
430
430
  const fresh = current.filter((f) => !known.has(f) && (!FAIL_ANCHOR_RE.test(f) || f.startsWith("FAIL ")));
431
- return { failing: fresh.filter((f) => f !== UNRECOGNIZED_FAILURE), unreadable: current.includes(UNRECOGNIZED_FAILURE) };
431
+ // OBS-1123: the baseline-recorded half, named so a reader never has to infer it from the fresh half.
432
+ return { failing: fresh.filter((f) => f !== UNRECOGNIZED_FAILURE), unreadable: current.includes(UNRECOGNIZED_FAILURE),
433
+ forgiven: current.filter((f) => known.has(f)) };
432
434
  }
433
435
  /**
434
436
  * Operator directive 2026-08-12 (D-OBS-10): tickmarkr is package-manager agnostic.
@@ -880,7 +882,10 @@ export async function compareToBaseline(cwd, commands, baseline, enabled, opts =
880
882
  // below rather than reading as a verified green. Raise the ceiling by teaching isFailureShaped
881
883
  // that runner's position rule (leading verdict + identifier, or identifier + separator + trailing
882
884
  // verdict); loosening back to vocabulary re-opens OBS-278.
883
- const { failing, unreadable } = freshFailures(entry, raw);
885
+ const { failing, unreadable, forgiven } = freshFailures(entry, raw);
886
+ // OBS-1123: which baseline-recorded fingerprints this verdict carried, and the capture that
887
+ // recorded them. Observational only — nothing below reads it to decide a verdict.
888
+ const provenance = baseline.provenance ? { baselineProvenance: baseline.provenance } : {};
884
889
  // T9: classify the FRESH diff before charging it. The complete runner output can legitimately
885
890
  // contain a baseline-recorded assertion beside a newly introduced infrastructure death; letting
886
891
  // that known assertion outvote the fresh birpc line turns machine failure into a worker defect.
@@ -957,7 +962,9 @@ export async function compareToBaseline(cwd, commands, baseline, enabled, opts =
957
962
  }
958
963
  if (failing.length) {
959
964
  const headlined = headlineDetails(raw, failing);
960
- const meta = { ...headlined.meta, ...(classification ? { classification } : {}) };
965
+ // A fresh red beside baseline-recorded ones names both halves, so no reader shows the new one as forgiven.
966
+ const meta = { ...headlined.meta, ...(classification ? { classification } : {}),
967
+ ...(forgiven.length ? { freshFingerprints: failing, forgivenFingerprints: forgiven, ...provenance } : {}) };
961
968
  record({ gate: name, pass: false, details: headlined.details, ...(Object.keys(meta).length ? { meta } : {}) });
962
969
  continue;
963
970
  }
@@ -981,7 +988,7 @@ export async function compareToBaseline(cwd, commands, baseline, enabled, opts =
981
988
  gate: name,
982
989
  pass: true,
983
990
  details: `exit ${r.code} but only pre-existing failures (forgiven)${unreadable ? " — no failure shape recognized in this output, so a new failure from this runner is invisible to the baseline gate" : ""}`,
984
- ...(classification ? { meta: { classification } } : {}),
991
+ meta: { ...(classification ? { classification } : {}), forgivenFingerprints: forgiven, ...provenance },
985
992
  });
986
993
  }
987
994
  return results;
@@ -1,4 +1,5 @@
1
1
  import { type WorkerAdapter } from "../adapters/types.js";
2
+ import type { Effort } from "../graph/schema.js";
2
3
  import { type ExecutorDriver, type Slot } from "../drivers/types.js";
3
4
  export declare const GATE_PANE_SEP = " \u00B7 ";
4
5
  export declare const COMPLETION_FAKING_CHECKLIST = "## Completion-faking checklist\nHunt for these concrete completion-faking shortcuts before ruling on any criterion:\n- hardcoded-result: output or fixture hardcoded to satisfy the stated criterion instead of real logic\n- test-weakening: tests skipped, deleted, or assertions loosened until failing behavior looks green\n- vacuous-assertion: a test that cannot fail (asserts a constant, asserts its own setup, no assertion)\n- fixture-overfit: implementation narrowed to the exact test inputs rather than the described behavior\n- echo-not-implement: criterion text echoed in names, comments, or strings without the behavior itself\n- stub-left-behind: TODO, throw, or no-op stub where the real implementation should be\n- error-swallowing: catch or fallback that hides failures instead of handling them\n- self-mocking: the code under test mocked or faked so the test exercises the mock\n- check-bypass: lint, type, or CI checks disabled, relaxed, or excluded to get green\n- rename-as-work: code moved or renamed and presented as the requested change\n- scope-padding: unrelated edits padding the diff while the criterion's behavior is untouched\nWhen a criterion fails, the verdict MUST name which shortcut above it matches, or state that none does.";
@@ -70,11 +71,11 @@ export declare const PROMPT_GLYPHS: readonly ["➜", "❯", "$", "%", ">>", ">"]
70
71
  export declare function reviewSeatOutput(raw: string, nonce: string, adapterBannerRows?: readonly string[]): string;
71
72
  export declare const REVIEW_FIRST_LIVENESS_MS = 30000;
72
73
  export declare const REVIEW_SILENT_BYTE_FLOOR = 64;
73
- export declare function runHeadless(adapter: WorkerAdapter, model: string, prompt: string, cwd: string, timeoutMs?: number): Promise<string>;
74
- export declare function runViaDriver(adapter: WorkerAdapter, model: string, prompt: string, cwd: string, via: LlmVia, timeoutMs?: number): Promise<string>;
74
+ export declare function runHeadless(adapter: WorkerAdapter, model: string, prompt: string, cwd: string, timeoutMs?: number, effort?: Effort): Promise<string>;
75
+ export declare function runViaDriver(adapter: WorkerAdapter, model: string, prompt: string, cwd: string, via: LlmVia, timeoutMs?: number, effort?: Effort): Promise<string>;
75
76
  export declare function dewrapPaneVerdict(out: string, nonce: string): string;
76
- export declare function runLlmDetailed(adapter: WorkerAdapter, model: string, prompt: string, cwd: string, via?: LlmVia, timeoutMs?: number): Promise<LlmRunResult>;
77
- export declare function runLlm(adapter: WorkerAdapter, model: string, prompt: string, cwd: string, via?: LlmVia, timeoutMs?: number): Promise<string>;
77
+ export declare function runLlmDetailed(adapter: WorkerAdapter, model: string, prompt: string, cwd: string, via?: LlmVia, timeoutMs?: number, effort?: Effort): Promise<LlmRunResult>;
78
+ export declare function runLlm(adapter: WorkerAdapter, model: string, prompt: string, cwd: string, via?: LlmVia, timeoutMs?: number, effort?: Effort): Promise<string>;
78
79
  export declare function extractJson<T>(raw: string): T | null;
79
80
  /** Fable F3: verdict JSON must echo the call nonce — skip unbound or mismatched objects. */
80
81
  export declare function extractVerdictJson<T>(raw: string, nonce: string): T | null;