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.
- package/dist/adapters/claude-code.js +8 -2
- package/dist/adapters/codex-commit-check.d.ts +3 -2
- package/dist/adapters/codex-commit-check.js +37 -10
- package/dist/adapters/codex.js +10 -4
- package/dist/cli/commands/beat.d.ts +7 -2
- package/dist/cli/commands/beat.js +144 -77
- package/dist/cli/commands/fleet.js +66 -21
- package/dist/cli/commands/init.js +18 -7
- package/dist/cli/commands/plan.js +16 -18
- package/dist/cli/commands/verify.js +4 -2
- package/dist/cli/help.d.ts +1 -0
- package/dist/cli/help.js +14 -6
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/drivers/herdr.js +2 -2
- package/dist/drivers/orca.d.ts +12 -4
- package/dist/drivers/orca.js +200 -39
- package/dist/gates/baseline.d.ts +9 -0
- package/dist/gates/baseline.js +14 -3
- package/dist/gates/cache.js +1 -1
- package/dist/gates/evidence.js +3 -3
- package/dist/gates/review.d.ts +1 -1
- package/dist/gates/review.js +77 -13
- package/dist/gates/run-gates.d.ts +21 -6
- package/dist/gates/run-gates.js +439 -224
- package/dist/gates/test-manifest.d.ts +42 -0
- package/dist/gates/test-manifest.js +203 -118
- package/dist/run/beat-lifecycle.d.ts +239 -0
- package/dist/run/beat-lifecycle.js +1782 -0
- package/dist/run/daemon.d.ts +4 -0
- package/dist/run/daemon.js +129 -40
- package/dist/run/git-trust.d.ts +102 -0
- package/dist/run/git-trust.js +334 -0
- package/dist/run/git.d.ts +24 -3
- package/dist/run/git.js +169 -30
- package/dist/run/journal.js +15 -3
- package/dist/run/lease.d.ts +23 -0
- package/dist/run/lease.js +232 -38
- package/dist/run/merge.js +4 -4
- package/dist/run/stall.js +20 -1
- package/dist/run/supervision.d.ts +8 -0
- package/dist/run/supervision.js +24 -4
- package/dist/run/verification-job.d.ts +53 -0
- package/dist/run/verification-job.js +159 -0
- package/dist/tui/ink/fleet-app.d.ts +16 -2
- package/dist/tui/ink/fleet-app.js +265 -141
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +80 -27
- package/skills/tickmarkr-loop/SKILL.md +87 -27
- package/skills/tickmarkr-overseer/SKILL.md +125 -47
- package/skills/tickmarkr-overseer/scripts/watch-journal.sh +9 -7
package/dist/drivers/orca.js
CHANGED
|
@@ -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
|
-
|
|
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 >
|
|
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
|
-
|
|
261
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
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
|
|
1112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
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 === "
|
|
1185
|
-
: stop === "
|
|
1186
|
-
|
|
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
|
|
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) {
|
package/dist/gates/baseline.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/gates/baseline.js
CHANGED
|
@@ -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[
|
|
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[
|
|
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
|
package/dist/gates/cache.js
CHANGED
|
@@ -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);
|
package/dist/gates/evidence.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { shGitOk } from "../run/git.js";
|
|
2
2
|
export async function evidenceGate(worktree, baseRef) {
|
|
3
|
-
const commits = (await
|
|
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
|
|
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
|
}
|
package/dist/gates/review.d.ts
CHANGED
|
@@ -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[],
|
|
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>;
|