tickmarkr 2.6.6 → 2.6.8

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 (51) hide show
  1. package/dist/adapters/claude-code.js +8 -2
  2. package/dist/adapters/codex-commit-check.d.ts +3 -2
  3. package/dist/adapters/codex-commit-check.js +37 -10
  4. package/dist/adapters/codex.js +10 -4
  5. package/dist/cli/commands/beat.d.ts +7 -2
  6. package/dist/cli/commands/beat.js +144 -77
  7. package/dist/cli/commands/fleet.js +66 -21
  8. package/dist/cli/commands/init.js +18 -7
  9. package/dist/cli/commands/plan.js +16 -18
  10. package/dist/cli/commands/verify.js +4 -2
  11. package/dist/cli/help.d.ts +1 -0
  12. package/dist/cli/help.js +14 -6
  13. package/dist/cli/index.d.ts +1 -1
  14. package/dist/cli/index.js +1 -1
  15. package/dist/drivers/herdr.js +2 -2
  16. package/dist/drivers/orca.d.ts +12 -4
  17. package/dist/drivers/orca.js +200 -39
  18. package/dist/gates/baseline.d.ts +9 -0
  19. package/dist/gates/baseline.js +14 -3
  20. package/dist/gates/cache.js +1 -1
  21. package/dist/gates/evidence.js +3 -3
  22. package/dist/gates/review.d.ts +1 -1
  23. package/dist/gates/review.js +77 -13
  24. package/dist/gates/run-gates.d.ts +21 -6
  25. package/dist/gates/run-gates.js +439 -224
  26. package/dist/gates/test-manifest.d.ts +42 -0
  27. package/dist/gates/test-manifest.js +203 -118
  28. package/dist/run/beat-lifecycle.d.ts +239 -0
  29. package/dist/run/beat-lifecycle.js +1782 -0
  30. package/dist/run/daemon.d.ts +4 -0
  31. package/dist/run/daemon.js +129 -40
  32. package/dist/run/git-trust.d.ts +102 -0
  33. package/dist/run/git-trust.js +334 -0
  34. package/dist/run/git.d.ts +24 -3
  35. package/dist/run/git.js +169 -30
  36. package/dist/run/journal.js +15 -3
  37. package/dist/run/lease.d.ts +23 -0
  38. package/dist/run/lease.js +232 -38
  39. package/dist/run/merge.js +4 -4
  40. package/dist/run/stall.js +20 -1
  41. package/dist/run/supervision.d.ts +8 -0
  42. package/dist/run/supervision.js +24 -4
  43. package/dist/run/verification-job.d.ts +53 -0
  44. package/dist/run/verification-job.js +159 -0
  45. package/dist/tui/ink/fleet-app.d.ts +16 -2
  46. package/dist/tui/ink/fleet-app.js +265 -141
  47. package/package.json +1 -1
  48. package/skills/tickmarkr-auto/SKILL.md +80 -27
  49. package/skills/tickmarkr-loop/SKILL.md +87 -27
  50. package/skills/tickmarkr-overseer/SKILL.md +125 -47
  51. package/skills/tickmarkr-overseer/scripts/watch-journal.sh +9 -7
