tickmarkr 2.6.4 → 2.6.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 (63) hide show
  1. package/dist/adapters/catalog-remote.d.ts +23 -0
  2. package/dist/adapters/catalog-remote.js +38 -0
  3. package/dist/adapters/claude-code.d.ts +1 -1
  4. package/dist/adapters/claude-code.js +4 -1
  5. package/dist/adapters/codex-commit-check.d.ts +25 -0
  6. package/dist/adapters/codex-commit-check.js +108 -0
  7. package/dist/adapters/model-discovery-check.d.ts +33 -0
  8. package/dist/adapters/model-discovery-check.js +123 -0
  9. package/dist/adapters/model-lints.d.ts +1 -1
  10. package/dist/adapters/model-lints.js +4 -0
  11. package/dist/adapters/prompt.js +3 -2
  12. package/dist/adapters/types.d.ts +7 -0
  13. package/dist/cli/commands/approve.js +10 -4
  14. package/dist/cli/commands/doctor.d.ts +2 -0
  15. package/dist/cli/commands/doctor.js +14 -2
  16. package/dist/cli/commands/fleet.d.ts +10 -1
  17. package/dist/cli/commands/fleet.js +55 -38
  18. package/dist/cli/commands/init.js +5 -1
  19. package/dist/cli/commands/plan.js +65 -4
  20. package/dist/cli/commands/verify.d.ts +1 -1
  21. package/dist/cli/commands/verify.js +28 -6
  22. package/dist/config/fleet-overlay.d.ts +2 -1
  23. package/dist/config/fleet-overlay.js +77 -4
  24. package/dist/drivers/orca.d.ts +19 -1
  25. package/dist/drivers/orca.js +161 -31
  26. package/dist/gates/cache.d.ts +4 -0
  27. package/dist/gates/cache.js +10 -2
  28. package/dist/gates/llm.d.ts +4 -1
  29. package/dist/gates/llm.js +6 -1
  30. package/dist/gates/review.js +8 -3
  31. package/dist/gates/run-gates.d.ts +3 -2
  32. package/dist/gates/run-gates.js +91 -50
  33. package/dist/run/daemon.js +150 -19
  34. package/dist/run/git.js +9 -0
  35. package/dist/run/journal.d.ts +6 -2
  36. package/dist/run/journal.js +22 -7
  37. package/dist/run/launchservices-check.d.ts +56 -0
  38. package/dist/run/launchservices-check.js +117 -0
  39. package/dist/run/operator-state.d.ts +22 -2
  40. package/dist/run/operator-state.js +20 -3
  41. package/dist/run/title-environment.d.ts +45 -0
  42. package/dist/run/title-environment.js +127 -0
  43. package/dist/run/title-preload.cjs +38 -0
  44. package/dist/run/title-preload.d.cts +1 -0
  45. package/dist/tui/cockpit/board.js +12 -6
  46. package/dist/tui/cockpit/keys.d.ts +8 -0
  47. package/dist/tui/cockpit/keys.js +4 -1
  48. package/dist/tui/cockpit/layout.d.ts +2 -2
  49. package/dist/tui/cockpit/layout.js +2 -2
  50. package/dist/tui/cockpit/live-runtime.js +46 -20
  51. package/dist/tui/cockpit/live-store.d.ts +24 -1
  52. package/dist/tui/cockpit/live-store.js +99 -8
  53. package/dist/tui/cockpit/log-view.d.ts +28 -0
  54. package/dist/tui/cockpit/log-view.js +60 -0
  55. package/dist/tui/cockpit/run-view.js +4 -0
  56. package/dist/tui/cockpit/shell.d.ts +2 -1
  57. package/dist/tui/cockpit/shell.js +6 -4
  58. package/dist/tui/ink/fleet-app.d.ts +12 -6
  59. package/dist/tui/ink/fleet-app.js +17 -11
  60. package/package.json +1 -1
  61. package/skills/tickmarkr-auto/SKILL.md +32 -2
  62. package/skills/tickmarkr-loop/SKILL.md +46 -2
  63. package/skills/tickmarkr-overseer/SKILL.md +83 -8
@@ -363,20 +363,34 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
363
363
  const channels = discoverChannels(cfg, adapters, health);
364
364
  const profile = loadRoutingProfile(cwd, cfg, { preview: true });
365
365
  const stagedDenyOf = (editable) => Object.fromEntries(DENY_SCOPES.map((scope) => [stagedDenyKeyOf(scope), editable[scope.key] ?? []]));
366
- // B2: the classifications and efforts of the last reviewed state — the preview callbacks carry mode,
367
- // map and deny only (fleet-app's contract), so every preview after a review ranks the same fully
368
- // staged candidate that review rendered, never the assembly-time tiers.
369
- // ponytail: one staged after the last review reaches previews at the next w; thread classifications
370
- // and efforts through fleet-app's preview callbacks if that lag ever matters.
366
+ // T8: staged classifications and efforts as the editable tiers/efforts a review writes — one fold for
367
+ // the review and every preview, so the mode/shape/picker callbacks rank exactly what w would write.
368
+ const stagedMetadataOf = (stage) => {
369
+ const tiers = structuredClone(initial.tiers);
370
+ const today = new Date().toISOString().slice(0, 10);
371
+ for (const classification of stage.classifications) {
372
+ tiers[classification.adapter] ??= {};
373
+ tiers[classification.adapter][classification.model] = {
374
+ tier: classification.tier,
375
+ provenance: `${classification.note} — fleet ${today}`,
376
+ };
377
+ }
378
+ // OBS-1182: staged efforts replace the loaded ones; none staged anywhere ⇒ no key, as loaded
379
+ const efforts = Object.fromEntries(Object.entries(stage.efforts ?? {}).filter(([, models]) => Object.keys(models).length));
380
+ return { tiers, ...(Object.keys(efforts).length ? { efforts } : {}) };
381
+ };
382
+ // B2: the classifications and efforts of the last reviewed state — what a preview called without a
383
+ // stage (the browser's staged routing, --why) ranks; the editor passes its current stage (T8).
371
384
  let reviewedStage = { tiers: initial.tiers, efforts: initial.efforts };
