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.
- package/dist/adapters/catalog-remote.d.ts +23 -0
- package/dist/adapters/catalog-remote.js +38 -0
- package/dist/adapters/claude-code.d.ts +1 -1
- package/dist/adapters/claude-code.js +4 -1
- package/dist/adapters/codex-commit-check.d.ts +25 -0
- package/dist/adapters/codex-commit-check.js +108 -0
- package/dist/adapters/model-discovery-check.d.ts +33 -0
- package/dist/adapters/model-discovery-check.js +123 -0
- package/dist/adapters/model-lints.d.ts +1 -1
- package/dist/adapters/model-lints.js +4 -0
- package/dist/adapters/prompt.js +3 -2
- package/dist/adapters/types.d.ts +7 -0
- package/dist/cli/commands/approve.js +10 -4
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +14 -2
- package/dist/cli/commands/fleet.d.ts +10 -1
- package/dist/cli/commands/fleet.js +55 -38
- package/dist/cli/commands/init.js +5 -1
- package/dist/cli/commands/plan.js +65 -4
- package/dist/cli/commands/verify.d.ts +1 -1
- package/dist/cli/commands/verify.js +28 -6
- package/dist/config/fleet-overlay.d.ts +2 -1
- package/dist/config/fleet-overlay.js +77 -4
- package/dist/drivers/orca.d.ts +19 -1
- package/dist/drivers/orca.js +161 -31
- package/dist/gates/cache.d.ts +4 -0
- package/dist/gates/cache.js +10 -2
- package/dist/gates/llm.d.ts +4 -1
- package/dist/gates/llm.js +6 -1
- package/dist/gates/review.js +8 -3
- package/dist/gates/run-gates.d.ts +3 -2
- package/dist/gates/run-gates.js +91 -50
- package/dist/run/daemon.js +150 -19
- package/dist/run/git.js +9 -0
- package/dist/run/journal.d.ts +6 -2
- package/dist/run/journal.js +22 -7
- package/dist/run/launchservices-check.d.ts +56 -0
- package/dist/run/launchservices-check.js +117 -0
- package/dist/run/operator-state.d.ts +22 -2
- package/dist/run/operator-state.js +20 -3
- package/dist/run/title-environment.d.ts +45 -0
- package/dist/run/title-environment.js +127 -0
- package/dist/run/title-preload.cjs +38 -0
- package/dist/run/title-preload.d.cts +1 -0
- package/dist/tui/cockpit/board.js +12 -6
- package/dist/tui/cockpit/keys.d.ts +8 -0
- package/dist/tui/cockpit/keys.js +4 -1
- package/dist/tui/cockpit/layout.d.ts +2 -2
- package/dist/tui/cockpit/layout.js +2 -2
- package/dist/tui/cockpit/live-runtime.js +46 -20
- package/dist/tui/cockpit/live-store.d.ts +24 -1
- package/dist/tui/cockpit/live-store.js +99 -8
- package/dist/tui/cockpit/log-view.d.ts +28 -0
- package/dist/tui/cockpit/log-view.js +60 -0
- package/dist/tui/cockpit/run-view.js +4 -0
- package/dist/tui/cockpit/shell.d.ts +2 -1
- package/dist/tui/cockpit/shell.js +6 -4
- package/dist/tui/ink/fleet-app.d.ts +12 -6
- package/dist/tui/ink/fleet-app.js +17 -11
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +32 -2
- package/skills/tickmarkr-loop/SKILL.md +46 -2
- package/skills/tickmarkr-overseer/SKILL.md +83 -8
package/dist/drivers/orca.js
CHANGED
|
@@ -76,6 +76,7 @@ export class OrcaError extends Error {
|
|
|
76
76
|
raw;
|
|
77
77
|
code;
|
|
78
78
|
runtimeId;
|
|
79
|
+
launchCause;
|
|
79
80
|
constructor(family, reason, raw, opts = {}) {
|
|
80
81
|
super(`orca ${family} failed — ${reason}; raw response:\n${raw}`);
|
|
81
82
|
this.family = family;
|
|
@@ -84,14 +85,15 @@ export class OrcaError extends Error {
|
|
|
84
85
|
this.name = "OrcaError";
|
|
85
86
|
this.code = opts.code;
|
|
86
87
|
this.runtimeId = opts.runtimeId;
|
|
88
|
+
this.launchCause = opts.launchCause;
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
/** The slot cannot be addressed: dead/unknown terminal record, or a handle that cannot be recovered
|
|
90
92
|
* to exactly one owned terminal in the slot's own worktree. Never a silent false or empty string. */
|
|
91
93
|
export class OrcaUnavailableError extends OrcaError {
|
|
92
94
|
terminalStatus;
|
|
93
|
-
constructor(family, reason, raw, terminalStatus) {
|
|
94
|
-
super(family, reason, raw, { code: "terminal_unavailable" });
|
|
95
|
+
constructor(family, reason, raw, terminalStatus, launchCause) {
|
|
96
|
+
super(family, reason, raw, { code: "terminal_unavailable", launchCause });
|
|
95
97
|
this.terminalStatus = terminalStatus;
|
|
96
98
|
this.name = "OrcaUnavailableError";
|
|
97
99
|
}
|
|
@@ -209,7 +211,26 @@ export const CHECKOUT_MARK = "TICKMARKR_CHECKOUT";
|
|
|
209
211
|
// never decode to this one; `A` versus `A B`, `…--T1` versus `…--T10` are different frames.
|
|
210
212
|
const CHECKOUT_FRAME_RE = /TICKMARKR_CHECKOUT (\d+):([0-9a-f]*)(;?)/g;
|
|
211
213
|
const PROOF_PAGES = 16; // pages read from the oldest cursor before the proof is declared absent
|
|
212
|
-
|
|
214
|
+
// E1: the ceiling for the wrapper shell's startup proof. A loaded host can print a review seat's proof
|
|
215
|
+
// long after the old 2 s window closed (the window a healthy seat was retired on); a proof first
|
|
216
|
+
// arriving at 10 s is accepted, and 20 s still bounds a terminal that never proves anything.
|
|
217
|
+
const CHECKOUT_PROOF_TIMEOUT_MS = 20_000;
|
|
218
|
+
// E1: the printed mark's first bytes past the printf split. The typed command never carries them (the
|
|
219
|
+
// mark is split at its underscore; exit trailers type `TICKMARKR_''EXIT`) and no other tickmarkr name
|
|
220
|
+
// starts `TICKMARKR_C`, so any occurrence is proof bytes — whole, cut or with a header the frame
|
|
221
|
+
// grammar cannot parse — and never an absent proof.
|
|
222
|
+
const MARK_TRACE = CHECKOUT_MARK.slice(0, CHECKOUT_MARK.indexOf("_") + 2);
|
|
223
|
+
/** E1: placement evidence in text actually read — a complete frame naming another checkout, a cut
|
|
224
|
+
* frame, or a mark the frame grammar cannot parse (more mark traces than frames). It denies the
|
|
225
|
+
* timeout exemption: only a terminal that printed no mark at all was merely slow.
|
|
226
|
+
* ponytail: a wrapped frame split across the last page read (the ceiling or PROOF_PAGES stopped
|
|
227
|
+
* paging) reads as cut — conservative (no exemption); it needs the proof line on that boundary. */
|
|
228
|
+
function misplacedProof(text, checkout) {
|
|
229
|
+
const { complete, incomplete } = checkoutFrames(text);
|
|
230
|
+
const joined = joinWrapped(text);
|
|
231
|
+
return incomplete || complete.some((c) => c !== checkout)
|
|
232
|
+
|| joined.split(MARK_TRACE).length - 1 > [...joined.matchAll(CHECKOUT_FRAME_RE)].length;
|
|
233
|
+
}
|
|
213
234
|
/** The exact bytes the create command prints as its first line. */
|
|
214
235
|
export function checkoutProofLine(checkout) {
|
|
215
236
|
const bytes = Buffer.from(checkout, "utf8");
|
|
@@ -752,9 +773,9 @@ export class OrcaDriver {
|
|
|
752
773
|
throw new OrcaError("show", `unknown slot ${slot.id}`, "");
|
|
753
774
|
return s;
|
|
754
775
|
}
|
|
755
|
-
latched(family, st, reason, raw) {
|
|
776
|
+
latched(family, st, reason, raw, launchCause) {
|
|
756
777
|
st.unavailable = reason;
|
|
757
|
-
return new OrcaUnavailableError(family, reason, raw);
|
|
778
|
+
return new OrcaUnavailableError(family, reason, raw, undefined, launchCause);
|
|
758
779
|
}
|
|
759
780
|
assertAvailable(family, st) {
|
|
760
781
|
if (st.unavailable)
|
|
@@ -836,20 +857,39 @@ export class OrcaDriver {
|
|
|
836
857
|
catch { /* already gone, or no longer provably ours — never a blind retry */ }
|
|
837
858
|
throw this.latched("create", st, `create receipt bound to ${worktree ?? "no worktree"}, not the tracked ${tracked} enclosing ${st.cwd}`, env.raw);
|
|
838
859
|
}
|
|
839
|
-
// A receipt only proves placement on the enclosing tracked checkout. Wait
|
|
840
|
-
// startup command's proof before run() can resolve and the daemon can count a worker launch.
|
|
860
|
+
// A receipt only proves placement on the enclosing tracked checkout. Wait, up to the ceiling, for
|
|
861
|
+
// the startup command's proof before run() can resolve and the daemon can count a worker launch.
|
|
862
|
+
// Polls and reads are bounded by what is left of it, and proof read past it proves nothing.
|
|
863
|
+
// One absolute deadline, rechecked after every sleep: no read starts once it has passed.
|
|
841
864
|
const deadline = this.time.now() + CHECKOUT_PROOF_TIMEOUT_MS;
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
865
|
+
// Evidence is kept across polls and never downgraded: once a poll read a misplaced mark or left
|
|
866
|
+
// output unread, no later poll — absent, cut or otherwise — turns the window back into absence.
|
|
867
|
+
let misplaced, unread;
|
|
868
|
+
let proof = await this.checkoutProven(handle, st.cwd, this.cliCwd(st), env.runtimeId, deadline);
|
|
869
|
+
for (;;) {
|
|
870
|
+
if (proof.proven)
|
|
871
|
+
break;
|
|
872
|
+
if (proof.seen === "misplaced")
|
|
873
|
+
misplaced = proof;
|
|
874
|
+
else if (proof.seen === "unread")
|
|
875
|
+
unread = proof;
|
|
876
|
+
const left = deadline - this.time.now();
|
|
877
|
+
if (left <= 0)
|
|
878
|
+
break;
|
|
879
|
+
await this.time.sleep(Math.min(POLL_MS, left));
|
|
880
|
+
if (this.time.now() >= deadline)
|
|
881
|
+
break;
|
|
882
|
+
proof = await this.checkoutProven(handle, st.cwd, this.cliCwd(st), env.runtimeId, deadline);
|
|
846
883
|
}
|
|
847
884
|
if (!proof.proven) {
|
|
848
885
|
try {
|
|
849
886
|
await this.closeTerminal({ ...st, handle, runtimeId: env.runtimeId });
|
|
850
887
|
}
|
|
851
888
|
catch { /* best effort under the answering runtime; the latch prevents another launch */ }
|
|
852
|
-
|
|
889
|
+
// The typed timeout only when EVERY poll read its whole scrollback inside the ceiling and found no
|
|
890
|
+
// trace of the mark; one misplaced or unread poll anywhere in the window denies it.
|
|
891
|
+
const evidence = misplaced ?? unread ?? proof;
|
|
892
|
+
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);
|
|
853
893
|
}
|
|
854
894
|
st.handle = handle;
|
|
855
895
|
// The handle is bound to the runtime identity that ANSWERED its create.
|
|
@@ -972,12 +1012,38 @@ export class OrcaDriver {
|
|
|
972
1012
|
* that supplied the ownership listing — else another terminal's or another runtime's bytes were
|
|
973
1013
|
* answered and nothing is proven. Proven means provesCheckout: exact canonical full-path equality
|
|
974
1014
|
* of a complete frame, no other checkout named, no incomplete frame.
|
|
1015
|
+
* E1: with a `deadline` (create's window) there is ONE absolute deadline: a read is issued only while
|
|
1016
|
+
* budget is left and is handed exactly that budget, and a page that still lands past it is late —
|
|
1017
|
+
* nothing late proves the checkout. `seen` is what the scrollback showed, for the typed timeout:
|
|
1018
|
+
* `misplaced` — a frame naming another checkout, a cut frame or an unparseable mark in anything read
|
|
1019
|
+
* (the anchor's tail and every page included, even when paging stopped or a later read threw);
|
|
1020
|
+
* `absent` — ONLY when every page down to the start of the scrollback was read before the ceiling
|
|
1021
|
+
* and none carries a trace of the mark; `unread` — every other exit: PROOF_PAGES reached, a limited
|
|
1022
|
+
* read with no (or the same) next cursor or a truncated anchor without one, a read that threw, a read
|
|
1023
|
+
* the ceiling cut off or never let start (the anchor included), or a page that landed late (even one
|
|
1024
|
+
* carrying the right proof), a malformed page (its tail, cursors or paging flags), or a blind stream
|
|
1025
|
+
* page (isBlindStreamPage). Unread output is unknown — never an empty page, never absence.
|
|
1026
|
+
* Without a deadline (reconcile, recovery) only `proven` is consumed.
|
|
975
1027
|
*/
|
|
976
|
-
async checkoutProven(handle, checkout, from, runtimeId) {
|
|
1028
|
+
async checkoutProven(handle, checkout, from, runtimeId, deadline) {
|
|
1029
|
+
let late = false;
|
|
977
1030
|
const page = async (cursor) => {
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
|
|
1031
|
+
const budget = deadline === undefined ? undefined : deadline - this.time.now();
|
|
1032
|
+
if (budget !== undefined && budget <= 0)
|
|
1033
|
+
return undefined; // no budget left: the read is never issued
|
|
1034
|
+
let env;
|
|
1035
|
+
try {
|
|
1036
|
+
env = await this.call("read", [
|
|
1037
|
+
"terminal", "read", "--terminal", handle, ...(cursor === undefined ? [] : ["--cursor", cursor]), "--limit", String(this.pageLines),
|
|
1038
|
+
], from, budget);
|
|
1039
|
+
}
|
|
1040
|
+
catch (error) {
|
|
1041
|
+
if (deadline === undefined || this.time.now() < deadline)
|
|
1042
|
+
throw error;
|
|
1043
|
+
return undefined; // the read the ceiling cut off: unread
|
|
1044
|
+
}
|
|
1045
|
+
if (deadline !== undefined && this.time.now() > deadline)
|
|
1046
|
+
late = true;
|
|
981
1047
|
if (env.runtimeId !== runtimeId) {
|
|
982
1048
|
throw new OrcaError("read", `proof page answered by runtime ${env.runtimeId}, not the listing's ${runtimeId}`, env.raw);
|
|
983
1049
|
}
|
|
@@ -985,39 +1051,103 @@ export class OrcaDriver {
|
|
|
985
1051
|
if (str(term.handle) !== handle) {
|
|
986
1052
|
throw new OrcaError("read", `proof page names terminal ${str(term.handle) ?? "none"}, not the candidate ${handle}`, env.raw);
|
|
987
1053
|
}
|
|
988
|
-
|
|
1054
|
+
// E1: only a well-formed page is read text. A tail that is missing, not an array or carries a
|
|
1055
|
+
// non-string row (tailText), a cursor that is not a non-empty string, or a paging flag that is not
|
|
1056
|
+
// a boolean is a malformed page: it throws, so it is unread output — never filtered to an empty
|
|
1057
|
+
// page that would read as an absent proof.
|
|
1058
|
+
for (const key of ["oldestCursor", "nextCursor"]) {
|
|
1059
|
+
if (term[key] !== undefined && str(term[key]) === undefined)
|
|
1060
|
+
throw new OrcaError("read", `proof page ${key} is not a cursor`, env.raw);
|
|
1061
|
+
}
|
|
1062
|
+
for (const key of ["limited", "truncated"]) {
|
|
1063
|
+
if (term[key] !== undefined && typeof term[key] !== "boolean")
|
|
1064
|
+
throw new OrcaError("read", `proof page ${key} is not a boolean`, env.raw);
|
|
1065
|
+
}
|
|
1066
|
+
// E1: the OBS-1011 add.1 blind stream (exited, empty tail, zero lines returned) is the shape a
|
|
1067
|
+
// live terminal's stream answers while its screen still paints — and a dead terminal's too. The
|
|
1068
|
+
// stream alone cannot tell them apart, so its empty tail is unread scrollback, never absence.
|
|
1069
|
+
if (isBlindStreamPage(term))
|
|
1070
|
+
throw new OrcaError("read", "proof page is a blind stream page (status exited, empty tail): its scrollback is unread", env.raw);
|
|
1071
|
+
return { term, text: this.tailText("read", term, env.raw) };
|
|
1072
|
+
};
|
|
1073
|
+
// Every validated read is evidence, whatever stops the next one: the anchor's tail and the pages
|
|
1074
|
+
// read from the oldest cursor are inspected even when paging hits PROOF_PAGES or a read throws.
|
|
1075
|
+
let tail = "", text = "";
|
|
1076
|
+
const described = (why) => {
|
|
1077
|
+
const misplaced = checkout !== undefined && [text, tail].some((t) => misplacedProof(t, checkout));
|
|
1078
|
+
const shown = [text, tail].map(checkoutFrames);
|
|
1079
|
+
const named = [...new Set(shown.flatMap((f) => f.complete))];
|
|
1080
|
+
const flaw = shown.some((f) => f.incomplete) ? " and carries an incomplete proof frame"
|
|
1081
|
+
: misplaced && named.length === 0 ? " and carries a malformed proof marker" : "";
|
|
1082
|
+
return {
|
|
1083
|
+
proven: false,
|
|
1084
|
+
reason: `${why}its scrollback ${named.length ? `names ${named.join(", ")}` : "names no checkout"}${flaw}`,
|
|
1085
|
+
...(misplaced ? { seen: "misplaced" } : {}),
|
|
1086
|
+
};
|
|
989
1087
|
};
|
|
990
|
-
const lines = (term) => Array.isArray(term.tail) ? term.tail.filter((l) => typeof l === "string") : [];
|
|
991
1088
|
try {
|
|
992
|
-
const
|
|
1089
|
+
const first = await page();
|
|
1090
|
+
if (first === undefined)
|
|
1091
|
+
return { proven: false, reason: "the ceiling cut off the proof read before any scrollback was read", seen: "unread" };
|
|
1092
|
+
const anchor = first.term;
|
|
1093
|
+
tail = first.text;
|
|
993
1094
|
let cursor = str(anchor.oldestCursor);
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1095
|
+
// why paging ended with scrollback still unread. Every exit is one of: complete (an anchor that
|
|
1096
|
+
// is the whole scrollback, or a page that is not limited), whole frames found, or one of these.
|
|
1097
|
+
let stop;
|
|
1098
|
+
if (cursor === undefined) {
|
|
1099
|
+
text = tail;
|
|
1100
|
+
// A cursorless anchor that says it holds less than the scrollback leaves the rest unreadable.
|
|
1101
|
+
if (anchor.truncated === true || anchor.limited === true)
|
|
1102
|
+
stop = "stalled";
|
|
1103
|
+
}
|
|
1104
|
+
for (let n = 0; cursor !== undefined; n++) {
|
|
1105
|
+
if (n === PROOF_PAGES) {
|
|
1106
|
+
stop = "exhausted";
|
|
1107
|
+
break;
|
|
1108
|
+
} // more scrollback than the bound: unread, not absent
|
|
1109
|
+
const got = late ? undefined : await page(cursor);
|
|
1110
|
+
if (got === undefined) {
|
|
1111
|
+
stop = "cut";
|
|
1112
|
+
break;
|
|
1113
|
+
}
|
|
1114
|
+
const term = got.term;
|
|
1115
|
+
text += `${got.text}\n`;
|
|
998
1116
|
const frames = checkoutFrames(text);
|
|
999
1117
|
if (frames.complete.length > 0 && !frames.incomplete)
|
|
1000
1118
|
break; // whole frames, nothing dangling
|
|
1119
|
+
if (term.limited !== true)
|
|
1120
|
+
break; // the last page: the scrollback was read to its end
|
|
1001
1121
|
const next = str(term.nextCursor);
|
|
1002
|
-
|
|
1122
|
+
// A limited page with no next cursor, or the same one, says more scrollback exists that no
|
|
1123
|
+
// read can reach: unread, never absent.
|
|
1124
|
+
if (next === undefined || next === cursor) {
|
|
1125
|
+
stop = "stalled";
|
|
1003
1126
|
break;
|
|
1127
|
+
}
|
|
1004
1128
|
cursor = next;
|
|
1005
1129
|
}
|
|
1006
1130
|
const frames = checkoutFrames(text);
|
|
1007
1131
|
// Reconcile has no task-checkout path after a daemon restart, but the proof itself remains
|
|
1008
1132
|
// an ownership record: exactly one complete, unambiguous checkout frame can only have been
|
|
1009
1133
|
// written by tickmarkr's create command. Slot recovery additionally requires its exact path.
|
|
1010
|
-
|
|
1134
|
+
// A stalled read proves only what it read, as before; a cut or exhausted one proves nothing.
|
|
1135
|
+
const proven = stop !== "cut" && stop !== "exhausted" && (checkout === undefined
|
|
1011
1136
|
? !frames.incomplete && frames.complete.length > 0 && new Set(frames.complete).size === 1
|
|
1012
|
-
: provesCheckout(text, checkout))
|
|
1137
|
+
: provesCheckout(text, checkout));
|
|
1138
|
+
if (proven && !late)
|
|
1013
1139
|
return { proven: true, reason: "" };
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1140
|
+
const why = `${late ? "read past the ceiling, " : ""}${stop === "cut" ? "the ceiling cut off paging, "
|
|
1141
|
+
: stop === "exhausted" ? `paging stopped at ${PROOF_PAGES} pages with scrollback unread, `
|
|
1142
|
+
: stop === "stalled" ? "a limited read gave no next cursor with scrollback unread, " : ""}`;
|
|
1143
|
+
const read = described(why);
|
|
1144
|
+
return { ...read, seen: read.seen ?? (stop || late ? "unread" : "absent") };
|
|
1018
1145
|
}
|
|
1019
1146
|
catch (error) {
|
|
1020
|
-
|
|
1147
|
+
// A read that failed midway keeps what was already read as evidence; what it could not read is
|
|
1148
|
+
// unread output — never an absent proof.
|
|
1149
|
+
const read = described(""), failed = error instanceof Error ? error.message : String(error);
|
|
1150
|
+
return { ...read, reason: tail || text ? `${read.reason}; then a read failed — ${failed}` : failed, seen: read.seen ?? "unread" };
|
|
1021
1151
|
}
|
|
1022
1152
|
}
|
|
1023
1153
|
async recover(family, st, newRuntimeId, raw, opts) {
|
package/dist/gates/cache.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export declare function getWorktreeTree(worktree: string): Promise<string>;
|
|
|
13
13
|
* ponytail: a named list, not the whole env (pids and terminal vars would defeat every reuse); extend
|
|
14
14
|
* it when another variable is shown to change what a runner executes. */
|
|
15
15
|
export declare const RUNNER_ENV_KEYS: readonly ["PATH", "NODE_OPTIONS", "NODE_PATH", "NODE_ENV", "TZ"];
|
|
16
|
+
/** A2: NODE_OPTIONS is hashed as the child RECEIVES it (shell appends the Darwin title preload), plus the
|
|
17
|
+
* preload's content identity, so a replaced preload, the opt-out or a refused preflight never reuses a
|
|
18
|
+
* verdict or baseline measured under another mode. Native mode hashes the pre-A2 inputs less any inherited
|
|
19
|
+
* managed preload, which shell removes from that child too. */
|
|
16
20
|
export declare function runnerInputsHash(env?: NodeJS.ProcessEnv): string;
|
|
17
21
|
export interface GateEnvironmentInput {
|
|
18
22
|
nodeRuntime?: string;
|
package/dist/gates/cache.js
CHANGED
|
@@ -5,6 +5,7 @@ import { dirname, join, relative, resolve } from "node:path";
|
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { checkoutIncarnation, describeCapacity, inventoryDependencyLinks, resolvedCapacity, shGit, verificationProtocol } from "../run/git.js";
|
|
7
7
|
import { shq } from "../adapters/types.js";
|
|
8
|
+
import { titleEnvironment } from "../run/title-environment.js";
|
|
8
9
|
export const DEFAULT_VERDICT_CACHE_BOUND = 128;
|
|
9
10
|
let testCacheBound;
|
|
10
11
|
export function setVerdictCacheBoundForTests(bound) {
|
|
@@ -81,9 +82,16 @@ export async function getWorktreeTree(worktree) {
|
|
|
81
82
|
* ponytail: a named list, not the whole env (pids and terminal vars would defeat every reuse); extend
|
|
82
83
|
* it when another variable is shown to change what a runner executes. */
|
|
83
84
|
export const RUNNER_ENV_KEYS = ["PATH", "NODE_OPTIONS", "NODE_PATH", "NODE_ENV", "TZ"];
|
|
85
|
+
/** A2: NODE_OPTIONS is hashed as the child RECEIVES it (shell appends the Darwin title preload), plus the
|
|
86
|
+
* preload's content identity, so a replaced preload, the opt-out or a refused preflight never reuses a
|
|
87
|
+
* verdict or baseline measured under another mode. Native mode hashes the pre-A2 inputs less any inherited
|
|
88
|
+
* managed preload, which shell removes from that child too. */
|
|
84
89
|
export function runnerInputsHash(env = process.env) {
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
const title = titleEnvironment(env);
|
|
91
|
+
const inputs = Object.fromEntries(RUNNER_ENV_KEYS.map((k) => [k, (k === "NODE_OPTIONS" ? title.nodeOptions : env[k]) ?? null]));
|
|
92
|
+
if (title.mode === "preload")
|
|
93
|
+
inputs.titlePreload = title.identity;
|
|
94
|
+
return createHash("sha256").update(canonicalJson(inputs)).digest("hex").slice(0, 16);
|
|
87
95
|
}
|
|
88
96
|
export function environmentFingerprint(env) {
|
|
89
97
|
const nodeRuntime = env.nodeRuntime ?? process.version;
|
package/dist/gates/llm.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type WorkerAdapter } from "../adapters/types.js";
|
|
2
2
|
import type { Effort } from "../graph/schema.js";
|
|
3
3
|
import { type ExecutorDriver, type Slot } from "../drivers/types.js";
|
|
4
|
+
import type { SeatLaunchCause } from "../drivers/orca.js";
|
|
4
5
|
export declare const GATE_PANE_SEP = " \u00B7 ";
|
|
5
6
|
export declare const COMPLETION_FAKING_CHECKLIST = "## Completion-faking checklist\nHunt for these concrete completion-faking shortcuts before ruling on any criterion:\n- hardcoded-result: output or fixture hardcoded to satisfy the stated criterion instead of real logic\n- test-weakening: tests skipped, deleted, or assertions loosened until failing behavior looks green\n- vacuous-assertion: a test that cannot fail (asserts a constant, asserts its own setup, no assertion)\n- fixture-overfit: implementation narrowed to the exact test inputs rather than the described behavior\n- echo-not-implement: criterion text echoed in names, comments, or strings without the behavior itself\n- stub-left-behind: TODO, throw, or no-op stub where the real implementation should be\n- error-swallowing: catch or fallback that hides failures instead of handling them\n- self-mocking: the code under test mocked or faked so the test exercises the mock\n- check-bypass: lint, type, or CI checks disabled, relaxed, or excluded to get green\n- rename-as-work: code moved or renamed and presented as the requested change\n- scope-padding: unrelated edits padding the diff while the criterion's behavior is untouched\nWhen a criterion fails, the verdict MUST name which shortcut above it matches, or state that none does.";
|
|
6
7
|
/** Fable F3: per-call nonce echoed in verdict JSON and gate exit markers. */
|
|
@@ -70,9 +71,11 @@ export interface LlmRunResult {
|
|
|
70
71
|
export declare const PROMPT_GLYPHS: readonly ["➜", "❯", "$", "%", ">>", ">"];
|
|
71
72
|
export declare function reviewSeatOutput(raw: string, nonce: string, adapterBannerRows?: readonly string[]): string;
|
|
72
73
|
/** OBS-1168(b): a judge/review seat whose pane could not be created or launched. Typed so the gates can
|
|
73
|
-
* contain it as seat recovery (another seat) or an infra park — never as a worker failure.
|
|
74
|
+
* contain it as seat recovery (another seat) or an infra park — never as a worker failure. E1: the
|
|
75
|
+
* driver's typed launch cause survives the wrap as a field; the message alone is prose. */
|
|
74
76
|
export declare class SeatLaunchError extends Error {
|
|
75
77
|
readonly seat: string;
|
|
78
|
+
readonly launchCause?: SeatLaunchCause;
|
|
76
79
|
constructor(seat: string, cause: unknown);
|
|
77
80
|
}
|
|
78
81
|
export declare const REVIEW_FIRST_LIVENESS_MS = 30000;
|
package/dist/gates/llm.js
CHANGED
|
@@ -303,13 +303,18 @@ export function reviewSeatOutput(raw, nonce, adapterBannerRows = []) {
|
|
|
303
303
|
return trailer ? seat.slice(0, trailer.index) : seat;
|
|
304
304
|
}
|
|
305
305
|
/** OBS-1168(b): a judge/review seat whose pane could not be created or launched. Typed so the gates can
|
|
306
|
-
* contain it as seat recovery (another seat) or an infra park — never as a worker failure.
|
|
306
|
+
* contain it as seat recovery (another seat) or an infra park — never as a worker failure. E1: the
|
|
307
|
+
* driver's typed launch cause survives the wrap as a field; the message alone is prose. */
|
|
307
308
|
export class SeatLaunchError extends Error {
|
|
308
309
|
seat;
|
|
310
|
+
launchCause;
|
|
309
311
|
constructor(seat, cause) {
|
|
310
312
|
super(`seat ${seat} failed to launch: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
311
313
|
this.seat = seat;
|
|
312
314
|
this.name = "SeatLaunchError";
|
|
315
|
+
const launchCause = cause?.launchCause;
|
|
316
|
+
if (launchCause === "checkout-proof-timeout")
|
|
317
|
+
this.launchCause = launchCause;
|
|
313
318
|
}
|
|
314
319
|
}
|
|
315
320
|
export const REVIEW_FIRST_LIVENESS_MS = 30_000;
|
package/dist/gates/review.js
CHANGED
|
@@ -367,7 +367,9 @@ function daemonRepoRoot(worktree, artifactDir) {
|
|
|
367
367
|
export function renderPriorMaterials(priorMaterials) {
|
|
368
368
|
const fingerprints = priorMaterials.map((finding, i) => {
|
|
369
369
|
const observed = observedReviewFingerprints(finding);
|
|
370
|
-
|
|
370
|
+
// E2 (T4 D-766): every finding is headed. A one-spelling finding left unheaded read as one more
|
|
371
|
+
// spelling of the multi-spelling finding above it, so a faithful seat closed F1 alone.
|
|
372
|
+
const heading = observed.length > 1 ? `Finding ${i + 1} (choose one observed spelling):\n` : `Finding ${i + 1}:\n`;
|
|
371
373
|
return heading + observed.map((id) => `Fingerprint: ${id}`).join("\n");
|
|
372
374
|
}).join("\n");
|
|
373
375
|
return `## Prior materials this attempt must close
|
|
@@ -642,11 +644,14 @@ ${responseRequirement}
|
|
|
642
644
|
throw error;
|
|
643
645
|
// OBS-1168(b): the seat never launched, so there is no verdict and nothing about the WORK. A typed
|
|
644
646
|
// no-verdict re-routes to another seat in run-gates; an exhausted pool is an infra park.
|
|
647
|
+
// E1: the cause stays seat-launch-failed; a typed launchCause rides beside it, never replaces it.
|
|
648
|
+
const launch = error.launchCause ? `; launchCause: ${error.launchCause}` : "";
|
|
645
649
|
return { gate: "review", pass: false,
|
|
646
|
-
details: `review dispatch failed — ${error.message} (reviewer ${reviewer.adapter}:${reviewer.model}; vendor: ${reviewer.vendor}; provider: ${provider}; cause: seat-launch-failed) — failing closed`,
|
|
650
|
+
details: `review dispatch failed — ${error.message} (reviewer ${reviewer.adapter}:${reviewer.model}; vendor: ${reviewer.vendor}; provider: ${provider}; cause: seat-launch-failed${launch}) — failing closed`,
|
|
647
651
|
meta: { ...policyMeta, ...rotationMeta, ...floorMeta, reviewer: channelKey(reviewer), reviewerTier: reviewer.tier,
|
|
648
652
|
vendor: reviewer.vendor, provider, noVerdict: true, classification: "infra", infra: true,
|
|
649
|
-
cause: "seat-launch-failed", ...(
|
|
653
|
+
cause: "seat-launch-failed", ...(error.launchCause ? { launchCause: error.launchCause } : {}),
|
|
654
|
+
...(savedBrief ? { briefPath: savedBrief } : {}) } };
|
|
650
655
|
}
|
|
651
656
|
const raw = llm.output;
|
|
652
657
|
let saved;
|
|
@@ -75,8 +75,9 @@ export interface GateContext {
|
|
|
75
75
|
reviewNoVerdicts?: Map<string, string[]>;
|
|
76
76
|
recheck?: boolean;
|
|
77
77
|
/** Explicit worker funding requires fresh red measurements, never a gate waiver. OBS-1106: so does
|
|
78
|
-
* a retry that landed nothing on a timeout-class red — its one fresh re-observation.
|
|
79
|
-
|
|
78
|
+
* a retry that landed nothing on a timeout-class red — its one fresh re-observation. C: and one whose
|
|
79
|
+
* unchanged red is attributed wholly outside the task's files[] — its one fresh re-execution. */
|
|
80
|
+
cachedRedBypass?: "operator-rerun" | "timeout-fresh" | "out-of-scope-fresh";
|
|
80
81
|
carriedAuthors?: readonly string[];
|
|
81
82
|
/** The attempt whose worker last wrote the gated checkout; a dirty-tree refusal stamps it on the
|
|
82
83
|
* preserve commit and its row. Absent (standalone verify, gate-only restores) preserves as "unknown". */
|
package/dist/gates/run-gates.js
CHANGED
|
@@ -467,16 +467,28 @@ export async function runGates(task, ctx) {
|
|
|
467
467
|
};
|
|
468
468
|
const shapeGates = ctx.cfg.gates.byShape?.[task.shape];
|
|
469
469
|
const enabled = (g) => task.gates.includes(g) && (g !== "acceptance" && g !== "review" || shapeGates?.[g] !== false);
|
|
470
|
-
const failed = () => results.some((r) => !r.pass);
|
|
471
470
|
// T4 (OBS-265): a GREEN selected-test run is a screen, not the round's verdict — the merge-candidate
|
|
472
471
|
// round re-runs the full suite on the same commit and THAT is what the round reports. With no
|
|
473
472
|
// semantic gate to act on it, the screen is held so its full suite speaks for it in one row.
|
|
474
473
|
// (A RED screen IS the verdict: the round ends there, so it is recorded immediately.)
|
|
475
474
|
let heldTest;
|
|
475
|
+
const semantic = enabled("acceptance") || enabled("review");
|
|
476
|
+
// v2.6.5 T6 (D): the closed repair-mode table. A repair the REVIEW sent back (carried review:material)
|
|
477
|
+
// whose test history is clean — selection allowed, no required repair test — asks the question it
|
|
478
|
+
// was sent back for first: cheap gates → acceptance ‖ review → screen → merge-candidate full suite,
|
|
479
|
+
// so a semantic red costs zero test starts. selectTests false or a nonempty requiredRepairTests is a
|
|
480
|
+
// test-red repair and keeps battery-first order; so does every first attempt (nothing carried).
|
|
481
|
+
const semanticFirst = semantic && ctx.selectTests === true && !ctx.requiredRepairTests?.length
|
|
482
|
+
&& (ctx.carriedFindings ?? []).some((f) => f.class === "review:material");
|
|
483
|
+
// Semantic-first, a review that returned NO verdict is not a red that ends the round before its
|
|
484
|
+
// test proof: the daemon re-asks only the review (runReviewRecovery) and merges on the rows beside
|
|
485
|
+
// it, so the round still buys its screen and full suite. The row stays red; the round is unsatisfied.
|
|
486
|
+
const failed = () => results.some((r) => !r.pass && !(semanticFirst && r.gate === "review" && r.meta?.noVerdict === true));
|
|
476
487
|
// OBS-1176: when acceptance/review WILL act on a green screen, the screen is published before they
|
|
477
488
|
// start, as its own selected row. The full suite afterwards is a second invocation on its own row —
|
|
478
489
|
// it carries only its own receipts and interval, so it neither erases nor re-counts the screen.
|
|
479
|
-
|
|
490
|
+
// Semantic-first, nothing acts on the screen but its full suite: it is held like a gate-less round's.
|
|
491
|
+
const publishScreen = semantic && !semanticFirst;
|
|
480
492
|
// v2.0 T2 (OBS-554): this round's per-gate measurement. Every interval a gate actually spends
|
|
481
493
|
// executing is added HERE, at the call site that runs it, so a gate that runs twice (the test
|
|
482
494
|
// gate's screen and its full suite) sums to its own cost and never to the span between them.
|
|
@@ -1126,8 +1138,12 @@ export async function runGates(task, ctx) {
|
|
|
1126
1138
|
if (typeof rv.meta.reviewer === "string") {
|
|
1127
1139
|
const reviewer = rv.meta.reviewer;
|
|
1128
1140
|
const cause = String(rv.meta.cause);
|
|
1141
|
+
// E1: a checkout-proof timeout is the host being slow to print the seat's proof, not the seat
|
|
1142
|
+
// failing. It is still a no-verdict re-routed away from for THIS round, but it never strikes the
|
|
1143
|
+
// run-scoped tally; every other launch failure (foreign or malformed proof included) does.
|
|
1144
|
+
const proofTimeout = cause === "seat-launch-failed" && rv.meta.launchCause === "checkout-proof-timeout";
|
|
1129
1145
|
// OBS-1025 add.2: the run-scoped tally; two no-verdicts retire the seat for the rest of the run.
|
|
1130
|
-
const causes = rv.meta.noVerdict === true && ctx.reviewNoVerdicts
|
|
1146
|
+
const causes = rv.meta.noVerdict === true && ctx.reviewNoVerdicts && !proofTimeout
|
|
1131
1147
|
? [...(ctx.reviewNoVerdicts.get(reviewer) ?? []), cause] : undefined;
|
|
1132
1148
|
if (causes)
|
|
1133
1149
|
ctx.reviewNoVerdicts.set(reviewer, causes);
|
|
@@ -1272,51 +1288,9 @@ export async function runGates(task, ctx) {
|
|
|
1272
1288
|
if (failed())
|
|
1273
1289
|
return done();
|
|
1274
1290
|
}
|
|
1275
|
-
//
|
|
1276
|
-
//
|
|
1277
|
-
|
|
1278
|
-
? await coveringTests(ctx.worktree, ctx.baseRef)
|
|
1279
|
-
: undefined;
|
|
1280
|
-
let selectionReason = ctx.selectionReason ?? (selected ? "affected-tests" : "full-suite-fallback");
|
|
1281
|
-
if (selected && ctx.requiredRepairTests?.length) {
|
|
1282
|
-
const required = ctx.requiredRepairTests;
|
|
1283
|
-
const safe = required.every((path) => posix.normalize(path) === path && !path.startsWith("../")
|
|
1284
|
-
&& !path.startsWith("/") && TEST_FILE_RE.test(path) && existsSync(join(ctx.worktree, path)));
|
|
1285
|
-
const listed = safe ? await shGit(`git ls-files -z -- ${required.map(shq).join(" ")}`, ctx.worktree) : undefined;
|
|
1286
|
-
const tracked = new Set(listed?.stdout.split("\0").filter(Boolean));
|
|
1287
|
-
if (!listed || listed.code !== 0 || required.some((path) => !tracked.has(path))) {
|
|
1288
|
-
selected = undefined;
|
|
1289
|
-
selectionReason = "required-repair-test-unavailable";
|
|
1290
|
-
}
|
|
1291
|
-
else
|
|
1292
|
-
selected = [...new Set([...selected, ...required])].sort();
|
|
1293
|
-
}
|
|
1294
|
-
// OBS-635: a screen buys nothing when the full suite that must follow it already has a qualified
|
|
1295
|
-
// green on this exact identity (read BEFORE any screen), or when the harness-measured screen costs
|
|
1296
|
-
// at least 75 % of it — then the full suite runs in the battery instead. Unknown timing keeps the
|
|
1297
|
-
// screen. A selected-only green never answers here: its identity names its selection.
|
|
1298
|
-
let promotion;
|
|
1299
|
-
if (selected) {
|
|
1300
|
-
const hit = verdictStore.get(await fullTestIdentity());
|
|
1301
|
-
const costRatio = screenCostRatio(ctx.baseline, selected);
|
|
1302
|
-
if (hit?.pass === true && !isInfraResult(hit) && await certifiesFullManifest(hit))
|
|
1303
|
-
promotion = { reason: "full-green-cache" };
|
|
1304
|
-
else if (costRatio !== undefined && costRatio >= SCREEN_PROMOTION_RATIO)
|
|
1305
|
-
promotion = { reason: "screen-cost-promoted", costRatio };
|
|
1306
|
-
if (promotion)
|
|
1307
|
-
selected = undefined;
|
|
1308
|
-
}
|
|
1309
|
-
if (ctx.selectionReason || promotion)
|
|
1310
|
-
selectionDecision = {
|
|
1311
|
-
scope: selected ? "selected" : "full", reason: promotion?.reason ?? (selected ? selectionReason
|
|
1312
|
-
: selectionReason === "known-failing-files" ? "unsupported-selection-full-suite" : selectionReason),
|
|
1313
|
-
requiredFiles: [...(ctx.requiredRepairTests ?? [])],
|
|
1314
|
-
...(promotion?.costRatio !== undefined ? { costRatio: promotion.costRatio } : {}),
|
|
1315
|
-
};
|
|
1316
|
-
await runBattery(selected ? { ...ctx.commands, test: testCommandForFiles(ctx.commands.test, selected) } : ctx.commands, selected, enabled("test") ? ["test"] : []);
|
|
1317
|
-
if (failed())
|
|
1318
|
-
return done();
|
|
1319
|
-
if (enabled("acceptance") || enabled("review")) {
|
|
1291
|
+
// The judge and the reviewer, together. Returns true when the round ends here: a red, or a
|
|
1292
|
+
// cancelled sibling — a cancelled seat's round never goes on to borrow a test row.
|
|
1293
|
+
const runSemantics = async () => {
|
|
1320
1294
|
// Judge and review are launched TOGETHER (96m of serialization over 5 runs). Enforcement is
|
|
1321
1295
|
// unchanged — it is still the AND of both, both still fail closed, and neither reads the other's
|
|
1322
1296
|
// verdict: each gets the same commit and the same brief it always got, and neither promise is
|
|
@@ -1365,16 +1339,83 @@ export async function runGates(task, ctx) {
|
|
|
1365
1339
|
if (failure)
|
|
1366
1340
|
throw failure.reason;
|
|
1367
1341
|
executionSignal()?.throwIfAborted();
|
|
1368
|
-
|
|
1342
|
+
return cancelled || failed();
|
|
1343
|
+
};
|
|
1344
|
+
if (semanticFirst) {
|
|
1345
|
+
if (await runSemantics()) {
|
|
1346
|
+
// A cancelled round starts no test, yet its no-verdict review can still be re-asked alone. Its test
|
|
1347
|
+
// proof is recorded OWED — unsatisfied, never a verdict — so that re-ask cannot merge an unrun subject.
|
|
1348
|
+
if (!failed() && enabled("test") && ctx.commands.test !== undefined) {
|
|
1349
|
+
await record({ gate: "test", pass: false, details: "test not run — the semantic round was cancelled before its "
|
|
1350
|
+
+ "test screen; the merge-candidate full suite is still owed on this subject",
|
|
1351
|
+
meta: { skipped: true, infra: true, classification: "infra", retryable: false, testOwed: true } });
|
|
1352
|
+
}
|
|
1353
|
+
return done();
|
|
1354
|
+
}
|
|
1355
|
+
// Subject freshness: the semantic gates ran oracles and vendor CLIs in this worktree. Their dirt is
|
|
1356
|
+
// not the committed subject, so no screen measures it and no cached green row answers for it.
|
|
1357
|
+
const dirt = enabled("test") && ctx.commands.test !== undefined ? await dirtyWorktree() : undefined;
|
|
1358
|
+
if (dirt) {
|
|
1359
|
+
await emitStart("test");
|
|
1360
|
+
await record(await dirtyRefusal("test", dirt));
|
|
1369
1361
|
return done();
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
// A non-final round may run only the tests covering its own diff; the merge-candidate round below
|
|
1365
|
+
// pays the full suite anyway, so a selection that misses costs a round and can never merge.
|
|
1366
|
+
let selected = ctx.selectTests && enabled("test") && ctx.commands.test
|
|
1367
|
+
? await coveringTests(ctx.worktree, ctx.baseRef)
|
|
1368
|
+
: undefined;
|
|
1369
|
+
let selectionReason = ctx.selectionReason ?? (selected ? "affected-tests" : "full-suite-fallback");
|
|
1370
|
+
if (selected && ctx.requiredRepairTests?.length) {
|
|
1371
|
+
const required = ctx.requiredRepairTests;
|
|
1372
|
+
const safe = required.every((path) => posix.normalize(path) === path && !path.startsWith("../")
|
|
1373
|
+
&& !path.startsWith("/") && TEST_FILE_RE.test(path) && existsSync(join(ctx.worktree, path)));
|
|
1374
|
+
const listed = safe ? await shGit(`git ls-files -z -- ${required.map(shq).join(" ")}`, ctx.worktree) : undefined;
|
|
1375
|
+
const tracked = new Set(listed?.stdout.split("\0").filter(Boolean));
|
|
1376
|
+
if (!listed || listed.code !== 0 || required.some((path) => !tracked.has(path))) {
|
|
1377
|
+
selected = undefined;
|
|
1378
|
+
selectionReason = "required-repair-test-unavailable";
|
|
1379
|
+
}
|
|
1380
|
+
else
|
|
1381
|
+
selected = [...new Set([...selected, ...required])].sort();
|
|
1382
|
+
}
|
|
1383
|
+
// OBS-635: a screen buys nothing when the full suite that must follow it already has a qualified
|
|
1384
|
+
// green on this exact identity (read BEFORE any screen), or when the harness-measured screen costs
|
|
1385
|
+
// at least 75 % of it — then the full suite runs in the battery instead. Unknown timing keeps the
|
|
1386
|
+
// screen. A selected-only green never answers here: its identity names its selection.
|
|
1387
|
+
let promotion;
|
|
1388
|
+
if (selected) {
|
|
1389
|
+
const hit = verdictStore.get(await fullTestIdentity());
|
|
1390
|
+
const costRatio = screenCostRatio(ctx.baseline, selected);
|
|
1391
|
+
if (hit?.pass === true && !isInfraResult(hit) && await certifiesFullManifest(hit))
|
|
1392
|
+
promotion = { reason: "full-green-cache" };
|
|
1393
|
+
else if (costRatio !== undefined && costRatio >= SCREEN_PROMOTION_RATIO)
|
|
1394
|
+
promotion = { reason: "screen-cost-promoted", costRatio };
|
|
1395
|
+
if (promotion)
|
|
1396
|
+
selected = undefined;
|
|
1370
1397
|
}
|
|
1398
|
+
if (ctx.selectionReason || promotion)
|
|
1399
|
+
selectionDecision = {
|
|
1400
|
+
scope: selected ? "selected" : "full", reason: promotion?.reason ?? (selected ? selectionReason
|
|
1401
|
+
: selectionReason === "known-failing-files" ? "unsupported-selection-full-suite" : selectionReason),
|
|
1402
|
+
requiredFiles: [...(ctx.requiredRepairTests ?? [])],
|
|
1403
|
+
...(promotion?.costRatio !== undefined ? { costRatio: promotion.costRatio } : {}),
|
|
1404
|
+
};
|
|
1405
|
+
await runBattery(selected ? { ...ctx.commands, test: testCommandForFiles(ctx.commands.test, selected) } : ctx.commands, selected, enabled("test") ? ["test"] : []);
|
|
1406
|
+
if (failed())
|
|
1407
|
+
return done();
|
|
1408
|
+
if (semantic && !semanticFirst && await runSemantics())
|
|
1409
|
+
return done();
|
|
1371
1410
|
// OBS-635: the semantic gates ran oracles and vendor CLIs in this worktree after an in-battery full
|
|
1372
1411
|
// suite spoke, and its green stands only for the identity it measured. Oracle dirt withdraws it; a
|
|
1373
1412
|
// changed or unmeasurable identity — tree, command, baseline, environment, dependency resolution,
|
|
1374
1413
|
// capacity, protocol, lifecycle, full manifest — buys a fresh merge-candidate suite below.
|
|
1414
|
+
// Semantic-first too: an in-battery full suite that ran AFTER the semantic gates is clear of their
|
|
1415
|
+
// dirt, never of its own test command's effects (an ignored lockfile, a newly collected test).
|
|
1375
1416
|
let rerunFull = false;
|
|
1376
1417
|
listing = undefined;
|
|
1377
|
-
if (fullInBattery && ctx.commands.test !== undefined &&
|
|
1418
|
+
if (fullInBattery && ctx.commands.test !== undefined && semantic) {
|
|
1378
1419
|
const dirt = await dirtyWorktree();
|
|
1379
1420
|
if (dirt) {
|
|
1380
1421
|
const refusal = withTelemetry(await dirtyRoundRefusal("test", dirt));
|