@@ -1,3 +1,4 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import { existsSync, linkSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, renameSync, rmdirSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
2
3
  import { randomUUID } from "node:crypto";
3
4
  import { dirname, join, resolve } from "node:path";
@@ -77,6 +78,8 @@ export class OrcaError extends Error {
77
78
  code;
78
79
  runtimeId;
79
80
  launchCause;
81
+ /** v2.6.8 T1: the CLI was killed at the timeout it was handed — the one evidence that a deadline interrupted it. */
82
+ timedOut;
80
83
  constructor(family, reason, raw, opts = {}) {
81
84
  super(`orca ${family} failed — ${reason}; raw response:\n${raw}`);
82
85
  this.family = family;
@@ -86,6 +89,7 @@ export class OrcaError extends Error {
86
89
  this.code = opts.code;
87
90
  this.runtimeId = opts.runtimeId;
88
91
  this.launchCause = opts.launchCause;
92
+ this.timedOut = opts.timedOut;
89
93
  }
90
94
  }
91
95
  /** The slot cannot be addressed: dead/unknown terminal record, or a handle that cannot be recovered
@@ -209,7 +213,9 @@ export const CHECKOUT_MARK = "TICKMARKR_CHECKOUT";
209
213
  // carries no whitespace or quotes, survives renderer wrapping (hex rows re-join losslessly), and is
210
214
  // either complete (length matches, terminator present) or nothing. A prefix of another checkout can
211
215
  // never decode to this one; `A` versus `A B`, `…--T1` versus `…--T10` are different frames.
212
- const CHECKOUT_FRAME_RE = /TICKMARKR_CHECKOUT (\d+):([0-9a-f]*)(;?)/g;
216
+ // v2.6.8 T1: the header separator is optional — a terminal that wraps the frame AT it leaves the space
217
+ // at a row edge, where joinWrapped trims it — so where the frame wraps never decides what it is.
218
+ const CHECKOUT_FRAME_RE = /TICKMARKR_CHECKOUT ?(\d+):([0-9a-f]*)(;?)/g;
213
219
  const PROOF_PAGES = 16; // pages read from the oldest cursor before the proof is declared absent
214
220
  // E1: the ceiling for the wrapper shell's startup proof. A loaded host can print a review seat's proof
215
221
  // long after the old 2 s window closed (the window a healthy seat was retired on); a proof first
@@ -228,8 +234,110 @@ const MARK_TRACE = CHECKOUT_MARK.slice(0, CHECKOUT_MARK.indexOf("_") + 2);
228
234
  function misplacedProof(text, checkout) {
229
235
  const { complete, incomplete } = checkoutFrames(text);
230
236
  const joined = joinWrapped(text);
237
+ const frames = [...joined.matchAll(CHECKOUT_FRAME_RE)];
231
238
  return incomplete || complete.some((c) => c !== checkout)
232
- || joined.split(MARK_TRACE).length - 1 > [...joined.matchAll(CHECKOUT_FRAME_RE)].length;
239
+ || joined.split(MARK_TRACE).length - 1 > frames.length || danglingMarkRow(text, joined, frames);
240
+ }
241
+ /** v2.6.8 T1: a row that is only a mark prefix too short to carry MARK_TRACE (`T` … `TICKMARKR_`) — the
242
+ * same partial frame ceilingCutFrame types as a cut — is placement evidence unless the rejoined text
243
+ * continues it into the mark there, or it lies inside a frame a wrap split (`…CHECKOU` | `T` | ` 40:…;`).
244
+ * ponytail: a long line whose last wrapped row is just `T` refuses too; fail-closed costs a relaunch. */
245
+ function danglingMarkRow(text, joined, frames) {
246
+ let at = 0;
247
+ for (const row of text.split("\n").map(joinWrapped)) {
248
+ const start = at;
249
+ at += row.length;
250
+ if (row.length === 0 || row.length >= MARK_TRACE.length || !MARK_TRACE.startsWith(row))
251
+ continue;
252
+ if (joined.startsWith(MARK_TRACE, start))
253
+ continue;
254
+ if (!frames.some((m) => m.index < start && start < m.index + m[0].length))
255
+ return true;
256
+ }
257
+ return false;
258
+ }
259
+ /** v2.6.8 T1: is `fragment` (rows rejoined) a strict prefix of the OWN proof frame `own` — the frame the
260
+ * unread rows could still complete into the proof? The separator is optional exactly as in
261
+ * CHECKOUT_FRAME_RE. A prefix no continuation can make the own frame (`TICKMARKR_CHECKOUT 1:ff`, a foreign
262
+ * length or hex) is placement evidence whatever the unread rows hold — never a cut. */
263
+ function framePrefix(fragment, own) {
264
+ return fragment.length > 0 && [own, own.replace(" ", "")].some((frame) => fragment.length < frame.length && frame.startsWith(fragment));
265
+ }
266
+ /** v2.6.8 T1: the evidence that paging left a frame unread — the paged text ENDS in a strict prefix of
267
+ * the own frame (cutFrameRow), and nothing read before it — the pages' reading of a position or the anchor's —
268
+ * nor the anchor's rows past it, is placement evidence. A malformed marker read whole is never a prefix,
269
+ * so it stays a placement refusal. A row that merely continues the cut frame (`TICKMARKR_CHECKOU`, then
270
+ * `T`) is never taken for a new frame.
271
+ * ponytail: the typed command's echo cut exactly after its `TICKMARKR` printf argument reads the same,
272
+ * and so does a last row that merely reads `T`; either way the proof line is unread at the ceiling. */
273
+ function ceilingCutFrame(pages, tail, checkout) {
274
+ const k = cutFrameRow(pages.rows, checkoutProofLine(checkout));
275
+ if (k === undefined)
276
+ return false;
277
+ // The anchor's rows past the pages' last position are the cut frame's continuation (`…CHECKOU` | `T` |
278
+ // ` 40:…;`), read after it; an anchor past a gap, or at an unknown position, stands alone and must read
279
+ // clean.
280
+ const beyond = tailBeyond(pages, tail), cut = pages.rows.slice(k);
281
+ const continued = beyond === undefined ? !misplacedProof(tail.rows.join("\n"), checkout)
282
+ : beyond.length === 0 || !misplacedProof([...cut, ...beyond].join("\n"), checkout);
283
+ if (!continued || misplacedProof(pages.rows.slice(0, k).join("\n"), checkout))
284
+ return false;
285
+ if (beyond === undefined)
286
+ return true;
287
+ // An anchor sharing positions with the pages is a SECOND reading of those rows, read WHOLE before them:
288
+ // it is classified in place and whole — only the pages' own cut rows past it are dropped — so a foreign,
289
+ // malformed or unterminated mark the anchor read where a later page read harmless text or the cut frame
290
+ // is never forgotten, and a trailing prefix in the anchor's own reading never inherits the pages' cut.
291
+ const from = tail.at - pages.at, ownEnd = Math.max(0, from + tail.rows.length);
292
+ const anchored = [...pages.rows.slice(0, Math.max(0, from)), ...tail.rows, ...pages.rows.slice(ownEnd, Math.max(k, ownEnd))];
293
+ return !misplacedProof(anchored.join("\n"), checkout);
294
+ }
295
+ /** v2.6.8 T1: the row a trailing strict prefix of the own frame starts at, or undefined. A frame starts its own row
296
+ * and the terminal may wrap it anywhere, so rows are rejoined first and the cut frame is the LONGEST run
297
+ * of trailing rows that rejoins into a prefix; only the trailing frame-alphabet run can hold it. */
298
+ function cutFrameRow(rows, own) {
299
+ const joinedRows = rows.map(joinWrapped), joined = joinedRows.join("");
300
+ let run = joined.length;
301
+ while (run > 0 && /[A-Z0-9a-f_ :]/.test(joined[run - 1]))
302
+ run--;
303
+ for (let k = 0, at = 0; k < rows.length; at += joinedRows[k].length, k++) {
304
+ if (joinedRows[k] && at >= run && framePrefix(joined.slice(at), own))
305
+ return k;
306
+ }
307
+ return undefined;
308
+ }
309
+ /** A row the read returned but that is not text: kept in its position so it never joins its neighbours. */
310
+ const UNREADABLE_ROW = "�";
311
+ function linePosition(cursor) {
312
+ return typeof cursor === "string" && /^\d+$/.test(cursor) ? Number(cursor) : undefined;
313
+ }
314
+ /** v2.6.8 T1: the anchor's rows past the pages, by ROW IDENTITY — scrollback position, never text: none when
315
+ * the anchor lies inside the pages (the scrollback may have grown between the reads), the rest when it
316
+ * overlaps or abuts their end, undefined when a gap separates them or either position is unknown. */
317
+ function tailBeyond(pages, tail) {
318
+ if (pages.at === undefined || tail.at === undefined)
319
+ return undefined;
320
+ const end = pages.at + pages.rows.length;
321
+ if (tail.at > end || tail.at + tail.rows.length < pages.at)
322
+ return undefined;
323
+ return tail.rows.slice(Math.max(0, end - tail.at));
324
+ }
325
+ /** v2.6.8 T1: the anchor read in place among the paged rows: the pages' rows before its position, every one
326
+ * of its own rows, then the pages' rows after it. An anchor sharing or abutting no position with the
327
+ * pages, or at an unknown one, stands alone (fail-closed). The pages' own reading is classified too. */
328
+ function tailInContext(pages, tail) {
329
+ if (tailBeyond(pages, tail) === undefined)
330
+ return tail.rows.join("\n");
331
+ const from = tail.at - pages.at;
332
+ return [...pages.rows.slice(0, Math.max(0, from)), ...tail.rows, ...pages.rows.slice(Math.max(0, from + tail.rows.length))].join("\n");
333
+ }
334
+ /** v2.6.8 T1: the caller's live launch check, consulted by create at its actual submission — after the runtime
335
+ * and worktree lookups it awaits, the last moment before a terminal is launched. A throwing guard withdraws
336
+ * the launch (nothing is created) and its error is the launch failure. Async-scoped, so it reaches create
337
+ * through every driver wrapper that forwards run. */
338
+ const launchGuards = new AsyncLocalStorage();
339
+ export function withLaunchGuard(guard, run) {
340
+ return launchGuards.run(guard, run);
233
341
  }
234
342
  /** The exact bytes the create command prints as its first line. */
235
343
  export function checkoutProofLine(checkout) {
@@ -257,8 +365,8 @@ export function checkoutFrames(text) {
257
365
  * frame names anything else, and no frame is incomplete. Full-path equality after canonicalization —
258
366
  * never a prefix, a substring, or a whitespace-terminated fragment. */
259
367
  export function provesCheckout(text, checkout) {
260
- const { complete, incomplete } = checkoutFrames(text);
261
- return !incomplete && complete.includes(checkout) && complete.every((c) => c === checkout);
368
+ // v2.6.8 T1: a malformed mark the frame grammar cannot parse is placement evidence too (misplacedProof).
369
+ return checkoutFrames(text).complete.includes(checkout) && !misplacedProof(text, checkout);
262
370
  }
263
371
  /**
264
372
  * Everything a terminal on the tracked worktree runs before the payload: enter the checkout (a
@@ -725,7 +833,7 @@ export class OrcaDriver {
725
833
  throw new OrcaError(family, e.reason, raw, { code: e.code, runtimeId: e.runtimeId });
726
834
  }
727
835
  }
728
- throw new OrcaError(family, `orca CLI exited ${r.code}${r.timedOut ? " after timeout" : ""}`, raw);
836
+ throw new OrcaError(family, `orca CLI exited ${r.code}${r.timedOut ? " after timeout" : ""}`, raw, r.timedOut === true ? { timedOut: true } : {});
729
837
  }
730
838
  return parseEnvelope(family, r.stdout, raw);
731
839
  }
@@ -859,6 +967,7 @@ export class OrcaDriver {
859
967
  // into the checkout, because Orca cannot be asked to place a terminal in a path it does not
860
968
  // track (OBS-1004). Shell startup can swallow that prefix; prove it from scrollback below.
861
969
  const payload = `export ${FORK_CAP_ENV}=${shq(process.env[FORK_CAP_ENV] ?? resolvedForkCap())}; ${cmd}`;
970
+ launchGuards.getStore()?.(); // v2.6.8 T1: eligibility is re-read here, after every awaited lookup above
862
971
  const env = await this.call("create", [
863
972
  "terminal", "create", "--worktree", `path:${tracked}`, "--title", st.title, "--command", inCheckout(st.cwd, payload),
864
973
  ], this.cliCwd(st));
@@ -892,7 +1001,9 @@ export class OrcaDriver {
892
1001
  let misplaced, unread;
893
1002
  let proof = await this.checkoutProven(handle, st.cwd, this.cliCwd(st), env.runtimeId, deadline);
894
1003
  for (;;) {
895
- if (proof.proven)
1004
+ // v2.6.8 T1: placement evidence is sticky — a later poll whose scrollback no longer shows the
1005
+ // foreign, malformed or partial mark an earlier one read never turns it into proof.
1006
+ if (proof.proven && misplaced === undefined)
896
1007
  break;
897
1008
  if (proof.seen === "misplaced")
898
1009
  misplaced = proof;
@@ -906,15 +1017,16 @@ export class OrcaDriver {
906
1017
  break;
907
1018
  proof = await this.checkoutProven(handle, st.cwd, this.cliCwd(st), env.runtimeId, deadline);
908
1019
  }
909
- if (!proof.proven) {
1020
+ if (!proof.proven || misplaced !== undefined) {
910
1021
  try {
911
1022
  await this.closeTerminal({ ...st, handle, runtimeId: env.runtimeId });
912
1023
  }
913
1024
  catch { /* best effort under the answering runtime; the latch prevents another launch */ }
914
1025
  // The typed timeout only when EVERY poll read its whole scrollback inside the ceiling and found no
915
- // trace of the mark; one misplaced or unread poll anywhere in the window denies it.
1026
+ // trace of the mark — or the ceiling cut the last one mid-frame; one misplaced or unread poll
1027
+ // anywhere in the window denies it.
916
1028
  const evidence = misplaced ?? unread ?? proof;
917
- throw this.latched("create", st, `terminal ${handle} does not prove checkout ${st.cwd} within ${CHECKOUT_PROOF_TIMEOUT_MS} ms (${evidence.reason})`, env.raw, evidence.seen === "absent" ? "checkout-proof-timeout" : undefined);
1029
+ throw this.latched("create", st, `terminal ${handle} does not prove checkout ${st.cwd} within ${CHECKOUT_PROOF_TIMEOUT_MS} ms (${evidence.reason})`, env.raw, evidence.seen === "absent" || evidence.seen === "cut" ? "checkout-proof-timeout" : undefined);
918
1030
  }
919
1031
  st.handle = handle;
920
1032
  // The handle is bound to the runtime identity that ANSWERED its create.
@@ -1036,12 +1148,14 @@ export class OrcaDriver {
1036
1148
  * the terminal record must name `handle` and `_meta.runtimeId` must be `runtimeId` — the runtime
1037
1149
  * that supplied the ownership listing — else another terminal's or another runtime's bytes were
1038
1150
  * answered and nothing is proven. Proven means provesCheckout: exact canonical full-path equality
1039
- * of a complete frame, no other checkout named, no incomplete frame.
1151
+ * of a complete frame, no other checkout named, no incomplete frame or unparseable mark — in the pages
1152
+ * or the anchor's tail (v2.6.8 T1).
1040
1153
  * E1: with a `deadline` (create's window) there is ONE absolute deadline: a read is issued only while
1041
1154
  * budget is left and is handed exactly that budget, and a page that still lands past it is late —
1042
1155
  * nothing late proves the checkout. `seen` is what the scrollback showed, for the typed timeout:
1043
1156
  * `misplaced` — a frame naming another checkout, a cut frame or an unparseable mark in anything read
1044
- * (the anchor's tail and every page included, even when paging stopped or a later read threw);
1157
+ * (the anchor's tail and every page included, even when paging stopped, a later read threw or the page's
1158
+ * own cursors were malformed — v2.6.8 T1; the tail read after the pages it overlaps, tailInContext);
1045
1159
  * `absent` — ONLY when every page down to the start of the scrollback was read before the ceiling
1046
1160
  * and none carries a trace of the mark; `unread` — every other exit: PROOF_PAGES reached, a limited
1047
1161
  * read with no (or the same) next cursor or a truncated anchor without one, a read that threw, a read
@@ -1057,10 +1171,13 @@ export class OrcaDriver {
1057
1171
  // F (D-832): a page read whose own metadata does not say how complete it is (classifyProofPage):
1058
1172
  // its bytes are evidence, but the scrollback it came from is not known to be read whole.
1059
1173
  let late = false, unknown = false;
1060
- const page = async (cursor) => {
1174
+ // v2.6.8 T1: `keep` takes every row a bound read returned, at its position, the moment it is read —
1175
+ // BEFORE any row type, cursor or overlap validation can discard it: a bound page whose cursor metadata
1176
+ // is malformed, or that carries a non-string row, is still evidence of every row it shows.
1177
+ const page = async (keep, cursor) => {
1061
1178
  const budget = deadline === undefined ? undefined : deadline - this.time.now();
1062
1179
  if (budget !== undefined && budget <= 0)
1063
- return undefined; // no budget left: the read is never issued
1180
+ return undefined; // no budget left: the read is never issued (the anchor only)
1064
1181
  let env;
1065
1182
  try {
1066
1183
  env = await this.call("read", [
@@ -1068,7 +1185,10 @@ export class OrcaDriver {
1068
1185
  ], from, budget);
1069
1186
  }
1070
1187
  catch (error) {
1071
- if (deadline === undefined || this.time.now() < deadline)
1188
+ // v2.6.8 T1: only a read the ceiling actually INTERRUPTED — killed at the budget it was handed — is
1189
+ // the cut. A read that failed for any other reason (a nonzero exit, unparseable output, an invocation
1190
+ // exception) keeps its own failure at every arrival time, at or past the ceiling included.
1191
+ if (deadline === undefined || !(error instanceof OrcaError && error.timedOut === true))
1072
1192
  throw error;
1073
1193
  return undefined; // the read the ceiling cut off: unread
1074
1194
  }
@@ -1084,6 +1204,16 @@ export class OrcaDriver {
1084
1204
  // E1: only a well-formed page is read text. A tail that is missing, not an array or carries a
1085
1205
  // non-string row (tailText), or a cursor that is not a non-empty string is a malformed page: it
1086
1206
  // throws, so it is unread output — never filtered to an empty page that would read as an absent proof.
1207
+ // v2.6.8 T1: the tail's rows are kept first — a non-string row as UNREADABLE_ROW in its place — so
1208
+ // neither a malformed cursor nor a malformed row discards the foreign, malformed or partial mark the
1209
+ // rest show: that evidence stays sticky for the whole create window. A cursor page's rows start at
1210
+ // its cursor; the anchor's end at its nextCursor (an impossible or unknown position stays unknown).
1211
+ if (Array.isArray(term.tail)) {
1212
+ const rows = term.tail.map((row) => typeof row === "string" ? row : UNREADABLE_ROW);
1213
+ const at = cursor === undefined ? (linePosition(term.nextCursor) ?? -1) - rows.length : linePosition(cursor);
1214
+ keep({ rows, ...(at !== undefined && at >= 0 ? { at } : {}) });
1215
+ }
1216
+ this.tailText("read", term, env.raw);
1087
1217
  for (const key of ["oldestCursor", "nextCursor"]) {
1088
1218
  if (term[key] !== undefined && str(term[key]) === undefined)
1089
1219
  throw new OrcaError("read", `proof page ${key} is not a cursor`, env.raw);
@@ -1093,42 +1223,51 @@ export class OrcaDriver {
1093
1223
  // stream alone cannot tell them apart, so its empty tail is unread scrollback, never absence.
1094
1224
  if (isBlindStreamPage(term))
1095
1225
  throw new OrcaError("read", "proof page is a blind stream page (status exited, empty tail): its scrollback is unread", env.raw);
1096
- const text = this.tailText("read", term, env.raw);
1097
1226
  // F: paging flags missing, not boolean or contradicted by the cursors leave the page's bytes read
1098
1227
  // but its completeness unknown — it ends paging, and the poll can never read as absent.
1099
1228
  const kind = classifyProofPage(term);
1100
1229
  if (kind === "unread")
1101
1230
  unknown = true;
1102
- return { term, text, kind };
1231
+ return { term, kind };
1103
1232
  };
1104
1233
  // Every validated read is evidence, whatever stops the next one: the anchor's tail and the pages
1105
1234
  // read from the oldest cursor are inspected even when paging hits PROOF_PAGES or a read throws.
1106
- let tail = "", text = "";
1107
- const described = (why) => {
1108
- const misplaced = checkout !== undefined && [text, tail].some((t) => misplacedProof(t, checkout));
1109
- const shown = [text, tail].map(checkoutFrames);
1235
+ // v2.6.8 T1: ONE record of everything read — the anchor's rows and the pages' rows, each row once at
1236
+ // its position. `pages` keeps a position only while every page starts where the rows before it end.
1237
+ let tail = { rows: [] }, pages = { rows: [] }, pagedEnd;
1238
+ const text = () => pages.rows.join("\n");
1239
+ // `cut`: the ceiling stopped this read. A frame it left incomplete at the end of the paged text is
1240
+ // the cut, not the terminal's misprint — unread, typed as the timeout — unless anything else read
1241
+ // (a complete foreign frame, a malformed marker read whole) is placement evidence (ceilingCutFrame).
1242
+ const described = (why, cut = false) => {
1243
+ const after = tailInContext(pages, tail);
1244
+ const misplaced = checkout !== undefined && [text(), after].some((t) => misplacedProof(t, checkout));
1245
+ const shown = [text(), after].map(checkoutFrames);
1110
1246
  const named = [...new Set(shown.flatMap((f) => f.complete))];
1111
- const flaw = shown.some((f) => f.incomplete) ? " and carries an incomplete proof frame"
1112
- : misplaced && named.length === 0 ? " and carries a malformed proof marker" : "";
1247
+ const ceilingCut = cut && checkout !== undefined && ceilingCutFrame(pages, tail, checkout);
1248
+ const flaw = ceilingCut ? " and carries a proof frame the ceiling cut off before it was read whole"
1249
+ : shown.some((f) => f.incomplete) ? " and carries an incomplete proof frame"
1250
+ : misplaced && named.every((c) => c === checkout) ? " and carries a malformed proof marker" : "";
1113
1251
  return {
1114
1252
  proven: false,
1115
1253
  reason: `${why}its scrollback ${named.length ? `names ${named.join(", ")}` : "names no checkout"}${flaw}`,
1116
- ...(misplaced ? { seen: "misplaced" } : {}),
1254
+ ...(ceilingCut ? { seen: "cut" } : misplaced ? { seen: "misplaced" } : {}),
1117
1255
  };
1118
1256
  };
1119
1257
  try {
1120
- const first = await page();
1258
+ const first = await page((read) => { tail = read; });
1121
1259
  if (first === undefined)
1122
1260
  return { proven: false, reason: "the ceiling cut off the proof read before any scrollback was read", seen: "unread" };
1123
1261
  const anchor = first.term;
1124
- tail = first.text;
1125
1262
  let cursor = str(anchor.oldestCursor);
1126
1263
  // why paging ended with scrollback still unread. Every exit is one of: complete (an anchor that
1127
1264
  // is the whole scrollback, or a page whose metadata says it is the last), whole frames found, a
1128
1265
  // page whose completeness is unknown (`unknown`, classifyProofPage), or one of these.
1266
+ // `spent`: a whole page landed exactly AT the ceiling, so no budget is left for the next read — paging
1267
+ // ended with scrollback unread, but no read was cut.
1129
1268
  let stop;
1130
1269
  if (cursor === undefined) {
1131
- text = tail;
1270
+ pages = tail; // the anchor is the whole read: the same rows, read once
1132
1271
  // A cursorless anchor that says it holds less than the scrollback leaves the rest unreadable.
1133
1272
  if (first.kind === "partial")
1134
1273
  stop = "stalled";
@@ -1138,14 +1277,31 @@ export class OrcaDriver {
1138
1277
  stop = "exhausted";
1139
1278
  break;
1140
1279
  } // more scrollback than the bound: unread, not absent
1141
- const got = late ? undefined : await page(cursor);
1280
+ // v2.6.8 T1: lateness ends paging but never CUTS a read — a page returned whole, whether it landed
1281
+ // past the ceiling (`late`) or exactly at it (`spent`: the next read is never issued), keeps its rows
1282
+ // (an unterminated or malformed marker included) as placement evidence; only a read the ceiling
1283
+ // actually INTERRUPTED below is `cut` (described's typed-timeout exemption).
1284
+ if (late)
1285
+ break;
1286
+ if (deadline !== undefined && this.time.now() >= deadline) {
1287
+ stop = "spent";
1288
+ break;
1289
+ }
1290
+ const got = await page((read) => {
1291
+ // Pages join by POSITION, never by bytes: a page that does not start where the rows before it end — a
1292
+ // gap, an overlap or an unknown position — is set apart by UNREADABLE_ROW, so no frame bridges rows
1293
+ // it never read beside.
1294
+ const contiguous = n === 0 || (pagedEnd !== undefined && read.at === pagedEnd);
1295
+ pages = { rows: [...pages.rows, ...(contiguous ? [] : [UNREADABLE_ROW]), ...read.rows],
1296
+ ...(n === 0 ? { at: read.at } : contiguous && pages.at !== undefined ? { at: pages.at } : {}) };
1297
+ pagedEnd = read.at === undefined ? undefined : read.at + read.rows.length;
1298
+ }, cursor);
1142
1299
  if (got === undefined) {
1143
1300
  stop = "cut";
1144
1301
  break;
1145
- }
1302
+ } // issued with budget left, interrupted by the ceiling
1146
1303
  const term = got.term;
1147
- text += `${got.text}\n`;
1148
- const frames = checkoutFrames(text);
1304
+ const frames = checkoutFrames(text());
1149
1305
  if (frames.complete.length > 0 && !frames.incomplete)
1150
1306
  break; // whole frames, nothing dangling
1151
1307
  if (got.kind === "complete")
@@ -1170,20 +1326,25 @@ export class OrcaDriver {
1170
1326
  }
1171
1327
  cursor = next;
1172
1328
  }
1173
- const frames = checkoutFrames(text);
1174
1329
  // Reconcile has no task-checkout path after a daemon restart, but the proof itself remains
1175
1330
  // an ownership record: exactly one complete, unambiguous checkout frame can only have been
1176
1331
  // written by tickmarkr's create command. Slot recovery additionally requires its exact path.
1177
- // A stalled read proves only what it read, as before; a cut or exhausted one proves nothing.
1178
- const proven = stop !== "cut" && stop !== "exhausted" && (checkout === undefined
1179
- ? !frames.incomplete && frames.complete.length > 0 && new Set(frames.complete).size === 1
1180
- : provesCheckout(text, checkout));
1332
+ // A stalled read proves only what it read, as before; a cut, spent or exhausted one proves nothing.
1333
+ // v2.6.8 T1: proof is classified over EVERYTHING read — the pages and the anchor's tail, which
1334
+ // may hold rows paging stopped short of. A foreign, incomplete or malformed mark in either
1335
+ // (misplacedProof) contradicts the proof: refused, never accepted beside a matching frame.
1336
+ const own = checkout ?? checkoutFrames(text()).complete[0];
1337
+ const proven = (stop === undefined || stop === "stalled") && own !== undefined
1338
+ && provesCheckout(text(), own) && !misplacedProof(tailInContext(pages, tail), own);
1181
1339
  if (proven && !late)
1182
1340
  return { proven: true, reason: "" };
1183
1341
  const why = `${late ? "read past the ceiling, " : ""}${stop === "cut" ? "the ceiling cut off paging, "
1184
- : stop === "exhausted" ? `paging stopped at ${PROOF_PAGES} pages with scrollback unread, `
1185
- : stop === "stalled" ? "a limited read gave no next cursor with scrollback unread, " : ""}${unknown ? "a page carried missing, non-boolean or contradictory paging metadata, " : ""}`;
1186
- const read = described(why);
1342
+ : stop === "spent" ? "a page landed at the ceiling with scrollback unread, "
1343
+ : stop === "exhausted" ? `paging stopped at ${PROOF_PAGES} pages with scrollback unread, `
1344
+ : stop === "stalled" ? "a limited read gave no next cursor with scrollback unread, " : ""}${unknown ? "a page carried missing, non-boolean or contradictory paging metadata, " : ""}`;
1345
+ // Only a read the ceiling actually cut short leaves a frame unread; a page that landed late or at the
1346
+ // ceiling was still returned whole, so a prefix it ends in is a misprint read whole, never the typed timeout.
1347
+ const read = described(why, stop === "cut");
1187
1348
  // F: absent only when every page read said, explicitly and consistently, how complete it was.
1188
1349
  return { ...read, seen: read.seen ?? (stop || late || unknown ? "unread" : "absent") };
1189
1350
  }
@@ -1191,7 +1352,7 @@ export class OrcaDriver {
1191
1352
  // A read that failed midway keeps what was already read as evidence; what it could not read is
1192
1353
  // unread output — never an absent proof.
1193
1354
  const read = described(""), failed = error instanceof Error ? error.message : String(error);
1194
- return { ...read, reason: tail || text ? `${read.reason}; then a read failed — ${failed}` : failed, seen: read.seen ?? "unread" };
1355
+ return { ...read, reason: tail.rows.length + pages.rows.length > 0 ? `${read.reason}; then a read failed — ${failed}` : failed, seen: read.seen ?? "unread" };
1195
1356
  }
1196
1357
  }
1197
1358
  async recover(family, st, newRuntimeId, raw, opts) {
@@ -125,6 +125,9 @@ export interface BaselineCommand {
125
125
  /** VL-1: every per-file duration the runner named, so a later per-file hang budget (test-manifest.ts)
126
126
  * can be derived per file rather than from one slowest-file number; null when unavailable. */
127
127
  fileDurations?: BaselineFileDuration[] | null;
128
+ /** v2.6.7 close: set when fileTiming classified each duration's outcome (`failed` on a red file). A red capture
129
+ * without it (an older binary, a cached or hand-supplied baseline) never says which files were red. */
130
+ fileOutcomes?: true;
128
131
  /** The ceiling that measurement implies, persisted so every later battery uses the same number. */
129
132
  ceilingMs?: number;
130
133
  /**
@@ -145,6 +148,8 @@ export interface BaselineCommand {
145
148
  export interface BaselineFileDuration {
146
149
  file: string;
147
150
  durationMs: number;
151
+ /** The file was RED in this capture (× or ❯): its duration ended early and is no measure of its green runtime. */
152
+ failed?: true;
148
153
  }
149
154
  /**
150
155
  * OBS-1123: WHICH capture a forgiveness rests on — the commit it measured and when it was published.
@@ -175,6 +180,10 @@ export type FailureClassification = "regression" | "infra";
175
180
  * unreadable-runner case the existing fail-closed path already owns.
176
181
  */
177
182
  export declare function classifyFailureOutput(output: string): FailureClassification | undefined;
183
+ /** The per-file timings a consumer may read as green runtimes. A red capture that never classified its files
184
+ * (no fileOutcomes) marks every file failed, so each is budgeted and costed as untimed — never as the early-ending
185
+ * time of a run that may have been red for it. */
186
+ export declare function classifiedFileDurations(entry: BaselineCommand | undefined): BaselineFileDuration[] | null | undefined;
178
187
  export declare const UNRECOGNIZED_FAILURE = "<unrecognized failure output>";
179
188
  export declare function fingerprint(output: string): string[];
180
189
  /**
@@ -430,7 +430,7 @@ const normalizeLine = (l) => l.replace(/\d+/g, "#").replace(/\s+/g, " ").trim();
430
430
  // turbo-prefixed, but a test-name timing has no parenthesized file tally and therefore cannot enter
431
431
  // this measurement. These are observations of runner output, not a promise that every runner exposes
432
432
  // them — callers record null, never zero, when no line matches.
433
- const FILE_DURATION_RE = /^\s*(?:(?:[\w@./-]+:\s*)*)[✓✔×❯]\s+(?:\|[^|\r\n]+\|\s+)?(\S+)\s+\([^\r\n)]*\)\s+(\d+(?:\.\d+)?)\s*(ms|s|m)\b/;
433
+ const FILE_DURATION_RE = /^\s*(?:(?:[\w@./-]+:\s*)*)([✓✔×❯])\s+(?:\|[^|\r\n]+\|\s+)?(\S+)\s+\([^\r\n)]*\)\s+(\d+(?:\.\d+)?)\s*(ms|s|m)\b/;
434
434
  const durationUnitMs = (unit) => unit === "m" ? 60_000 : unit === "s" ? 1_000 : 1;
435
435
  function fileTiming(output, wallClockMs) {
436
436
  const files = [];
@@ -438,9 +438,10 @@ function fileTiming(output, wallClockMs) {
438
438
  const match = FILE_DURATION_RE.exec(line.replace(ANSI_RE, ""));
439
439
  if (!match)
440
440
  continue;
441
- const durationMs = Number(match[2]) * durationUnitMs(match[3]);
441
+ const durationMs = Number(match[3]) * durationUnitMs(match[4]);
442
+ // vitest marks a red file ❯ (suiteFail) and a red test × (taskFail)
442
443
  if (Number.isFinite(durationMs))
443
- files.push({ file: match[1], durationMs });
444
+ files.push({ file: match[2], durationMs, ...(match[1] === "×" || match[1] === "❯" ? { failed: true } : {}) });
444
445
  }
445
446
  if (!files.length)
446
447
  return { fileDurationSumMs: null, impliedParallelism: null, longestFile: null, fileDurations: null };
@@ -451,8 +452,18 @@ function fileTiming(output, wallClockMs) {
451
452
  impliedParallelism: wallClockMs > 0 ? fileDurationSumMs / wallClockMs : null,
452
453
  longestFile,
453
454
  fileDurations: files,
455
+ fileOutcomes: true,
454
456
  };
455
457
  }
458
+ /** The per-file timings a consumer may read as green runtimes. A red capture that never classified its files
459
+ * (no fileOutcomes) marks every file failed, so each is budgeted and costed as untimed — never as the early-ending
460
+ * time of a run that may have been red for it. */
461
+ export function classifiedFileDurations(entry) {
462
+ const files = entry?.fileDurations;
463
+ if (!Array.isArray(files) || entry?.fileOutcomes || entry?.exitCode === 0)
464
+ return files;
465
+ return files.map((f) => ({ ...f, failed: true }));
466
+ }
456
467
  // A failing command whose output holds no shape any runner here names. The marker is content-free and
457
468
  // constant: downstream consumers (tip verify journals fingerprint counts) still see that the command
458
469
  // failed, while no line of the output — narration, status strip, stack trace — can enter the set. Being
@@ -53,7 +53,7 @@ export function baselineIdentity(baseline) {
53
53
  return "none";
54
54
  // Measurements control execution budgets, not which failures the baseline forgives.
55
55
  const commands = Object.fromEntries(Object.entries(baseline.commands).map(([gate, entry]) => {
56
- const { durationMs: _duration, fileDurationSumMs: _sum, impliedParallelism: _parallelism, longestFile: _longest, fileDurations: _files, ceilingMs: _ceiling, ...evidence } = entry;
56
+ const { durationMs: _duration, fileDurationSumMs: _sum, impliedParallelism: _parallelism, longestFile: _longest, fileDurations: _files, fileOutcomes: _outcomes, ceilingMs: _ceiling, ...evidence } = entry;
57
57
  return [gate, evidence];
58
58
  }));
59
59
  return createHash("sha256").update(canonicalJson(commands)).digest("hex").slice(0, 16);
@@ -1,10 +1,10 @@
1
- import { shOk } from "../run/git.js";
1
+ import { shGitOk } from "../run/git.js";
2
2
  export async function evidenceGate(worktree, baseRef) {
3
- const commits = (await shOk(`git rev-list '${baseRef}..HEAD'`, worktree)).trim().split("\n").filter(Boolean);
3
+ const commits = (await shGitOk(`git rev-list '${baseRef}..HEAD'`, worktree)).trim().split("\n").filter(Boolean);
4
4
  if (!commits.length) {
5
5
  return { gate: "evidence", pass: false, details: "no commits — worker claimed work but committed nothing", commits: [] };
6
6
  }
7
- const stat = (await shOk(`git diff --stat '${baseRef}..HEAD'`, worktree)).trim();
7
+ const stat = (await shGitOk(`git diff --stat '${baseRef}..HEAD'`, worktree)).trim();
8
8
  if (!stat) {
9
9
  return { gate: "evidence", pass: false, details: "commits exist but the cumulative diff is empty", commits };
10
10
  }
@@ -148,4 +148,4 @@ export declare function renderPriorMaterials(priorMaterials: readonly Structured
148
148
  */
149
149
  export declare function reviewResponseExample(nonce: string): string;
150
150
  export declare function reviewResponseRequirement(nonce: string): string;
151
- export declare function reviewGate(task: Task, worktree: string, baseRef: string, author: Assignment, channels: BillingChannel[], adapters: WorkerAdapter[], cfg: TickmarkrConfig, via?: GateVia, excludeReviewers?: string[], artifactDir?: string, reviewHistory?: string[], demotedReviewers?: ReadonlySet<string>, carriedFindings?: readonly StructuredFinding[], priorReviewers?: readonly PriorReviewer[], carriedAuthors?: readonly string[], operatorContext?: string): Promise<GateResult>;
151
+ export declare function reviewGate(task: Task, worktree: string, baseRef: string, author: Assignment, channels: BillingChannel[], adapters: WorkerAdapter[], cfg: TickmarkrConfig, via?: GateVia, excludeReviewers?: string[], artifactDir?: string, reviewHistory?: string[], demotedReviewers?: ReadonlySet<string>, carriedFindings?: readonly StructuredFinding[], priorReviewers?: readonly PriorReviewer[], carriedAuthors?: readonly string[], _operatorContext?: string): Promise<GateResult>;