372
385
  // OBS-1182: the layers under the written overlay — B2: the defaults alone under the USER overlay —
373
386
  // read raw, so clearing an effort masks an inherited one. OBS-1188: re-read at every review and
374
387
  // save, never an assembly-time snapshot; an unreadable read keeps the last good one for the
375
388
  // preview only — the save guard below refuses it.
376
389
  let lowerOverrides = {};
377
- const stagedEditable = (map, deny) => ({
390
+ const stagedEditable = (map, deny, metadata) => ({
378
391
  ...structuredClone(initial),
379
- ...structuredClone(reviewedStage),
392
+ efforts: undefined, // metadata carries efforts only when some are staged — as a review writes them
393
+ ...structuredClone(metadata),
380
394
  ...Object.fromEntries(DENY_SCOPES.map((scope) => [scope.key, deny[stagedDenyKeyOf(scope)]])),
381
395
  // OBS-1046: the allow complement is staged beside the deny lists, never folded into them
382
396
  allowOut: deny.allowOut ?? initial.allowOut,
@@ -424,14 +438,17 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
424
438
  };
425
439
  // B2: a changed slot staged with no declaration (auto, no prefer) where the defaults under the user
426
440
  // overlay declare one: deleting the user's pin/pool would restore that default, so the slot is
427
- // masked with prefer: [] — a declaration that replaces the lower slot atomically and ranks nothing
441
+ // masked with prefer: [] — a declaration that replaces the lower slot atomically and ranks nothing.
442
+ // T8: a default PIN is masked by the writer's raw pin: null tombstone instead (lowerMap below), so
443
+ // Auto saves no prefer declaration the operator never made.
428
444
  const maskClearedSlots = (base, staged) => {
429
445
  const map = { ...staged.map };
430
446
  for (const shape of new Set([...Object.keys(base.map), ...Object.keys(staged.map)])) {
431
447
  const entry = staged.map[shape];
448
+ const lower = mapSlot(DEFAULT_CONFIG.routing.map[shape]);
432
449
  if (JSON.stringify(base.map[shape]) === JSON.stringify(entry))
433
450
  continue;
434
- if (Object.keys(mapSlot(entry)).length || !Object.keys(mapSlot(DEFAULT_CONFIG.routing.map[shape])).length)
451
+ if (Object.keys(mapSlot(entry)).length || !Object.keys(lower).length || lower.pin)
435
452
  continue;
436
453
  map[shape] = { ...entry, prefer: [] };
437
454
  }
@@ -449,6 +466,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
449
466
  try {
450
467
  const bytes = renderFleetOverlayWrite(userText, {
451
468
  ...rest, initial: destinationInitial(initial, unchanged, userText, universe), edited: unchanged, universe,
469
+ lowerMap: DEFAULT_CONFIG.routing.map,
452
470
  });
453
471
  const candidate = loadConfigWithMode(cwd, { globalDir, userOverlayText: bytes }).cfg;
454
472
  target = universeOf(candidateChannels(candidate));
@@ -461,20 +479,23 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
461
479
  // the full render below (or the reload guard) names the refusal
462
480
  }
463
481
  }
464
- return { ...rest, initial: destinationInitial(initial, edited, userText, target), edited, universe: target };
482
+ // T8: the user overlay's lower layer is the defaults alone (B2) — a pin cleared over one is tombstoned
483
+ return { ...rest, initial: destinationInitial(initial, edited, userText, target), edited, universe: target, lowerMap: DEFAULT_CONFIG.routing.map };
465
484
  };
466
485
  const candidateMemo = new Map();
467
- const previewCfg = (mode, map, deny) => {
486
+ const previewCfg = (mode, map, deny, stage) => {
468
487
  // B2: the user and repo overlay bytes are part of the key — a layer edited mid-session (a repo
469
- // deny added before a review) re-renders the preview instead of serving the pre-edit one
488
+ // deny added before a review) re-renders the preview instead of serving the pre-edit one.
489
+ // T8: so are the staged tiers/efforts — a metadata edit never serves the preview cached before it
490
+ const metadata = stage ? stagedMetadataOf(stage) : reviewedStage;
470
491
  const userText = currentUserText();
471
- const key = JSON.stringify([mode, map, deny, userText, currentRepoOverlayText(cwd)]);
492
+ const key = JSON.stringify([mode, map, deny, metadata, userText, currentRepoOverlayText(cwd)]);
472
493
  const hit = candidateMemo.get(key);
473
494
  if (hit)
474
495
  return hit;
475
496
  let preview;
476
497
  try {
477
- const bytes = renderFleetOverlayWrite(userText, userLayerWrite(stagedEditable(map, deny), userText, {
498
+ const bytes = renderFleetOverlayWrite(userText, userLayerWrite(stagedEditable(map, deny, metadata), userText, {
478
499
  lowerOverrides,
479
500
  ...(mode !== rm.mode.mode ? { mode } : {}),
480
501
  }));
@@ -509,12 +530,12 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
509
530
  // route() never deny-filters its pool — that contract lives in discoverChannels — so every
510
531
  // preview call rebuilds the pool the staged deny would discover
511
532
  const previewPool = (cfgPreview) => candidateChannels(cfgPreview).filter((c) => disallowedBy(c, cfgPreview.routing) === null);
512
- const modeSpend = (mode, map, deny) => {
533
+ const modeSpend = (mode, map, deny, stage) => {
513
534
  const tierCount = {};
514
535
  let subs = 0;
515
536
  let apiN = 0;
516
537
  let apiUsd = 0;
517
- const preview = previewCfg(mode, map, deny);
538
+ const preview = previewCfg(mode, map, deny, stage);
518
539
  if (!preview.ok)
519
540
  return ` mix: ${previewUnavailable(preview.error)}`;
520
541
  const cfgPreview = preview.cfg;
@@ -547,10 +568,10 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
547
568
  };
548
569
  // B2: the floors the candidate user-layer bytes resolve to — the same config the routed mix ranks
549
570
  // against, so a repo-declared mode that shadows the user's pick previews no floor change
550
- const floorPreview = (mode, map, deny) => {
571
+ const floorPreview = (mode, map, deny, stage) => {
551
572
  if (mode === rm.mode.mode)
552
573
  return [];
553
- const preview = previewCfg(mode, map, deny);
574
+ const preview = previewCfg(mode, map, deny, stage);
554
575
  if (!preview.ok)
555
576
  return []; // modeSpend names the refusal
556
577
  const current = cfg.routing.floors;
@@ -585,8 +606,8 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
585
606
  ? { declaredAt: `routing.floors.${shape}` }
586
607
  : { declaredAt: "routing.mode" };
587
608
  };
588
- const projectedShapeRows = (mode, map, deny) => {
589
- const preview = previewCfg(mode, map, deny);
609
+ const projectedShapeRows = (mode, map, deny, stage) => {
610
+ const preview = previewCfg(mode, map, deny, stage);
590
611
  if (!preview.ok) {
591
612
  return projectFleetWhy(SHAPES.map((shape) => ({
592
613
  id: shape,
@@ -611,7 +632,9 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
611
632
  // a pin. The declaration is the operator's decision; the winner is today's dice.
612
633
  const declaredPool = entry?.pool;
613
634
  const poolPrefix = declaredPool ? `pool(${declaredPool.mode}·${declaredPool.channels.length}) → ` : "";
614
- const effective = `${poolPrefix}${assignment.adapter}:${assignment.model} (${assignment.channel}, ${assignment.tier}) ${costSignal(assignment, cfgPreview.pricing)}${shadowNote}`;
635
+ // T8: the launch effort the routed channel carries — a staged effort shows here before w
636
+ const effort = assignment.effort ? `, effort ${assignment.effort}` : "";
637
+ const effective = `${poolPrefix}${assignment.adapter}:${assignment.model} (${assignment.channel}, ${assignment.tier}${effort}) ${costSignal(assignment, cfgPreview.pricing)}${shadowNote}`;
615
638
  const mapProducedValue = entry?.pin !== undefined || entry?.pool !== undefined
616
639
  || routed.provenance.includes("via prefer");
617
640
  return {
@@ -633,7 +656,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
633
656
  });
634
657
  return projectFleetWhy(values, { repoRoot: cwd, globalDir });
635
658
  };
636
- const routedShapeRows = (mode, map, deny) => projectedShapeRows(mode, map, deny).map(({ id, label }) => ({ id, label }));
659
+ const routedShapeRows = (mode, map, deny, stage) => projectedShapeRows(mode, map, deny, stage).map(({ id, label }) => ({ id, label }));
637
660
  // OBS-530: the picker silently omitted every excluded channel — the operator asked "why is X
638
661
  // missing" three times in one session and the answer lived only in config archaeology. One dim
639
662
  // line now names each bucket. Static buckets (unauthed, unclassified) computed once; the
@@ -802,8 +825,8 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
802
825
  const order = (key) => (carried.includes(key) ? carried.indexOf(key) : carried.length);
803
826
  return [...lines, ...unnamed].sort((a, b) => order(a.key) - order(b.key)).map((row) => row.line);
804
827
  };
805
- const candidatesForShape = (shape, mode, map, deny) => {
806
- const preview = previewCfg(mode, map, deny);
828
+ const candidatesForShape = (shape, mode, map, deny, stage) => {
829
+ const preview = previewCfg(mode, map, deny, stage);
807
830
  if (!preview.ok)
808
831
  return { rows: [], excludedNote: previewUnavailable(preview.error) };
809
832
  const cfgPreview = preview.cfg;
@@ -882,21 +905,13 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
882
905
  staged[scope.key] = state[scope.key];
883
906
  staged.allowOut = state.allowOut ?? initial.allowOut;
884
907
  staged.map = state.map;
885
- // OBS-1182: staged efforts replace the loaded ones; none staged anywhere ⇒ no key, as loaded
886
- const efforts = Object.fromEntries(Object.entries(state.efforts ?? {}).filter(([, models]) => Object.keys(models).length));
887
- if (Object.keys(efforts).length)
888
- staged.efforts = efforts;
908
+ const metadata = stagedMetadataOf(state);
909
+ staged.tiers = metadata.tiers;
910
+ if (metadata.efforts)
911
+ staged.efforts = metadata.efforts;
889
912
  else
890
913
  delete staged.efforts;
891
- const today = new Date().toISOString().slice(0, 10);
892
- for (const classification of state.classifications) {
893
- staged.tiers[classification.adapter] ??= {};
894
- staged.tiers[classification.adapter][classification.model] = {
895
- tier: classification.tier,
896
- provenance: `${classification.note} — fleet ${today}`,
897
- };
898
- }
899
- reviewedStage = { tiers: staged.tiers, efforts: staged.efforts };
914
+ reviewedStage = metadata;
900
915
  // This callback is the sole candidate-overlay builder. The Ink component renders
901
916
  // the diff and asks for confirmation, but owns neither filesystem access nor a writer.
902
917
  // B2: defaults/user/repo are re-read here, at review — the destination is the user overlay.
@@ -971,7 +986,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
971
986
  initialMode: rm.mode.mode,
972
987
  modeOptions: ROUTING_MODES.map((mode) => ({ id: mode, gloss: MODE_GLOSS[mode] })),
973
988
  initialMap: editable.map,
974
- modePreview: (mode, map, deny) => [modeSpend(mode, map, deny), ...floorPreview(mode, map, deny)],
989
+ modePreview: (mode, map, deny, stage) => [modeSpend(mode, map, deny, stage), ...floorPreview(mode, map, deny, stage)],
975
990
  shapeRows: routedShapeRows,
976
991
  candidatesForShape,
977
992
  preferOptionsForShape: (_shape, current) => [
@@ -1050,5 +1065,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
1050
1065
  props,
1051
1066
  commit,
1052
1067
  renderWhy: () => renderFleetWhy(projectedShapeRows(rm.mode.mode, editable.map, stagedDenyOf(editable))),
1068
+ // T8: the memoized candidate every preview callback ranks — read-only, so its identity is checkable
1069
+ previewConfig: previewCfg,
1053
1070
  };
1054
1071
  }
@@ -132,7 +132,11 @@ you were handed the whole picture — list the repository root, open every guida
132
132
  - **The gates are the product.** Seven, defined in \`src/graph/schema.ts\`:
133
133
  \`build test lint evidence scope acceptance review\`. **That is DECLARATION order, not execution order** —
134
134
  the first five run as a battery that stops at its first red, then \`acceptance\` and \`review\` run
135
- CONCURRENTLY (\`run-gates.ts:39\`, *"judge ‖ review"*). The first five are MANDATORY; only \`acceptance\` and
135
+ CONCURRENTLY (\`run-gates.ts:39\`, *"judge ‖ review"*). One repair reorders them: a repair the REVIEW sent
136
+ back (carried \`review:material\`, test selection on, no required repair test) runs build, lint, evidence and
137
+ scope, then \`acceptance\` ‖ \`review\`, then its test screen, so a review that re-reds an unchanged subject
138
+ starts no test at all; a test-red repair keeps the battery first. Either way nothing merges until the FULL
139
+ suite has passed on the merge-candidate commit. The first five are MANDATORY; only \`acceptance\` and
136
140
  \`review\` may be omitted per task. Implementations are in \`src/gates/\` — \`baseline.ts\` (build/test/lint,
137
141
  diffed against a recorded baseline so pre-existing failures are forgiven), \`evidence.ts\`, \`scope.ts\`,
138
142
  \`acceptance.ts\` (its judge reads the DIFF and every criterion must cite a changed hunk), \`review.ts\`
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { parseArgs } from "node:util";
4
4
  import { allAdapters, doctorAgeMs, modelAuthExclusions, probeAll, readDoctor, rolePools, servableExclusions, servabilityLine } from "../../adapters/registry.js";
5
+ import { aliasIdentityHeadlines } from "../../adapters/model-discovery-check.js";
5
6
  import { formatModelAuthLine, contextWindowLints, modelLints, preferEntryLints, ttyVisual } from "../../adapters/model-lints.js";
6
7
  import { GLYPHS, dim, rule, title, warn } from "../../brand.js";
7
8
  import { collateralLints, sourceScopeLints } from "../../compile/collateral.js";
@@ -15,9 +16,10 @@ import { disallowedBy, excludedChannels, exclusionLine, observedSeat, routingEnt
15
16
  import { decayWeight, HALF_LIFE_RUNS, staffLedEvidence } from "../../route/profile.js";
16
17
  import { resolvedFloor, route, RoutingError } from "../../route/router.js";
17
18
  import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
18
- import { modelId, modelProvider, pickReviewer } from "../../gates/review.js";
19
+ import { carriedAuthorVendors, gateReviewerFloor, modelId, modelProvider, pickReviewer } from "../../gates/review.js";
19
20
  import { Journal, loadRoutingProfile, readProfileCursor, recordedGraphDefinitionHash, RUNS_WINDOW } from "../../run/journal.js";
20
21
  import { harnessLine, resolveHarness } from "../harness.js";
22
+ import { codexCommitHeadline } from "../../adapters/codex-commit-check.js";
21
23
  import { channelKey, shq } from "../../adapters/types.js";
22
24
  import { shGit } from "../../run/git.js";
23
25
  import { runLockRunId, runStatusLine } from "../../run/lock.js";
@@ -135,6 +137,13 @@ async function taskInputFindings(tasks, cwd) {
135
137
  }
136
138
  return findings;
137
139
  }
140
+ /**
141
+ * v2.6.5 T9 (D-862): a task the run never dispatched gates under the fresh route's author, so plan can
142
+ * mirror its review eligibility exactly. Once dispatched, resume state only the daemon decides (a restored
143
+ * or failed-over author, a tier climb, carried commits, a parked recheck) picks the author — fail closed.
144
+ * ponytail: unknown, not exact; call production's own resume functions once daemon.ts can export them (POOL).
145
+ */
146
+ const resumeDecidesAuthor = (events, taskId) => events.some((row) => row.taskId === taskId && row.event === "task-dispatch");
138
147
  // v1.89 T4: harnessFrom is the resolver's INPUT — a caller (the byte-pinned goldens) fixes the location
139
148
  // and keeps this machine's absolute paths out of a fixture. The default is the INVOKED entrypoint,
140
149
  // `process.argv[1]`: the bin symlink a global install puts on PATH, which resolves to dist/cli/index.js.
@@ -183,6 +192,26 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
183
192
  .filter((event) => event.event === "gate-result" && event.data.gate === "review" && typeof event.data.reviewer === "string")
184
193
  .map((event) => event.data.reviewer)
185
194
  : [];
195
+ // daemon.ts replayedReviewerExclusions: a journaled review-pool-demotion is excluded from every review
196
+ // pick of the resumed run this plan previews (reviewer role only).
197
+ const replayedReviewerExclusions = [...new Set((matchingEvents ?? [])
198
+ .filter((event) => event.event === "review-pool-demotion" && typeof event.data.reviewer === "string")
199
+ .map((event) => event.data.reviewer))];
200
+ const demotedReviewers = new Set(replayedReviewerExclusions);
201
+ // daemon.ts demotedChannels: the resumed run's worker exclusions (journal replayExcludedChannels).
202
+ const runJournal = matchingRunId ? Journal.open(cwd, matchingRunId) : undefined;
203
+ const demotedWorkers = runJournal?.replayExcludedChannels() ?? new Set();
204
+ // run-gates.ts retired (OBS-1025 add.2): seats with two no-verdicts this run are out of every pick, tallied
205
+ // as daemon.ts seeds reviewNoVerdicts — noVerdict rows only, and a checkout-proof timeout never strikes.
206
+ const noVerdicts = new Map();
207
+ for (const event of matchingEvents ?? []) {
208
+ if (event.event !== "review-no-verdict" || event.data.noVerdict !== true || typeof event.data.reviewer !== "string")
209
+ continue;
210
+ if (event.data.cause === "seat-launch-failed" && event.data.launchCause === "checkout-proof-timeout")
211
+ continue;
212
+ noVerdicts.set(event.data.reviewer, (noVerdicts.get(event.data.reviewer) ?? 0) + 1);
213
+ }
214
+ const retiredReviewers = [...noVerdicts].filter(([, n]) => n >= 2).map(([seat]) => seat);
186
215
  // OBS-947 repair: a resumed run re-dispatches only tasks whose replayed status is pending (or that the
187
216
  // journal never mentions). done, failed and human are all skipped alike, so rendering their preview
188
217
  // picks must not advance the shared reviewHistory — a phantom draw wraps the LRU and makes a later
@@ -243,6 +272,10 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
243
272
  if (judgeDeny?.by === "deny") {
244
273
  lines.push(`REFUSAL OBS-576: judge seat ${cfg.judge.adapter}:${cfg.judge.model} is removed by flat routing.deny (${judgeDeny.entry}) — move worker-only policy to routing.deny.workers`, "");
245
274
  }
275
+ // v2.6.5 T9 (H): alias drift is read from the identity doctor persisted, never re-probed here.
276
+ const aliasHeadlines = aliasIdentityHeadlines(cfg, health);
277
+ if (aliasHeadlines.length)
278
+ lines.push(...aliasHeadlines, "");
246
279
  const entrySeats = routingEntrySeatLines(cfg);
247
280
  if (entrySeats.length)
248
281
  lines.push(...entrySeats, "");
@@ -359,9 +392,36 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
359
392
  return { line: "none — reviewPolicy judge-only: declared leaf paths", cost: 0 };
360
393
  let rotationSeat = 1;
361
394
  let rotationCount = 0;
362
- const reviewer = pickReviewer(author, pools.review, [], cfg.review.prefer ?? [], task.routingHints?.floor, reviewHistory, (seat, count) => { rotationSeat = seat; rotationCount = count; });
395
+ // Dispatched before: no count, no single-seat claim, no previewed reviewer (and so no rotation draw this
396
+ // plan could name). Never a guess.
397
+ if (resumeDecidesAuthor(matchingEvents ?? [], task.id)) {
398
+ lints.push(`${task.id}: eligible cross-vendor review seat count unknown — resume state decides the author`);
399
+ return { line: "unknown — resume state decides the author", cost: 0 };
400
+ }
401
+ // Never dispatched, daemon.ts subjectAuthors is just the fresh author; its vendor stays off the review.
402
+ const authors = [channelKey(author)];
403
+ const authorVendors = carriedAuthorVendors(pools.review, authors);
404
+ // Production's inputs as the task's first review round sees them: excludeReviewers = daemon badReviewers =
405
+ // replayedReviewerExclusions. run-gates.ts:1303 floors on THIS task's journaled reviewers at their dispatched
406
+ // tiers (daemon taskReviewers) ∪ excludeReviewers, so a replayed exclusion RAISES the floor; :1305 picks
407
+ // with excludeReviewers ∪ retired. prefer only reorders (the seat count below passes none).
408
+ const priorReviewers = [...(matchingEvents ?? []).filter((e) => e.taskId === task.id && typeof e.data.reviewer === "string"
409
+ && ((e.event === "gate-result" && e.data.gate === "review") || e.event === "review-no-verdict"))
410
+ .map((e) => ({ reviewer: e.data.reviewer, tier: e.data.reviewerTier })), ...replayedReviewerExclusions];
411
+ const reviewFloor = gateReviewerFloor(task, cfg, author, pools.review, priorReviewers).floor;
412
+ const exclusions = [...replayedReviewerExclusions, ...retiredReviewers];
413
+ const reviewer = pickReviewer(author, pools.review, exclusions, cfg.review.prefer ?? [], reviewFloor, reviewHistory, (seat, count) => { rotationSeat = seat; rotationCount = count; }, demotedReviewers, authorVendors, authors);
363
414
  if (!reviewer)
364
415
  return { line: "none — no cross-vendor reviewer available", cost: 0 };
416
+ // v2.6.5 T9 (H): one flaked or demoted seat away from no reviewer. Enumerate the production filter
417
+ // (pickReviewer at the gate's floor) and count served identities, so two aliases of one model count once.
418
+ const seats = [];
419
+ for (let s; (s = pickReviewer(author, pools.review, [...exclusions, ...seats.map(channelKey)], [], reviewFloor, [], undefined, demotedReviewers, authorVendors, authors));)
420
+ seats.push(s);
421
+ const identities = new Set(seats.map((c) => c.identity ?? modelId(c.model)));
422
+ if (identities.size === 1) {
423
+ lints.push(`${task.id}: exactly one eligible cross-vendor review seat remains — identity ${[...identities][0]} via ${seats.map(channelKey).join(", ")}; one flake or demotion leaves no reviewer`);
424
+ }
365
425
  // Advance the simulated rotation only for tasks the run would actually dispatch next — the same
366
426
  // rule as readyTasks (src/graph/graph.ts): pending, with every dependency already done. A pending
367
427
  // task behind a parked/failed/pending dependency cannot run, so it consumes no draw (RULING-229-14).
@@ -402,7 +462,8 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
402
462
  continue;
403
463
  }
404
464
  try {
405
- const r = route(t, cfg, channels, dispatchProfile);
465
+ // daemon.ts:3322: the resumed run routes around its replayed worker exclusions.
466
+ const r = route(t, cfg, channels, dispatchProfile, undefined, demotedWorkers);
406
467
  routed.push({ taskId: t.id, adapter: r.assignment.adapter, model: r.assignment.model });
407
468
  for (const l of r.lints) {
408
469
  const suf = exclusionReason(l);
@@ -412,7 +473,7 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
412
473
  const judge = judgeSeat(t);
413
474
  const review = reviewSeat(t, r.assignment);
414
475
  cost += est + judge.cost + review.cost;
415
- lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} c${String(t.complexity).padEnd(3)}→ ${r.assignment.adapter}:${r.assignment.model} [${r.assignment.channel}/${r.assignment.tier}]${t.timeoutMinutes !== undefined ? ` (timeout ${t.timeoutMinutes}m)` : ""}${t.humanGate ? " (human gate)" : ""}${est ? ` ~$${est.toFixed(2)}` : ""} — ${r.provenance}`, ` judge: ${judge.line}${judge.cost ? ` ~$${judge.cost.toFixed(2)}` : ""}`, ` review: ${review.line}${review.cost ? ` ~$${review.cost.toFixed(2)}` : ""}`, chainLine(t));
476
+ lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} c${String(t.complexity).padEnd(3)}→ ${r.assignment.adapter}:${r.assignment.model} [${r.assignment.channel}/${r.assignment.tier}]${t.timeoutMinutes !== undefined ? ` (timeout ${t.timeoutMinutes}m)` : ""}${t.humanGate ? " (human gate)" : ""}${est ? ` ~$${est.toFixed(2)}` : ""} — ${r.provenance}`, ...[codexCommitHeadline(r.assignment.adapter, adapters, health)].filter((l) => l !== null), ` judge: ${judge.line}${judge.cost ? ` ~$${judge.cost.toFixed(2)}` : ""}`, ` review: ${review.line}${review.cost ? ` ~$${review.cost.toFixed(2)}` : ""}`, chainLine(t));
416
477
  const d = derivation(t);
417
478
  if (d)
418
479
  lines.push(d);
@@ -7,7 +7,7 @@ export declare const HUMAN_AUTHOR: Assignment;
7
7
  export declare function verifyStateDir(cwd: string): string;
8
8
  /** Linked worktrees share operational state with the repository that owns the common git dir. */
9
9
  export declare function verifyStateRoot(cwd: string): Promise<string>;
10
- /** Cache identity is repository content, never a checkout path. */
10
+ /** Repository content plus the effective runner inputs the daemon hashes; never a checkout path. */
11
11
  export declare function baselineCachePath(cwd: string, baseSha: string, commands: Record<string, string>): string;
12
12
  export declare function excludeAuthorProvider(channels: BillingChannel[], author: BillingChannel): BillingChannel[];
13
13
  export declare const VERIFY_HELP = "usage: tickmarkr verify [--base <ref>] [--criteria <file> | --task <id>] [--json]\nThe final verdict and JSON result are written to stdout; progress and diagnostics are written to stderr.\nDo not merge stdout and stderr (for example with 2>&1): doing so corrupts the verdict stream.";
@@ -1,13 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
- import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
2
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { parseArgs } from "node:util";
6
- import { allAdapters, probeAll, readDoctor, rolePools } from "../../adapters/registry.js";
6
+ import { allAdapters, probeAll, readDoctor, rolePools, writeDoctor } from "../../adapters/registry.js";
7
7
  import { channelKey } from "../../adapters/types.js";
8
8
  import { loadConfig } from "../../config/config.js";
9
9
  import { captureBaseline, detectGateCommands, staleFileCountCommands } from "../../gates/baseline.js";
10
- import { getVerdictStore } from "../../gates/cache.js";
10
+ import { getVerdictStore, runnerInputsHash } from "../../gates/cache.js";
11
11
  import { modelProvider } from "../../gates/review.js";
12
12
  import { runGates } from "../../gates/run-gates.js";
13
13
  import { getTask, loadGraph } from "../../graph/graph.js";
@@ -17,6 +17,7 @@ import { linkNodeModules, removeWorktree, shGit, shGitOk } from "../../run/git.j
17
17
  import { foldOwedChecks, integrationMapped, owedCriteria, Journal, OWED_DISCHARGE_EVENT } from "../../run/journal.js";
18
18
  import { withRepositoryLease } from "../../run/lease.js";
19
19
  import { isPidLive } from "../../run/lock.js";
20
+ import { recordLaunchServices } from "../../run/launchservices-check.js";
20
21
  /**
21
22
  * tickmarkr verify — the gate battery as a standalone command (OPERATING-MODEL-2026-08-11 item 3).
22
23
  *
@@ -86,7 +87,7 @@ const hashParts = (parts) => {
86
87
  hash.update(part);
87
88
  return hash.digest("hex").slice(0, 12);
88
89
  };
89
- /** Cache identity is repository content, never a checkout path. */
90
+ /** Repository content plus the effective runner inputs the daemon hashes; never a checkout path. */
90
91
  export function baselineCachePath(cwd, baseSha, commands) {
91
92
  const lockParts = [];
92
93
  for (const file of LOCKFILES) {
@@ -94,7 +95,7 @@ export function baselineCachePath(cwd, baseSha, commands) {
94
95
  lockParts.push(file, existsSync(path) ? readFileSync(path) : "<absent>");
95
96
  }
96
97
  const commandParts = Object.entries(commands).sort(([a], [b]) => a.localeCompare(b)).map(([name, command]) => `${name}\0${command}\0`);
97
- return join(realpathSync(tmpdir()), "tickmarkr-verify", "cache", `baseline-${baseSha.slice(0, 12)}-${hashParts(lockParts)}-${hashParts(commandParts)}.json`);
98
+ return join(realpathSync(tmpdir()), "tickmarkr-verify", "cache", `baseline-${baseSha.slice(0, 12)}-${hashParts(lockParts)}-${hashParts(commandParts)}-${runnerInputsHash()}.json`);
98
99
  }
99
100
  const verdictlessCommands = (baseline, commands) => Object.keys(commands).filter((name) => baseline.commands[name]?.exitCode === undefined);
100
101
  export function excludeAuthorProvider(channels, author) {
@@ -378,7 +379,7 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
378
379
  console.error(`verify: reusing cached baseline for ${mergeBase.slice(0, 12)} (${names.join(", ")}: ${cachePath})`);
379
380
  return cached;
380
381
  }
381
- console.error(`verify: capturing baseline at merge-base ${mergeBase.slice(0, 12)} for ${names.join(", ")} (cached by base, lockfile and command hashes at ${cachePath})`);
382
+ console.error(`verify: capturing baseline at merge-base ${mergeBase.slice(0, 12)} for ${names.join(", ")} (cached by base, lockfile, command and effective runner hashes at ${cachePath})`);
382
383
  // Unleased captures of the SAME checkout overlap, so each owns a unique base worktree and removes
383
384
  // only its own; the pid in the name lets a later verify sweep what a killed one left behind.
384
385
  for (const entry of readdirSync(stateDir)) {
@@ -577,6 +578,27 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
577
578
  ...review,
578
579
  });
579
580
  }
581
+ // A1 (D-787/D-789): one bounded launchservicesd identity read AFTER the verdict is final, so a suite
582
+ // that restarted the daemon is caught and the verdict, exit code and retries never depend on it.
583
+ // It rides the doctor.json verify read; with none there it creates no state file.
584
+ const doctorRoot = fileRoot("doctor.json");
585
+ const priorDoctor = readDoctor(doctorRoot);
586
+ const launchServices = priorDoctor ? await recordLaunchServices(priorDoctor, priorDoctor) : undefined;
587
+ if (launchServices && launchServices.health !== priorDoctor) {
588
+ // Evidence only, not a probe: keep doctor.json's mtime (the auth-cache freshness signal, doctorAgeMs),
589
+ // and a failed write is an advisory diagnostic — the verdict below is returned regardless.
590
+ const doctorFile = join(doctorRoot, ".tickmarkr", "doctor.json");
591
+ try {
592
+ const { atime, mtime } = statSync(doctorFile);
593
+ writeDoctor(doctorRoot, launchServices.health);
594
+ utimesSync(doctorFile, atime, mtime);
595
+ }
596
+ catch (e) {
597
+ console.error(`verify: launchservicesd evidence not persisted (${e instanceof Error ? e.message : String(e)}) (advisory — verdict unchanged)`);
598
+ }
599
+ }
600
+ if (launchServices?.line)
601
+ console.error(`verify: ${launchServices.line}`);
580
602
  if (values.json) {
581
603
  return { out: JSON.stringify({ base: baseTip, head, mergeBase, green, artifactPath, artifactSha256, artifactAvailability, results,
582
604
  ...(owedAfter && owedLines.length ? { owed: { debt: owedAfter.debt, discharged: dischargeRefusal ? [] : owed.map((o) => o.id), ...(dischargeRefusal ? { refused: dischargeRefusal } : {}) } } : {}),
@@ -1,4 +1,4 @@
1
- import { type FleetEditable, type FleetUniverseRow, type LowerLayerModelOverrides, type RoutingMode } from "./config.js";
1
+ import { type FleetEditable, type FleetUniverseRow, type LowerLayerModelOverrides, type MapEntry, type RoutingMode } from "./config.js";
2
2
  /** Fleet-owned overlay keys — the only config surface `tickmarkr fleet` may write. */
3
3
  export declare const FLEET_OVERLAY_KEYS: readonly ["routing", "tiers"];
4
4
  type FleetOverlayWriteFields = {
@@ -20,6 +20,7 @@ type FleetOverlayWriteFields = {
20
20
  };
21
21
  };
22
22
  universe?: FleetUniverseRow[];
23
+ lowerMap?: Record<string, MapEntry>;
23
24
  };
24
25
  type EffortFree = FleetEditable & {
25
26
  efforts?: undefined;
@@ -1,5 +1,5 @@
1
1
  // Fleet-overlay mutation, serialization, and diff rendering for the `tickmarkr fleet` write path.
2
- import { isMap, isScalar, isSeq, parseDocument, stringify, visit } from "yaml";
2
+ import { isAlias, isMap, isNode, isScalar, isSeq, parseDocument, stringify, visit } from "yaml";
3
3
  import { DENY_SCOPES, universeCovers, universeEntryMatches } from "./config.js";
4
4
  // OBS-1099 add.1: the writer ranges over the schema-derived scopes. The flat (all-seats) scopes
5
5
  // take part in the membership/allow-form write; every nested scope (routing.deny.workers.*) is a
@@ -161,7 +161,71 @@ function setStringSequencePreservingComments(doc, path, values) {
161
161
  // Total forms: a delete treats the masked subtree as absent (the operator's tombstone already
162
162
  // denies it, and survives untouched); a set clears the scalar and rebuilds what fleet owns —
163
163
  // the routing.allow precedent below.
164
+ // T8 repair: an alias is no tombstone — `spec: *empty` resolves to a mapping. Each alias on the path
165
+ // is replaced by an un-anchored copy of its target, so what it resolves to decides, and a write under
166
+ // it never edits the anchored node its other aliases share.
167
+ function materializeAliases(doc, path) {
168
+ for (let depth = 1; depth < path.length; depth++) {
169
+ const node = doc.getIn(path.slice(0, depth), true);
170
+ if (!isAlias(node))
171
+ continue;
172
+ const target = node.resolve(doc);
173
+ if (target === undefined)
174
+ return;
175
+ doc.setIn(path.slice(0, depth), detachedCopy(doc, node, target, new Set()));
176
+ }
177
+ }
178
+ const nodesOf = (root) => {
179
+ const out = [];
180
+ visit(root, (_key, node) => {
181
+ if (isNode(node))
182
+ out.push(node);
183
+ });
184
+ return out;
185
+ };
186
+ // D-821: a copy carries NO anchor (a nested `&empty` would rebind `plan: *empty` and every later
187
+ // alias to the edited copy), and each inner alias must still name what it named in the original —
188
+ // one whose anchor is redefined before `at` is replaced by a detached copy of its original target.
189
+ function detachedCopy(doc, at, target, expanding) {
190
+ if (expanding.has(target))
191
+ throw new Error(`fleet cannot detach the recursive YAML alias *${at.source}`);
192
+ const visible = new Map();
193
+ visit(doc, (_key, node) => {
194
+ if (node === at)
195
+ return visit.BREAK;
196
+ if (isNode(node) && !isAlias(node) && node.anchor)
197
+ visible.set(node.anchor, node);
198
+ });
199
+ const copy = target.clone();
200
+ const originals = nodesOf(target);
201
+ const replace = new Map();
202
+ nodesOf(copy).forEach((node, index) => {
203
+ if (!isAlias(node)) {
204
+ node.anchor = undefined;
205
+ return;
206
+ }
207
+ const resolved = originals[index].resolve(doc);
208
+ if (resolved !== undefined && visible.get(node.source) !== resolved) {
209
+ replace.set(node, detachedCopy(doc, at, resolved, new Set([...expanding, target])));
210
+ }
211
+ });
212
+ if (!replace.size)
213
+ return copy;
214
+ visit(copy, (_key, node) => (isNode(node) ? replace.get(node) : undefined));
215
+ return copy;
216
+ }
217
+ function underScalar(doc, path) {
218
+ for (let depth = 1; depth < path.length; depth++) {
219
+ const node = doc.getIn(path.slice(0, depth), true);
220
+ if (node === undefined)
221
+ return false;
222
+ if (!isMap(node))
223
+ return true;
224
+ }
225
+ return false;
226
+ }
164
227
  function deleteAt(doc, path) {
228
+ materializeAliases(doc, path); // an aliased mapping is no tombstone: Auto under `spec: *pinned` deletes the pin
165
229
  for (let depth = 1; depth < path.length; depth++) {
166
230
  if (!isMap(doc.getIn(path.slice(0, depth), true)))
167
231
  return;
@@ -171,6 +235,7 @@ function deleteAt(doc, path) {
171
235
  doc.deleteIn(path);
172
236
  }
173
237
  function setAt(doc, path, value) {
238
+ materializeAliases(doc, path); // edit a copy of an aliased mapping, never drop what it resolved to
174
239
  for (let depth = 1; depth < path.length; depth++) {
175
240
  const node = doc.getIn(path.slice(0, depth), true);
176
241
  if (node === undefined)
@@ -281,11 +346,19 @@ export function renderFleetOverlayWrite(priorBytes, write) {
281
346
  for (const shape of new Set([...Object.keys(initial.map), ...Object.keys(edited.map)])) {
282
347
  const before = initial.map[shape];
283
348
  const after = edited.map[shape];
284
- if (JSON.stringify(before?.pin) !== JSON.stringify(after?.pin)) {
349
+ const pinPath = ["routing", "map", shape, "pin"];
350
+ const declares = after?.pin !== undefined || after?.pool !== undefined || after?.prefer !== undefined;
351
+ if (!declares && write.lowerMap?.[shape]?.pin !== undefined && JSON.stringify(before) !== JSON.stringify(after)) {
352
+ // pin: null tombstone — masks the lower-layer pin; a scalar tombstone above it already masks the entry
353
+ materializeAliases(doc, pinPath);
354
+ if (!underScalar(doc, pinPath))
355
+ setScalarPreservingComment(doc, pinPath, null);
356
+ }
357
+ else if (JSON.stringify(before?.pin) !== JSON.stringify(after?.pin)) {
285
358
  if (after?.pin === undefined)
286
- deleteAt(doc, ["routing", "map", shape, "pin"]);
359
+ deleteAt(doc, pinPath);
287
360
  else
288
- setAt(doc, ["routing", "map", shape, "pin"], doc.createNode(after.pin));
361
+ setAt(doc, pinPath, doc.createNode(after.pin));
289
362
  }
290
363
  if (JSON.stringify(before?.pool) !== JSON.stringify(after?.pool)) {
291
364
  const path = ["routing", "map", shape, "pool"];
@@ -31,16 +31,18 @@ export declare class OrcaError extends Error {
31
31
  readonly raw: string;
32
32
  readonly code?: string;
33
33
  readonly runtimeId?: string;
34
+ readonly launchCause?: SeatLaunchCause;
34
35
  constructor(family: string, reason: string, raw: string, opts?: {
35
36
  code?: string;
36
37
  runtimeId?: string;
38
+ launchCause?: SeatLaunchCause;
37
39
  });
38
40
  }
39
41
  /** The slot cannot be addressed: dead/unknown terminal record, or a handle that cannot be recovered
40
42
  * to exactly one owned terminal in the slot's own worktree. Never a silent false or empty string. */
41
43
  export declare class OrcaUnavailableError extends OrcaError {
42
44
  readonly terminalStatus?: string | undefined;
43
- constructor(family: string, reason: string, raw: string, terminalStatus?: string | undefined);
45
+ constructor(family: string, reason: string, raw: string, terminalStatus?: string | undefined, launchCause?: SeatLaunchCause);
44
46
  }
45
47
  export interface OrcaEnvelope {
46
48
  result: Record<string, unknown>;
@@ -83,6 +85,10 @@ export declare function terminalWorktree(term: Record<string, unknown>): string
83
85
  export declare function canonicalWorktreePath(path: string): string;
84
86
  /** The proof line a worker terminal prints first: create, recovery and focus read it back. */
85
87
  export declare const CHECKOUT_MARK = "TICKMARKR_CHECKOUT";
88
+ /** E1: the one typed launch cause a driver attaches to its launch failure. `checkout-proof-timeout`
89
+ * means the terminal printed NO proof frame by the ceiling — a slow host, not a misplaced seat. A
90
+ * foreign or malformed frame is placement evidence and carries no cause (never a timeout exemption). */
91
+ export type SeatLaunchCause = "checkout-proof-timeout";
86
92
  /** The exact bytes the create command prints as its first line. */
87
93
  export declare function checkoutProofLine(checkout: string): string;
88
94
  /** Every complete frame in a scrollback, decoded and canonicalized; whether an incomplete one was seen. */
@@ -250,6 +256,18 @@ export declare class OrcaDriver implements ExecutorDriver {
250
256
  * that supplied the ownership listing — else another terminal's or another runtime's bytes were
251
257
  * answered and nothing is proven. Proven means provesCheckout: exact canonical full-path equality
252
258
  * of a complete frame, no other checkout named, no incomplete frame.
259
+ * E1: with a `deadline` (create's window) there is ONE absolute deadline: a read is issued only while
260
+ * budget is left and is handed exactly that budget, and a page that still lands past it is late —
261
+ * nothing late proves the checkout. `seen` is what the scrollback showed, for the typed timeout:
262
+ * `misplaced` — a frame naming another checkout, a cut frame or an unparseable mark in anything read
263
+ * (the anchor's tail and every page included, even when paging stopped or a later read threw);
264
+ * `absent` — ONLY when every page down to the start of the scrollback was read before the ceiling
265
+ * and none carries a trace of the mark; `unread` — every other exit: PROOF_PAGES reached, a limited
266
+ * read with no (or the same) next cursor or a truncated anchor without one, a read that threw, a read
267
+ * the ceiling cut off or never let start (the anchor included), or a page that landed late (even one
268
+ * carrying the right proof), a malformed page (its tail, cursors or paging flags), or a blind stream
269
+ * page (isBlindStreamPage). Unread output is unknown — never an empty page, never absence.
270
+ * Without a deadline (reconcile, recovery) only `proven` is consumed.
253
271
  */
254
272
  private checkoutProven;
255
273
  private recover;