tickmarkr 2.6.1 → 2.6.3
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/README.md +16 -3
- package/dist/adapters/catalog-remote.js +89 -47
- package/dist/adapters/claude-code.js +9 -6
- package/dist/adapters/codex.js +7 -4
- package/dist/adapters/prompt.d.ts +1 -0
- package/dist/adapters/prompt.js +14 -6
- package/dist/adapters/registry.js +3 -3
- package/dist/adapters/types.d.ts +12 -4
- package/dist/adapters/types.js +6 -0
- package/dist/cli/commands/approve.d.ts +11 -4
- package/dist/cli/commands/approve.js +82 -27
- package/dist/cli/commands/compile.js +13 -3
- package/dist/cli/commands/doctor.d.ts +8 -2
- package/dist/cli/commands/doctor.js +11 -3
- package/dist/cli/commands/fleet.js +87 -11
- package/dist/cli/commands/plan.js +13 -8
- package/dist/cli/commands/report.d.ts +2 -1
- package/dist/cli/commands/report.js +74 -8
- package/dist/cli/commands/resume.js +4 -2
- package/dist/cli/commands/status.js +43 -20
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +9 -2
- package/dist/compile/native.js +7 -0
- package/dist/config/config.d.ts +35 -2
- package/dist/config/config.js +86 -10
- package/dist/config/fleet-overlay.d.ts +13 -2
- package/dist/config/fleet-overlay.js +60 -0
- package/dist/drivers/herdr.d.ts +12 -0
- package/dist/drivers/herdr.js +51 -0
- package/dist/drivers/orca.d.ts +35 -2
- package/dist/drivers/orca.js +222 -67
- package/dist/drivers/types.d.ts +2 -0
- package/dist/drivers/types.js +2 -2
- package/dist/eval/canary.d.ts +2 -1
- package/dist/eval/canary.js +2 -2
- package/dist/eval/dispatch.js +1 -0
- package/dist/gates/acceptance.d.ts +9 -1
- package/dist/gates/acceptance.js +31 -4
- package/dist/gates/baseline.d.ts +32 -2
- package/dist/gates/baseline.js +111 -24
- package/dist/gates/cache.d.ts +8 -0
- package/dist/gates/cache.js +12 -2
- package/dist/gates/llm.d.ts +11 -4
- package/dist/gates/llm.js +40 -21
- package/dist/gates/review.d.ts +14 -1
- package/dist/gates/review.js +160 -34
- package/dist/gates/run-gates.d.ts +56 -4
- package/dist/gates/run-gates.js +358 -58
- package/dist/gates/test-manifest.d.ts +45 -1
- package/dist/gates/test-manifest.js +78 -12
- package/dist/graph/schema.d.ts +2 -0
- package/dist/graph/schema.js +2 -0
- package/dist/plan/scope.js +2 -2
- package/dist/route/preference.d.ts +20 -2
- package/dist/route/preference.js +48 -13
- package/dist/route/router.d.ts +12 -1
- package/dist/route/router.js +56 -24
- package/dist/run/consult.d.ts +15 -1
- package/dist/run/consult.js +18 -7
- package/dist/run/daemon.d.ts +38 -2
- package/dist/run/daemon.js +895 -192
- package/dist/run/git.d.ts +8 -0
- package/dist/run/git.js +14 -0
- package/dist/run/interactive-seed.d.ts +4 -0
- package/dist/run/interactive-seed.js +35 -9
- package/dist/run/journal.d.ts +152 -3
- package/dist/run/journal.js +551 -50
- package/dist/run/lease.d.ts +13 -0
- package/dist/run/lease.js +45 -0
- package/dist/run/merge.d.ts +3 -1
- package/dist/run/merge.js +3 -2
- package/dist/run/operator-summary.d.ts +3 -0
- package/dist/run/operator-summary.js +3 -1
- package/dist/run/protocol.d.ts +46 -1
- package/dist/run/protocol.js +14 -2
- package/dist/run/receipt-resolver.d.ts +22 -0
- package/dist/run/receipt-resolver.js +40 -1
- package/dist/run/repair-selection.d.ts +11 -1
- package/dist/run/repair-selection.js +17 -9
- package/dist/run/supervision.d.ts +7 -1
- package/dist/run/supervision.js +5 -2
- package/dist/run/wall-budget.d.ts +48 -0
- package/dist/run/wall-budget.js +280 -0
- package/dist/tui/cockpit/board.js +3 -3
- package/dist/tui/cockpit/decision-actions.d.ts +8 -5
- package/dist/tui/cockpit/decision-actions.js +55 -32
- package/dist/tui/cockpit/derive.js +13 -2
- package/dist/tui/cockpit/live-runtime.d.ts +10 -0
- package/dist/tui/cockpit/live-runtime.js +50 -3
- package/dist/tui/cockpit/run-cockpit.d.ts +3 -0
- package/dist/tui/cockpit/run-cockpit.js +27 -2
- package/dist/tui/cockpit/run-view.d.ts +9 -2
- package/dist/tui/cockpit/run-view.js +66 -9
- package/dist/tui/cockpit/setup-cockpit.d.ts +6 -0
- package/dist/tui/cockpit/setup-cockpit.js +10 -3
- package/dist/tui/ink/fleet-app.d.ts +15 -3
- package/dist/tui/ink/fleet-app.js +91 -22
- package/package.json +3 -1
- package/schema/config.schema.json +825 -0
- package/skills/tickmarkr-loop/SKILL.md +15 -3
- package/skills/tickmarkr-overseer/SKILL.md +42 -0
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +91 -0
- package/skills/tickmarkr-overseer/scripts/context-statusline.sh +81 -0
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +36 -34
- package/skills/tickmarkr-overseer/scripts/watch-journal.sh +6 -4
package/dist/drivers/orca.js
CHANGED
|
@@ -5,7 +5,7 @@ import { shq } from "../adapters/types.js";
|
|
|
5
5
|
import { createWorktree, FORK_CAP_ENV, resolvedForkCap, sh } from "../run/git.js";
|
|
6
6
|
import { Journal, parseRunId } from "../run/journal.js";
|
|
7
7
|
import { stateDirName } from "../graph/graph.js";
|
|
8
|
-
import { readWatchBoard, requestWatchBoardStop, stopWatchBoard, WATCH_OWNER_ENV } from "../run/supervision.js";
|
|
8
|
+
import { readWatchBoard, requestWatchBoardStop, stopWatchBoard, supervisionPresencePath, WATCH_OWNER_ENV } from "../run/supervision.js";
|
|
9
9
|
import { MAX_BUF } from "./subprocess.js";
|
|
10
10
|
import { formatOwnedName, panesToClose, parseOwnedName } from "./types.js";
|
|
11
11
|
// Orca (onorca.dev) as a third execution surface beside herdr and subprocess. tickmarkr keeps
|
|
@@ -454,6 +454,26 @@ function lockEntryGeneration(entry) {
|
|
|
454
454
|
const legacy = /^pid(?:\.(\d+))?$/.exec(entry);
|
|
455
455
|
return owner ? Number(owner[1]) : legacy ? Number(legacy[1] ?? 0) : undefined;
|
|
456
456
|
}
|
|
457
|
+
/** The placing claim window: how long a narrator waits for its observer to claim the reservation. */
|
|
458
|
+
const CLAIM_WINDOW_MS = 5000;
|
|
459
|
+
/** Reservation tokens THIS process held after an indeterminate split receipt (OBS-1172). Any other
|
|
460
|
+
* reserved or claimed record under a live placer is a placement in flight (or a crash still being
|
|
461
|
+
* judged) and is refused untouched; a held one is reconciled again on every call, never split over. */
|
|
462
|
+
const held = new Set();
|
|
463
|
+
/** A claimed board's observer is live when its pid answers AND the arm it claimed with is present. */
|
|
464
|
+
function observerLive(r) {
|
|
465
|
+
return typeof r.pid === "number" && pidLive(r.pid) && typeof r.armId === "string"
|
|
466
|
+
&& existsSync(supervisionPresencePath(r.repo, "watch", r.armId));
|
|
467
|
+
}
|
|
468
|
+
/** Tombstone THIS reservation while it is still the untouched reservation; a record anyone else moved
|
|
469
|
+
* is left to them. Answers the tombstone, or undefined when the record was not this reservation. */
|
|
470
|
+
async function tombstoneReservation(path, cwd, runId, token, time, extra = {}) {
|
|
471
|
+
const now = readBoard(path, cwd, runId);
|
|
472
|
+
if (now?.record.token !== token || boardState(now.record) !== "reserved")
|
|
473
|
+
return undefined;
|
|
474
|
+
const record = { ...now.record, ...extra, retired: true };
|
|
475
|
+
return { raw: await casBoard("split", path, now.raw, record, time), record };
|
|
476
|
+
}
|
|
457
477
|
function pidLive(pid) {
|
|
458
478
|
try {
|
|
459
479
|
process.kill(pid, 0);
|
|
@@ -699,7 +719,28 @@ export class OrcaDriver {
|
|
|
699
719
|
this.pendingProjects.delete(owned.taskId);
|
|
700
720
|
}
|
|
701
721
|
}
|
|
702
|
-
|
|
722
|
+
const slot = { id, name: title, cwd: worktree, group: opts?.group, readScreen: () => this.readScreen(slot) };
|
|
723
|
+
return slot;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* OBS-1109: bind THIS instance to a terminal an earlier daemon created for an OWNED slot, so a
|
|
727
|
+
* resumed daemon can read the interrupted attempt's pane. Binding needs ownership evidence, never
|
|
728
|
+
* a title or handle alone: exactly one tab on the tracked worktree carries the FULL journaled owned
|
|
729
|
+
* title, it resolves to exactly one terminal, and that terminal's scrollback proves the exact task
|
|
730
|
+
* checkout `slot.cwd` (the frame only tickmarkr's own create command prints). Zero, ambiguous or
|
|
731
|
+
* foreign candidates leave the slot unbound: the throw is the caller's decline. The journaled
|
|
732
|
+
* nonce is then checked by the caller against the trailer this binding reads.
|
|
733
|
+
*/
|
|
734
|
+
async adopt(slot) {
|
|
735
|
+
if (!parseOwnedName(slot.name))
|
|
736
|
+
throw new OrcaError("list", `slot ${slot.name} carries no owned title — nothing to adopt`, "");
|
|
737
|
+
const cwd = canonicalWorktreePath(slot.cwd);
|
|
738
|
+
const st = { title: slot.name, cwd, buf: "", recoveries: 0, recovering: false };
|
|
739
|
+
st.tracked = await this.trackedWorktree(cwd);
|
|
740
|
+
await this.recover("adopt", st, undefined, "", { proveCheckout: true });
|
|
741
|
+
const id = `orca-${++this.n}`;
|
|
742
|
+
this.slots.set(id, st);
|
|
743
|
+
return { id, name: slot.name, cwd, ...(slot.group === undefined ? {} : { group: slot.group }) };
|
|
703
744
|
}
|
|
704
745
|
/** Where to invoke the CLI for this slot's calls (see OrcaSlotState.dir). */
|
|
705
746
|
cliCwd(st) {
|
|
@@ -979,7 +1020,7 @@ export class OrcaDriver {
|
|
|
979
1020
|
return { proven: false, reason: error instanceof Error ? error.message : String(error) };
|
|
980
1021
|
}
|
|
981
1022
|
}
|
|
982
|
-
async recover(family, st, newRuntimeId, raw) {
|
|
1023
|
+
async recover(family, st, newRuntimeId, raw, opts) {
|
|
983
1024
|
if (st.recovering)
|
|
984
1025
|
throw this.latched(family, st, "handle recovery re-entered", raw);
|
|
985
1026
|
st.recovering = true;
|
|
@@ -1034,17 +1075,19 @@ export class OrcaDriver {
|
|
|
1034
1075
|
throw this.latched(family, st, `the owned tab resolves to ${panes.length} terminals in ${home}`, env.raw);
|
|
1035
1076
|
}
|
|
1036
1077
|
const handle = panes[0];
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
if (st.tracked !== undefined && st.tracked !== st.cwd) {
|
|
1078
|
+
// Handles are runtime-scoped: the same VALUE under a different runtime proves nothing about
|
|
1079
|
+
// which terminal it addresses. OBS-1108: it is therefore reconciled exactly like any other
|
|
1080
|
+
// replacement plus the checkout proof — never on the handle text (a 2.5.8 latch refused a live,
|
|
1081
|
+
// finished worker for hours) and never adopted without that ownership evidence.
|
|
1082
|
+
const reused = handle === old;
|
|
1083
|
+
if (reused || opts?.proveCheckout || (st.tracked !== undefined && st.tracked !== st.cwd)) {
|
|
1043
1084
|
// FX-N01: a nested checkout shares its worktreePath with every sibling task's terminal, so
|
|
1044
1085
|
// the candidate must prove the checkout itself (read-only) before it is addressed as ours.
|
|
1045
1086
|
const proof = await this.checkoutProven(handle, st.cwd, this.cliCwd(st), env.runtimeId);
|
|
1046
1087
|
if (!proof.proven) {
|
|
1047
|
-
throw this.latched(family, st,
|
|
1088
|
+
throw this.latched(family, st, reused
|
|
1089
|
+
? `replacement handle ${handle} is the old handle value reused by runtime ${newRuntimeId ?? env.runtimeId ?? "unknown"} and does not prove checkout ${st.cwd} (${proof.reason}); a reused handle value is not adopted on its text`
|
|
1090
|
+
: `candidate ${handle} does not prove checkout ${st.cwd} (${proof.reason}); the sole same-titled tab on ${home} is not adopted`, env.raw);
|
|
1048
1091
|
}
|
|
1049
1092
|
}
|
|
1050
1093
|
st.handle = handle;
|
|
@@ -1133,14 +1176,14 @@ export class OrcaDriver {
|
|
|
1133
1176
|
throw new OrcaUnavailableError(family, `exited-shaped stream page: show answered by runtime ${show.runtimeId}, not the bound ${st.runtimeId}`, show.raw, "exited");
|
|
1134
1177
|
}
|
|
1135
1178
|
this.liveShowTerm(family, st, show);
|
|
1136
|
-
const screen = await this.
|
|
1179
|
+
const screen = await this.screenFrame(st);
|
|
1137
1180
|
if (screen.source !== "screen") {
|
|
1138
1181
|
throw new OrcaUnavailableError(family, `exited-shaped stream page and no rendered screen for ${st.handle}`, blind.raw, "exited");
|
|
1139
1182
|
}
|
|
1140
1183
|
return screen.term;
|
|
1141
1184
|
}
|
|
1142
1185
|
/** A rendered-frame liveness read. `--screen` and `--cursor` are mutually exclusive in Orca. */
|
|
1143
|
-
async
|
|
1186
|
+
async screenFrame(st) {
|
|
1144
1187
|
const env = await this.terminalOp("status", st, (h) => this.call("read", [
|
|
1145
1188
|
"terminal", "read", "--terminal", h, "--screen",
|
|
1146
1189
|
], this.cliCwd(st)));
|
|
@@ -1151,6 +1194,20 @@ export class OrcaDriver {
|
|
|
1151
1194
|
}
|
|
1152
1195
|
return { term, source };
|
|
1153
1196
|
}
|
|
1197
|
+
/**
|
|
1198
|
+
* OBS-1205: the public rendered-screen read a slot carries (Slot.readScreen). Kimi draws its trust
|
|
1199
|
+
* modal with cursor addressing, so the stream holds the box rules and no `Trust` text while the
|
|
1200
|
+
* painted frame shows `❯ Trust this folder`. Same ownership discipline as every read — bound
|
|
1201
|
+
* runtime, addressed handle, running status — and a frame the runtime could not render is
|
|
1202
|
+
* unreadable, never an empty screen a caller could read as "no modal".
|
|
1203
|
+
*/
|
|
1204
|
+
async readScreen(slot) {
|
|
1205
|
+
const st = this.state(slot);
|
|
1206
|
+
const { term, source } = await this.screenFrame(st);
|
|
1207
|
+
if (source !== "screen")
|
|
1208
|
+
throw new OrcaUnavailableError("read", `no rendered screen for ${st.handle}`, "");
|
|
1209
|
+
return this.tailText("read", term, "");
|
|
1210
|
+
}
|
|
1154
1211
|
/** A single UNPAGED tail read — exactly what the caller asked for and nothing more. Markers split
|
|
1155
1212
|
* across cursor pages are not reassembled here; that is waitOutput's job. */
|
|
1156
1213
|
async read(slot, lines) {
|
|
@@ -1227,7 +1284,7 @@ export class OrcaDriver {
|
|
|
1227
1284
|
// reporting even when the show record alone would still look connected (show carries no
|
|
1228
1285
|
// status field of its own, so a terminal can report "unknown"/"exited" on read while its
|
|
1229
1286
|
// show row still says connected — the read leg is the only place that catches that).
|
|
1230
|
-
const screen = await this.
|
|
1287
|
+
const screen = await this.screenFrame(st);
|
|
1231
1288
|
if (`${st.runtimeId}:${st.handle}:${st.recoveries}` !== gen) {
|
|
1232
1289
|
continue;
|
|
1233
1290
|
}
|
|
@@ -1365,7 +1422,7 @@ export class OrcaDriver {
|
|
|
1365
1422
|
async sendKey(slot, key) {
|
|
1366
1423
|
const st = this.state(slot);
|
|
1367
1424
|
this.assertAvailable("send", st);
|
|
1368
|
-
if (key === "enter") {
|
|
1425
|
+
if (key === "enter" || key === "Enter") { // "Enter": every adapter's declared trust key (OBS-1205)
|
|
1369
1426
|
await this.sendText(st, "");
|
|
1370
1427
|
return;
|
|
1371
1428
|
}
|
|
@@ -1398,7 +1455,7 @@ export class OrcaDriver {
|
|
|
1398
1455
|
return true;
|
|
1399
1456
|
const deadline = this.time.now() + NUDGE_ECHO_TIMEOUT_MS;
|
|
1400
1457
|
for (;;) {
|
|
1401
|
-
const screen = await this.
|
|
1458
|
+
const screen = await this.screenFrame(st);
|
|
1402
1459
|
// ponytail: "composer emptied" is "the text is no longer on the frame" — a TUI that keeps the
|
|
1403
1460
|
// submitted turn on screen reads as undelivered until the timeout; the receipt stage is primary.
|
|
1404
1461
|
if (screen.source === "screen") {
|
|
@@ -1435,13 +1492,29 @@ export class OrcaDriver {
|
|
|
1435
1492
|
}
|
|
1436
1493
|
}
|
|
1437
1494
|
else if (current) {
|
|
1438
|
-
|
|
1495
|
+
let state = boardState(current.record);
|
|
1496
|
+
const { placer, token: heldToken } = current.record;
|
|
1497
|
+
if ((state === "reserved" || state === "claimed") && placer === process.pid && held.has(heldToken)) {
|
|
1498
|
+
// OBS-1172: THIS process held the placement after an indeterminate split receipt. Reconciled
|
|
1499
|
+
// again, never split over: adopted once proven, still held while unknown, and tombstoned
|
|
1500
|
+
// only once proven to have started nothing.
|
|
1501
|
+
const settled = await this.settleIndeterminate(cwd, path, runId, heldToken, launchingHandle, 0);
|
|
1502
|
+
if (settled.kind === "held") {
|
|
1503
|
+
throw new OrcaError("split", `Orca narrator placement remains unresolved for ${name} (${state}, held after an indeterminate split receipt): ${settled.why}; no second board is split over it; ${kept}`, "");
|
|
1504
|
+
}
|
|
1505
|
+
held.delete(heldToken);
|
|
1506
|
+
if (settled.kind === "adopted")
|
|
1507
|
+
return settled.slot;
|
|
1508
|
+
current = await tombstoneReservation(path, cwd, runId, heldToken, this.time);
|
|
1509
|
+
if (!current)
|
|
1510
|
+
throw new OrcaError("split", `Orca narrator placement remains unresolved for ${name}: the held reservation changed underneath; ${kept}`, "");
|
|
1511
|
+
state = boardState(current.record);
|
|
1512
|
+
}
|
|
1439
1513
|
if (state === "reserved" || state === "claimed") {
|
|
1440
1514
|
// A reservation or claim with no bound pane is the placing narrator's normal intermediate
|
|
1441
1515
|
// state while that narrator lives (awaiting its receipt, listing, claim or bind): refused
|
|
1442
1516
|
// untouched, its observer never asked to stop. Only a DEAD placer (or one unrecorded) makes
|
|
1443
1517
|
// it a crash to recover.
|
|
1444
|
-
const { placer } = current.record;
|
|
1445
1518
|
if (typeof placer !== "number" || pidLive(placer)) {
|
|
1446
1519
|
throw new OrcaError("split", `Orca narrator placement remains unresolved for ${name} (${state}, no bound pane) while placing narrator ${placer ?? "unrecorded"} lives; ${kept}`, "");
|
|
1447
1520
|
}
|
|
@@ -1450,14 +1523,11 @@ export class OrcaDriver {
|
|
|
1450
1523
|
// The placing driver crashed between the observer's claim and its bind. The record is
|
|
1451
1524
|
// replaced only once that observer is proven gone: dead outright, or live and stopped with
|
|
1452
1525
|
// its acknowledgement awaited on injected time — unacknowledged, it is refused untouched.
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
}
|
|
1458
|
-
catch (error) {
|
|
1459
|
-
throw new OrcaError("split", `Orca narrator placement refused for ${name}: claimed board's live observer unacknowledged; ${kept}; ${errorText(error)}`, "");
|
|
1460
|
-
}
|
|
1526
|
+
try {
|
|
1527
|
+
await this.stopObserver(current.record);
|
|
1528
|
+
}
|
|
1529
|
+
catch (error) {
|
|
1530
|
+
throw new OrcaError("split", `Orca narrator placement refused for ${name}: claimed board's live observer unacknowledged; ${kept}; ${errorText(error)}`, "");
|
|
1461
1531
|
}
|
|
1462
1532
|
const retired = { ...current.record, retired: true };
|
|
1463
1533
|
current = { raw: await casBoard("split", path, current.raw, retired, this.time), record: retired };
|
|
@@ -1471,10 +1541,13 @@ export class OrcaDriver {
|
|
|
1471
1541
|
if (state === "bound") {
|
|
1472
1542
|
// The bound record is the durable truth of placement: the child handle PLUS the split
|
|
1473
1543
|
// envelope's runtime identity. A handle listed by a later runtime is a different pane.
|
|
1474
|
-
|
|
1544
|
+
// OBS-1131: it is adopted only while its observer lives — a resumed daemon retires the
|
|
1545
|
+
// board of a dead observer here (tombstone, stop request, handle-bound close) instead of
|
|
1546
|
+
// answering it and later spending a reopen to discover the death.
|
|
1547
|
+
if (observerLive(current.record) && listedOnRuntime(await this.listAll(cwd), current.record.pane, current.record.runtimeId)) {
|
|
1475
1548
|
return watchSlot(cwd, name, current.record.pane);
|
|
1476
1549
|
}
|
|
1477
|
-
// Lost
|
|
1550
|
+
// Lost or dead: tombstone before anything else.
|
|
1478
1551
|
const retired = { ...current.record, retired: true };
|
|
1479
1552
|
current = { raw: await casBoard("split", path, current.raw, retired, this.time), record: retired };
|
|
1480
1553
|
}
|
|
@@ -1482,12 +1555,7 @@ export class OrcaDriver {
|
|
|
1482
1555
|
// observer must stop and ack on injected time before the handle-bound close; timeout keeps
|
|
1483
1556
|
// the tombstone and does not replace it.
|
|
1484
1557
|
try {
|
|
1485
|
-
|
|
1486
|
-
await stopWatchBoard(current.record, this.time);
|
|
1487
|
-
}
|
|
1488
|
-
else {
|
|
1489
|
-
requestWatchBoardStop(current.record);
|
|
1490
|
-
}
|
|
1558
|
+
await this.stopObserver(current.record);
|
|
1491
1559
|
}
|
|
1492
1560
|
catch (error) {
|
|
1493
1561
|
throw new OrcaError("split", `Orca narrator placement refused for ${name}: retired board observer unacknowledged; ${kept}; ${errorText(error)}`, "");
|
|
@@ -1503,37 +1571,43 @@ export class OrcaDriver {
|
|
|
1503
1571
|
// judged replaceable above.
|
|
1504
1572
|
const token = randomUUID();
|
|
1505
1573
|
await casBoard("split", path, current?.raw, { repo: realpathSync(cwd), runId, driver: this.id, workspace: ORCA_SPACE, pane: "", name, token, placer: process.pid }, this.time);
|
|
1506
|
-
//
|
|
1507
|
-
//
|
|
1508
|
-
//
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1574
|
+
// OBS-1172: a split whose receipt never arrived intact — a transport failure or timeout, an
|
|
1575
|
+
// unparseable or handle-less body, a child the listing does not place in the launching tab —
|
|
1576
|
+
// may still have started the board. It is reconciled before any failure: a uniquely proven
|
|
1577
|
+
// live pane is adopted and bound; a split proven to have started nothing is tombstoned so the
|
|
1578
|
+
// next call splits afresh; anything else is HELD — the reservation stays, and no later call
|
|
1579
|
+
// splits a second board over it.
|
|
1580
|
+
const indeterminate = async (reason, raw) => {
|
|
1581
|
+
const settled = await this.settleIndeterminate(cwd, path, runId, token, launchingHandle, CLAIM_WINDOW_MS);
|
|
1582
|
+
if (settled.kind === "adopted")
|
|
1583
|
+
return settled.slot;
|
|
1584
|
+
if (settled.kind === "absent") {
|
|
1585
|
+
await tombstoneReservation(path, cwd, runId, token, this.time).catch(() => undefined);
|
|
1586
|
+
throw new OrcaError("split", `Orca narrator placement failed for ${name}: ${reason}; ${kept}`, raw);
|
|
1515
1587
|
}
|
|
1516
|
-
|
|
1588
|
+
held.add(token);
|
|
1589
|
+
throw new OrcaError("split", `Orca narrator placement unresolved for ${name}: ${reason}; ${settled.why} — the placement is held and no second board is split over it; ${kept}`, raw);
|
|
1517
1590
|
};
|
|
1518
1591
|
// Exactly one terminal split of the launching handle, horizontal, carrying token and command.
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1592
|
+
let env;
|
|
1593
|
+
try {
|
|
1594
|
+
env = await this.call("split", [
|
|
1595
|
+
"terminal", "split",
|
|
1596
|
+
"--terminal", launchingHandle,
|
|
1597
|
+
"--direction", "horizontal",
|
|
1598
|
+
"--command", `${WATCH_OWNER_ENV}=${shq(token)} ${command}`,
|
|
1599
|
+
], cwd);
|
|
1600
|
+
}
|
|
1601
|
+
catch (error) {
|
|
1602
|
+
return await indeterminate(`split receipt is unknown (${error instanceof OrcaError ? error.reason : errorText(error)})`, error instanceof OrcaError ? error.raw : "");
|
|
1603
|
+
}
|
|
1604
|
+
const malformed = "split receipt is unknown, malformed or handle-less";
|
|
1530
1605
|
let child;
|
|
1531
1606
|
try {
|
|
1532
1607
|
child = splitReceipt(env);
|
|
1533
1608
|
}
|
|
1534
1609
|
catch {
|
|
1535
|
-
await
|
|
1536
|
-
throw new OrcaError("split", `Orca narrator placement failed for ${name}: split receipt is unknown, malformed or handle-less; ${kept}`, env.raw);
|
|
1610
|
+
return await indeterminate(malformed, env.raw);
|
|
1537
1611
|
}
|
|
1538
1612
|
// Parent tabId must name the launching terminal's tab, and the child must actually appear
|
|
1539
1613
|
// there. A handle-only object, a tabId for some other tab, or a handle the list does not
|
|
@@ -1543,21 +1617,19 @@ export class OrcaDriver {
|
|
|
1543
1617
|
listing = await this.listAll(cwd);
|
|
1544
1618
|
}
|
|
1545
1619
|
catch (error) {
|
|
1546
|
-
await
|
|
1547
|
-
throw new OrcaError("split", `Orca narrator placement failed for ${name}: split receipt is unknown, malformed or handle-less; ${kept}`, error instanceof OrcaError ? error.raw : env.raw);
|
|
1620
|
+
return await indeterminate(malformed, error instanceof OrcaError ? error.raw : env.raw);
|
|
1548
1621
|
}
|
|
1549
1622
|
const launchingTabId = terminalTabId(listing, launchingHandle);
|
|
1550
1623
|
if (!launchingTabId
|
|
1551
1624
|
|| child.tabId !== launchingTabId
|
|
1552
1625
|
|| child.handle === launchingHandle
|
|
1553
1626
|
|| terminalTabId(listing, child.handle) !== launchingTabId) {
|
|
1554
|
-
await
|
|
1555
|
-
throw new OrcaError("split", `Orca narrator placement failed for ${name}: split receipt is unknown, malformed or handle-less; ${kept}`, env.raw);
|
|
1627
|
+
return await indeterminate(malformed, env.raw);
|
|
1556
1628
|
}
|
|
1557
1629
|
const childHandle = child.handle;
|
|
1558
1630
|
// The narrator writes nothing until the observer's single claim is visible, so neither write
|
|
1559
1631
|
// can erase the other; after it the observer never writes the record again.
|
|
1560
|
-
const claimDeadline = this.time.now() +
|
|
1632
|
+
const claimDeadline = this.time.now() + CLAIM_WINDOW_MS;
|
|
1561
1633
|
let claim;
|
|
1562
1634
|
for (;;) {
|
|
1563
1635
|
const check = readBoard(path, cwd, runId);
|
|
@@ -1574,7 +1646,7 @@ export class OrcaDriver {
|
|
|
1574
1646
|
if (!claim) {
|
|
1575
1647
|
// The receipt's handle may be closed; the reservation is tombstoned naming that pane so the
|
|
1576
1648
|
// next narrator can prove it gone (or close it) and split afresh.
|
|
1577
|
-
await
|
|
1649
|
+
await tombstoneReservation(path, cwd, runId, token, this.time, { pane: childHandle, runtimeId: env.runtimeId }).catch(() => undefined);
|
|
1578
1650
|
let pane;
|
|
1579
1651
|
try {
|
|
1580
1652
|
await this.closeRecordedPane(cwd, name, childHandle, env.runtimeId);
|
|
@@ -1589,6 +1661,67 @@ export class OrcaDriver {
|
|
|
1589
1661
|
return watchSlot(cwd, name, childHandle);
|
|
1590
1662
|
});
|
|
1591
1663
|
}
|
|
1664
|
+
/** OBS-1172: what an indeterminate split left behind, reconciled from the owner record and Orca's
|
|
1665
|
+
* terminal table — never from the receipt that did not arrive. The reservation's token reaches no
|
|
1666
|
+
* process but the split's own command, so a claim proves that split started a board; the owned
|
|
1667
|
+
* record (this driver, the watch title) is ours by that same token. The claimant must be live (pid
|
|
1668
|
+
* and arm presence), and it must name the terminal it runs in — a listed pane in the launching tab.
|
|
1669
|
+
* A claim that names none proves a board started, not WHICH pane hosts it: a sole neighbouring pane
|
|
1670
|
+
* (row titles are shell-controlled, never ownership keys) is not adopted, so the claim stays held.
|
|
1671
|
+
* No claim and no such pane: the split started nothing the table shows. Anything else is held. */
|
|
1672
|
+
async settleIndeterminate(cwd, path, runId, token, launchingHandle, waitMs) {
|
|
1673
|
+
const deadline = this.time.now() + waitMs;
|
|
1674
|
+
let claim;
|
|
1675
|
+
for (;;) {
|
|
1676
|
+
let check;
|
|
1677
|
+
try {
|
|
1678
|
+
check = readBoard(path, cwd, runId);
|
|
1679
|
+
}
|
|
1680
|
+
catch (error) {
|
|
1681
|
+
return { kind: "held", why: `the owner record is unreadable (${errorText(error)})` };
|
|
1682
|
+
}
|
|
1683
|
+
if (check?.record.token !== token || check.record.retired)
|
|
1684
|
+
return { kind: "held", why: "the owner record changed underneath" };
|
|
1685
|
+
if (boardState(check.record) === "claimed") {
|
|
1686
|
+
claim = check;
|
|
1687
|
+
break;
|
|
1688
|
+
}
|
|
1689
|
+
if (this.time.now() >= deadline)
|
|
1690
|
+
break;
|
|
1691
|
+
await this.time.sleep(20);
|
|
1692
|
+
}
|
|
1693
|
+
let listing;
|
|
1694
|
+
try {
|
|
1695
|
+
listing = await this.listAll(cwd);
|
|
1696
|
+
}
|
|
1697
|
+
catch (error) {
|
|
1698
|
+
return { kind: "held", why: `the terminal table is unreadable (${errorText(error)})` };
|
|
1699
|
+
}
|
|
1700
|
+
const tab = terminalTabId(listing, launchingHandle);
|
|
1701
|
+
if (!tab)
|
|
1702
|
+
return { kind: "held", why: `the launching terminal ${launchingHandle} is not listed` };
|
|
1703
|
+
const named = claim?.record.handle;
|
|
1704
|
+
const panes = terminalRows(listing).map((row) => str(row.handle)).filter((h) => h !== undefined && h !== launchingHandle && terminalTabId(listing, h) === tab && (named === undefined || h === named));
|
|
1705
|
+
if (!claim) {
|
|
1706
|
+
return panes.length === 0 ? { kind: "absent" }
|
|
1707
|
+
: { kind: "held", why: `no observer claimed the reservation and ${panes.length} pane(s) of unknown ownership share the launching tab` };
|
|
1708
|
+
}
|
|
1709
|
+
if (!observerLive(claim.record))
|
|
1710
|
+
return { kind: "held", why: `the observer that claimed it (pid ${claim.record.pid}) is not live` };
|
|
1711
|
+
if (named === undefined) {
|
|
1712
|
+
return { kind: "held", why: `the observer that claimed it (pid ${claim.record.pid}) reports no terminal handle, so no listed pane is proven to host it` };
|
|
1713
|
+
}
|
|
1714
|
+
if (panes.length !== 1)
|
|
1715
|
+
return { kind: "held", why: `its observer reports ${named}, which the launching tab does not list` };
|
|
1716
|
+
const pane = panes[0];
|
|
1717
|
+
try {
|
|
1718
|
+
await casBoard("split", path, claim.raw, { ...claim.record, pane, runtimeId: listing.runtimeId }, this.time);
|
|
1719
|
+
}
|
|
1720
|
+
catch (error) {
|
|
1721
|
+
return { kind: "held", why: `the bind was refused (${errorText(error)})` };
|
|
1722
|
+
}
|
|
1723
|
+
return { kind: "adopted", slot: watchSlot(cwd, claim.record.name, pane) };
|
|
1724
|
+
}
|
|
1592
1725
|
boardPath(cwd, runId) {
|
|
1593
1726
|
return join(cwd, stateDirName(cwd), "supervision", `watch-board.${parseRunId(runId)}.json`);
|
|
1594
1727
|
}
|
|
@@ -1626,13 +1759,28 @@ export class OrcaDriver {
|
|
|
1626
1759
|
async retireAndClose(slot) {
|
|
1627
1760
|
await this.serial(async () => {
|
|
1628
1761
|
const retired = await this.retireBoard("close", slot);
|
|
1629
|
-
|
|
1630
|
-
await stopWatchBoard(retired, this.time);
|
|
1631
|
-
else
|
|
1632
|
-
requestWatchBoardStop(retired);
|
|
1762
|
+
await this.stopObserver(retired);
|
|
1633
1763
|
await this.closeRecordedPane(slot.cwd, slot.name, slot.id, retired.runtimeId);
|
|
1634
1764
|
});
|
|
1635
1765
|
}
|
|
1766
|
+
/** Stop a retired board's observer before its pane is closed. A live observer acknowledges on
|
|
1767
|
+
* injected time; a dead one never can, so it is only asked. OBS-1131: an acknowledgement timeout
|
|
1768
|
+
* is not proof of death — while the pid lives the owned pane stays protected — but a pid that died
|
|
1769
|
+
* during the wait is, and its pane is closed instead of leaked. */
|
|
1770
|
+
async stopObserver(record) {
|
|
1771
|
+
const pid = record.pid;
|
|
1772
|
+
if (typeof pid !== "number" || !pidLive(pid)) {
|
|
1773
|
+
requestWatchBoardStop(record);
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
try {
|
|
1777
|
+
await stopWatchBoard(record, this.time);
|
|
1778
|
+
}
|
|
1779
|
+
catch (error) {
|
|
1780
|
+
if (pidLive(pid) || readWatchBoard(record.repo, record.runId)?.token !== record.token)
|
|
1781
|
+
throw error;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1636
1784
|
/** WB-1 seam: the daemon reports this board lost. "Lost" can be a stale beat or missing presence
|
|
1637
1785
|
* under a still-live owner pid, so it is not proof of a dead observer — retirement keeps close's
|
|
1638
1786
|
* acknowledgement discipline (Leg-2 T9 P1). */
|
|
@@ -1720,7 +1868,7 @@ export class OrcaDriver {
|
|
|
1720
1868
|
const st = this.slots.get(slot.id);
|
|
1721
1869
|
if (!st)
|
|
1722
1870
|
return;
|
|
1723
|
-
if (st.handle) {
|
|
1871
|
+
if (st.handle && !st.swept) {
|
|
1724
1872
|
await this.closeTerminal(st);
|
|
1725
1873
|
}
|
|
1726
1874
|
this.slots.delete(slot.id);
|
|
@@ -1889,6 +2037,13 @@ export class OrcaDriver {
|
|
|
1889
2037
|
recoveries: 0,
|
|
1890
2038
|
recovering: false,
|
|
1891
2039
|
});
|
|
2040
|
+
// A kept slot whose attempt the journal fold already retired (a seed readiness deadline
|
|
2041
|
+
// parks one) can still hold this exact terminal; its own close() must not re-address a
|
|
2042
|
+
// handle this driver just closed, or the run-end sweep latches on the vanished tab.
|
|
2043
|
+
for (const st of this.slots.values()) {
|
|
2044
|
+
if (st.handle === c.paneId && st.runtimeId === env.runtimeId)
|
|
2045
|
+
st.swept = true;
|
|
2046
|
+
}
|
|
1892
2047
|
}
|
|
1893
2048
|
catch { /* vanished, stale, or no longer provably ours — never a blind retry */ }
|
|
1894
2049
|
}
|
package/dist/drivers/types.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface Slot {
|
|
|
5
5
|
cwd: string;
|
|
6
6
|
tabId?: string;
|
|
7
7
|
group?: string;
|
|
8
|
+
readScreen?: () => Promise<string>;
|
|
8
9
|
}
|
|
9
10
|
export type NotifyTier = "routine" | "attention" | "decision";
|
|
10
11
|
export interface NotifyOpts {
|
|
@@ -92,6 +93,7 @@ export interface ExecutorDriver {
|
|
|
92
93
|
focus?(target: FocusTarget): Promise<FocusResult>;
|
|
93
94
|
describe?(slot: Slot): SlotPlacement | Promise<SlotPlacement> | undefined;
|
|
94
95
|
slot(cwd: string, name: string, opts?: SlotOpts): Promise<Slot>;
|
|
96
|
+
adopt?(slot: Slot): Promise<Slot>;
|
|
95
97
|
run(slot: Slot, cmd: string): Promise<void>;
|
|
96
98
|
waitOutput(slot: Slot, pattern: string, timeoutMs: number, opts?: {
|
|
97
99
|
regex?: boolean;
|
package/dist/drivers/types.js
CHANGED
|
@@ -63,8 +63,8 @@ export function canonicalizeLegacyName(name, runId) {
|
|
|
63
63
|
return { role: "worker", taskId: w[1], attempt: Number(w[2]), runId: `run-${w[3]}` };
|
|
64
64
|
const g = GATE_ROLE_RE.exec(name);
|
|
65
65
|
if (g) {
|
|
66
|
-
const
|
|
67
|
-
return { role: g[1], taskId:
|
|
66
|
+
const hop = /-r([1-9])$/.exec(g[2]);
|
|
67
|
+
return { role: g[1], taskId: hop ? g[2].slice(0, -hop[0].length) : g[2], attempt: hop ? Number(hop[1]) : 0, runId };
|
|
68
68
|
}
|
|
69
69
|
if (NARRATOR_RE.test(name))
|
|
70
70
|
return { role: "watch", taskId: "run", attempt: 0, runId };
|
package/dist/eval/canary.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { WorkerAdapter } from "../adapters/types.js";
|
|
2
|
+
import type { Effort } from "../graph/schema.js";
|
|
2
3
|
import { type Fixture } from "./fixtures.js";
|
|
3
4
|
export declare const CANARY_FIXTURE_ID = "canary";
|
|
4
5
|
export interface CanaryJudgeResult {
|
|
@@ -35,7 +36,7 @@ export declare function ensureCanary(fixtures: Fixture[], root: string): Fixture
|
|
|
35
36
|
* is evaluated against the resulting diff. A correct judge returns pass=false; any pass=true
|
|
36
37
|
* verdict is flagged as a judge-integrity breach.
|
|
37
38
|
*/
|
|
38
|
-
export declare function runCanaryJudge(fixture: Fixture, judgeAdapter: WorkerAdapter, model: string): Promise<CanaryJudgeResult>;
|
|
39
|
+
export declare function runCanaryJudge(fixture: Fixture, judgeAdapter: WorkerAdapter, model: string, effort?: Effort): Promise<CanaryJudgeResult>;
|
|
39
40
|
/**
|
|
40
41
|
* Aggregate per-channel qualification totals from fixture-channel results.
|
|
41
42
|
* Results marked as canary (by the supplied predicate, defaulting to isCanaryResult) are excluded
|
package/dist/eval/canary.js
CHANGED
|
@@ -53,7 +53,7 @@ async function applyKnownBadChange(repo) {
|
|
|
53
53
|
* is evaluated against the resulting diff. A correct judge returns pass=false; any pass=true
|
|
54
54
|
* verdict is flagged as a judge-integrity breach.
|
|
55
55
|
*/
|
|
56
|
-
export async function runCanaryJudge(fixture, judgeAdapter, model) {
|
|
56
|
+
export async function runCanaryJudge(fixture, judgeAdapter, model, effort) {
|
|
57
57
|
if (!isCanaryFixture(fixture)) {
|
|
58
58
|
throw new Error(`fixture ${fixture.id} is not the canary fixture`);
|
|
59
59
|
}
|
|
@@ -62,7 +62,7 @@ export async function runCanaryJudge(fixture, judgeAdapter, model) {
|
|
|
62
62
|
const initialCommit = (await shGitOk("git rev-parse HEAD", seeded.repo)).trim();
|
|
63
63
|
try {
|
|
64
64
|
await applyKnownBadChange(seeded.repo);
|
|
65
|
-
const gateResult = await acceptanceGate(task, seeded.repo, initialCommit, { adapter: judgeAdapter, model });
|
|
65
|
+
const gateResult = await acceptanceGate(task, seeded.repo, initialCommit, { adapter: judgeAdapter, model, effort });
|
|
66
66
|
const expectedPass = false;
|
|
67
67
|
const judgePass = gateResult.pass;
|
|
68
68
|
const breach = judgePass === true && expectedPass === false;
|
package/dist/eval/dispatch.js
CHANGED
|
@@ -157,6 +157,7 @@ async function dispatchChannel(task, channel, adapter, promptFile, nonce, fixtur
|
|
|
157
157
|
model: channel.model,
|
|
158
158
|
channel: channel.channel,
|
|
159
159
|
tier: channel.tier,
|
|
160
|
+
...(channel.effort ? { effort: channel.effort } : {}), // OBS-1182: measured at the effort it would run at
|
|
160
161
|
};
|
|
161
162
|
const invocation = adapter.invoke(task, seeded.repo, assignment, { promptFile });
|
|
162
163
|
const run = await sh(invocation.command, seeded.repo, cfg.taskTimeoutMinutes * 60_000);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type WorkerAdapter } from "../adapters/types.js";
|
|
2
|
-
import { type AcceptanceItem, type Task } from "../graph/schema.js";
|
|
2
|
+
import { type Effort, type AcceptanceItem, type Task } from "../graph/schema.js";
|
|
3
3
|
import { type LlmVia } from "./llm.js";
|
|
4
4
|
import type { GateResult } from "./types.js";
|
|
5
5
|
export interface EvidenceCitation {
|
|
@@ -55,6 +55,13 @@ export type AcceptanceCorpusAuditResult = {
|
|
|
55
55
|
};
|
|
56
56
|
};
|
|
57
57
|
export declare function auditAcceptanceCorpus(corpusRoot: string, listedTests: readonly VitestListedTest[]): AcceptanceCorpusAuditResult[];
|
|
58
|
+
/** OBS-1151: one criterion of a parsed judge verdict, as the contradiction check compares it. */
|
|
59
|
+
export interface JudgedCriterion {
|
|
60
|
+
id: string;
|
|
61
|
+
criterion: string;
|
|
62
|
+
met: boolean;
|
|
63
|
+
paths: string[];
|
|
64
|
+
}
|
|
58
65
|
export interface AcceptanceGateOpts {
|
|
59
66
|
testCmd?: string;
|
|
60
67
|
diffCap?: number;
|
|
@@ -62,4 +69,5 @@ export interface AcceptanceGateOpts {
|
|
|
62
69
|
export declare function acceptanceGate(task: Task, worktree: string, baseRef: string, judge: {
|
|
63
70
|
adapter: WorkerAdapter;
|
|
64
71
|
model: string;
|
|
72
|
+
effort?: Effort;
|
|
65
73
|
}, via?: LlmVia, opts?: AcceptanceGateOpts): Promise<GateResult>;
|
package/dist/gates/acceptance.js
CHANGED
|
@@ -7,7 +7,7 @@ import { DEFAULT_DIFF_CAP } from "../config/config.js";
|
|
|
7
7
|
import { renderAcceptanceItem } from "../graph/schema.js";
|
|
8
8
|
import { sh } from "../run/git.js";
|
|
9
9
|
import { checkTaskDiffCaps, fetchTaskDiff, isProtectedEvidence, setAsideReceiptPath } from "./review.js";
|
|
10
|
-
import { appendAnchoredReview, COMPLETION_FAKING_CHECKLIST, extractVerdictJson, generateVerdictNonce, runLlm, verdictNonceLine } from "./llm.js";
|
|
10
|
+
import { appendAnchoredReview, COMPLETION_FAKING_CHECKLIST, extractVerdictJson, generateVerdictNonce, runLlm, SeatLaunchError, verdictNonceLine } from "./llm.js";
|
|
11
11
|
import { classifyVerdictCause } from "./verdict-cause.js";
|
|
12
12
|
import { reviewableLogicDiff } from "./artifact-manifest.js";
|
|
13
13
|
import { classifyFailureOutput } from "./baseline.js";
|
|
@@ -342,7 +342,9 @@ function citesChangedLocation(evidence, changed, diff) {
|
|
|
342
342
|
return evidence.trim().length > 0 && diff.includes(evidence);
|
|
343
343
|
return changed.get(evidence.path)?.has(evidence.line) ?? false;
|
|
344
344
|
}
|
|
345
|
-
export async function acceptanceGate(task, worktree, baseRef,
|
|
345
|
+
export async function acceptanceGate(task, worktree, baseRef,
|
|
346
|
+
// OBS-1182: the judge seat's OWN effort (absent = CLI default) — never the worker's.
|
|
347
|
+
judge, via, opts = {}) {
|
|
346
348
|
// 1. deterministic oracles — exit code decides, fail-closed, zero LLM calls (spec §2, T2).
|
|
347
349
|
// A failure returns here, before any runLlm() call: a judge can never override it.
|
|
348
350
|
const passedDet = [];
|
|
@@ -439,7 +441,20 @@ Each criteria[].criterion MUST be the stable id from the rubric (c1, c2, ...) ex
|
|
|
439
441
|
Each criteria[].evidence MUST be a structured citation {"path", "line"} whose "path" and "line" appear in the "Citable evidence lines" list above (a new-file line number inside a changed hunk); a citation outside every changed hunk or to an untouched file voids the whole verdict.
|
|
440
442
|
The top-level comments array is optional. Use it only for actionable line-anchored feedback.
|
|
441
443
|
`;
|
|
442
|
-
|
|
444
|
+
// OBS-1182: an unset seat passes no effort argument at all — the CLI default, and the call shape callers pin.
|
|
445
|
+
let raw;
|
|
446
|
+
try {
|
|
447
|
+
raw = await (judge.effort
|
|
448
|
+
? runLlm(judge.adapter, judge.model, prompt, worktree, via, JUDGE_TIMEOUT_MS, judge.effort)
|
|
449
|
+
: runLlm(judge.adapter, judge.model, prompt, worktree, via, JUDGE_TIMEOUT_MS));
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
if (!(error instanceof SeatLaunchError))
|
|
453
|
+
throw error;
|
|
454
|
+
// OBS-1168(b): no seat, no verdict — run-gates re-routes the judge or parks infra; never a worker charge.
|
|
455
|
+
return { gate: "acceptance", pass: false, details: `${warn}${detBlock}judge dispatch failed — ${error.message} — failing closed`,
|
|
456
|
+
meta: { unparseable: true, cause: "seat-launch-failed", judge: channelKey({ adapter: judge.adapter.id, model: judge.model }) } };
|
|
457
|
+
}
|
|
443
458
|
const extracted = extractVerdictJson(raw, nonce);
|
|
444
459
|
if (!extracted) {
|
|
445
460
|
const cause = classifyVerdictCause(raw, nonce, "pass");
|
|
@@ -471,5 +486,17 @@ The top-level comments array is optional. Use it only for actionable line-anchor
|
|
|
471
486
|
lines.push("judge verdict pass=false");
|
|
472
487
|
lines.push(...inconsistencies);
|
|
473
488
|
const prose = warn + detBlock + (lines.join("\n") || "judge passed");
|
|
474
|
-
|
|
489
|
+
// OBS-1151: each criterion's canonical text, ruling and the paths its evidence cites. A legacy quote
|
|
490
|
+
// cites no path, so its criterion has no comparable subject (missing evidence is unknown). An
|
|
491
|
+
// inconsistent verdict (a duplicate row ruling both ways, a missing id) carries NO judgment: it is not
|
|
492
|
+
// a comparable subject, and as an adjudicator it can confirm nothing — run-gates parks it infra.
|
|
493
|
+
const judgment = inconsistencies.length ? undefined : v.criteria.flatMap((row) => {
|
|
494
|
+
const index = expectedIds.indexOf(row.criterion);
|
|
495
|
+
if (index < 0)
|
|
496
|
+
return [];
|
|
497
|
+
return [{ id: row.criterion, criterion: renderAcceptanceItem(judgeItems[index]), met: row.met,
|
|
498
|
+
paths: typeof row.evidence === "string" ? [] : [row.evidence.path] }];
|
|
499
|
+
});
|
|
500
|
+
return { gate: "acceptance", pass, details: appendAnchoredReview(prose, extracted),
|
|
501
|
+
meta: { judge: channelKey({ adapter: judge.adapter.id, model: judge.model }), ...(judgment ? { judgment } : {}) } };
|
|
475
502
|
}
|