tickmarkr 2.6.0 → 2.6.2
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 +14 -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 +3 -1
- package/dist/adapters/prompt.js +21 -3
- package/dist/adapters/registry.js +3 -3
- package/dist/adapters/types.d.ts +13 -4
- package/dist/adapters/types.js +16 -0
- package/dist/cli/commands/approve.d.ts +5 -1
- package/dist/cli/commands/approve.js +66 -23
- package/dist/cli/commands/compile.js +13 -3
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +11 -3
- package/dist/cli/commands/fleet.js +71 -8
- package/dist/cli/commands/plan.js +7 -3
- package/dist/cli/commands/report.js +18 -2
- package/dist/cli/commands/resume.js +4 -2
- package/dist/cli/commands/status.js +37 -22
- package/dist/cli/help.d.ts +4 -0
- package/dist/cli/help.js +11 -2
- package/dist/config/config.d.ts +20 -0
- package/dist/config/config.js +47 -8
- package/dist/config/fleet-overlay.d.ts +1 -0
- package/dist/config/fleet-overlay.js +56 -0
- package/dist/drivers/orca.d.ts +26 -1
- package/dist/drivers/orca.js +199 -61
- 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 +2 -1
- package/dist/gates/acceptance.js +7 -2
- package/dist/gates/baseline.d.ts +12 -1
- package/dist/gates/baseline.js +11 -4
- package/dist/gates/cache.d.ts +3 -1
- package/dist/gates/cache.js +10 -3
- package/dist/gates/llm.d.ts +5 -4
- package/dist/gates/llm.js +17 -14
- package/dist/gates/review.d.ts +8 -0
- package/dist/gates/review.js +47 -15
- package/dist/gates/run-gates.d.ts +6 -1
- package/dist/gates/run-gates.js +34 -17
- package/dist/gates/test-manifest.d.ts +14 -0
- package/dist/gates/test-manifest.js +33 -6
- package/dist/graph/graph.d.ts +6 -2
- package/dist/graph/graph.js +15 -4
- 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/role-pick.d.ts +16 -0
- package/dist/route/role-pick.js +15 -0
- package/dist/route/router.d.ts +14 -0
- package/dist/route/router.js +39 -16
- package/dist/run/consult.d.ts +13 -1
- package/dist/run/consult.js +19 -14
- package/dist/run/daemon.d.ts +46 -2
- package/dist/run/daemon.js +964 -190
- package/dist/run/git.d.ts +44 -1
- package/dist/run/git.js +103 -5
- package/dist/run/host-health.d.ts +20 -0
- package/dist/run/host-health.js +64 -0
- package/dist/run/journal.d.ts +126 -3
- package/dist/run/journal.js +418 -36
- package/dist/run/merge.d.ts +3 -1
- package/dist/run/merge.js +3 -2
- package/dist/run/operator-state.d.ts +24 -2
- package/dist/run/operator-state.js +41 -5
- package/dist/run/operator-summary.d.ts +3 -0
- package/dist/run/operator-summary.js +3 -1
- package/dist/run/protocol.d.ts +31 -1
- package/dist/run/protocol.js +3 -1
- package/dist/run/stall.d.ts +38 -2
- package/dist/run/stall.js +276 -6
- package/dist/run/supervision.d.ts +7 -1
- package/dist/run/supervision.js +5 -2
- package/dist/tui/cockpit/board.d.ts +1 -1
- package/dist/tui/cockpit/board.js +30 -22
- package/dist/tui/cockpit/decision-actions.d.ts +8 -5
- package/dist/tui/cockpit/decision-actions.js +55 -32
- package/dist/tui/cockpit/derive.d.ts +2 -0
- package/dist/tui/cockpit/derive.js +17 -2
- package/dist/tui/cockpit/live-runtime.d.ts +10 -0
- package/dist/tui/cockpit/live-runtime.js +50 -3
- package/dist/tui/cockpit/live-store.d.ts +1 -0
- package/dist/tui/cockpit/live-store.js +31 -8
- package/dist/tui/cockpit/run-cockpit.d.ts +3 -0
- package/dist/tui/cockpit/run-cockpit.js +28 -2
- package/dist/tui/cockpit/run-view.d.ts +11 -7
- package/dist/tui/cockpit/run-view.js +69 -15
- package/dist/tui/cockpit/setup-cockpit.d.ts +4 -0
- package/dist/tui/cockpit/setup-cockpit.js +6 -3
- package/dist/tui/ink/fleet-app.d.ts +15 -3
- package/dist/tui/ink/fleet-app.js +91 -22
- package/package.json +2 -1
- package/schema/config.schema.json +818 -0
- package/skills/tickmarkr-loop/SKILL.md +8 -2
- package/skills/tickmarkr-overseer/SKILL.md +85 -6
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +88 -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
|
|
@@ -246,7 +246,12 @@ export function provesCheckout(text, checkout) {
|
|
|
246
246
|
* subshell inside it all start in the checkout and its exit status is the payload's (FX-N02).
|
|
247
247
|
*/
|
|
248
248
|
export function checkoutPrefix(checkout) {
|
|
249
|
-
|
|
249
|
+
// OBS-1168: printf BUILDS the marker from two arguments, so the typed command never carries
|
|
250
|
+
// `TICKMARKR_CHECKOUT ` verbatim. The shell's echo of the typed line is repainted and truncated in
|
|
251
|
+
// the scrollback, which used to leave an INCOMPLETE frame beside the complete printed one and made
|
|
252
|
+
// provesCheckout refuse a correct launch. The printed bytes are exactly checkoutProofLine(checkout).
|
|
253
|
+
const head = CHECKOUT_MARK.slice(0, CHECKOUT_MARK.indexOf("_"));
|
|
254
|
+
return `cd ${shq(checkout)} && printf '%s%s\\n' ${head} ${shq(checkoutProofLine(checkout).slice(head.length))} && sh -c `;
|
|
250
255
|
}
|
|
251
256
|
/** The command a terminal on the tracked worktree runs so that it executes INSIDE the checkout. */
|
|
252
257
|
export function inCheckout(checkout, cmd) {
|
|
@@ -449,6 +454,26 @@ function lockEntryGeneration(entry) {
|
|
|
449
454
|
const legacy = /^pid(?:\.(\d+))?$/.exec(entry);
|
|
450
455
|
return owner ? Number(owner[1]) : legacy ? Number(legacy[1] ?? 0) : undefined;
|
|
451
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
|
+
}
|
|
452
477
|
function pidLive(pid) {
|
|
453
478
|
try {
|
|
454
479
|
process.kill(pid, 0);
|
|
@@ -696,6 +721,26 @@ export class OrcaDriver {
|
|
|
696
721
|
}
|
|
697
722
|
return { id, name: title, cwd: worktree, group: opts?.group };
|
|
698
723
|
}
|
|
724
|
+
/**
|
|
725
|
+
* OBS-1109: bind THIS instance to a terminal an earlier daemon created for an OWNED slot, so a
|
|
726
|
+
* resumed daemon can read the interrupted attempt's pane. Binding needs ownership evidence, never
|
|
727
|
+
* a title or handle alone: exactly one tab on the tracked worktree carries the FULL journaled owned
|
|
728
|
+
* title, it resolves to exactly one terminal, and that terminal's scrollback proves the exact task
|
|
729
|
+
* checkout `slot.cwd` (the frame only tickmarkr's own create command prints). Zero, ambiguous or
|
|
730
|
+
* foreign candidates leave the slot unbound: the throw is the caller's decline. The journaled
|
|
731
|
+
* nonce is then checked by the caller against the trailer this binding reads.
|
|
732
|
+
*/
|
|
733
|
+
async adopt(slot) {
|
|
734
|
+
if (!parseOwnedName(slot.name))
|
|
735
|
+
throw new OrcaError("list", `slot ${slot.name} carries no owned title — nothing to adopt`, "");
|
|
736
|
+
const cwd = canonicalWorktreePath(slot.cwd);
|
|
737
|
+
const st = { title: slot.name, cwd, buf: "", recoveries: 0, recovering: false };
|
|
738
|
+
st.tracked = await this.trackedWorktree(cwd);
|
|
739
|
+
await this.recover("adopt", st, undefined, "", { proveCheckout: true });
|
|
740
|
+
const id = `orca-${++this.n}`;
|
|
741
|
+
this.slots.set(id, st);
|
|
742
|
+
return { id, name: slot.name, cwd, ...(slot.group === undefined ? {} : { group: slot.group }) };
|
|
743
|
+
}
|
|
699
744
|
/** Where to invoke the CLI for this slot's calls (see OrcaSlotState.dir). */
|
|
700
745
|
cliCwd(st) {
|
|
701
746
|
return st.dir ?? st.cwd;
|
|
@@ -974,7 +1019,7 @@ export class OrcaDriver {
|
|
|
974
1019
|
return { proven: false, reason: error instanceof Error ? error.message : String(error) };
|
|
975
1020
|
}
|
|
976
1021
|
}
|
|
977
|
-
async recover(family, st, newRuntimeId, raw) {
|
|
1022
|
+
async recover(family, st, newRuntimeId, raw, opts) {
|
|
978
1023
|
if (st.recovering)
|
|
979
1024
|
throw this.latched(family, st, "handle recovery re-entered", raw);
|
|
980
1025
|
st.recovering = true;
|
|
@@ -1029,17 +1074,19 @@ export class OrcaDriver {
|
|
|
1029
1074
|
throw this.latched(family, st, `the owned tab resolves to ${panes.length} terminals in ${home}`, env.raw);
|
|
1030
1075
|
}
|
|
1031
1076
|
const handle = panes[0];
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
if (st.tracked !== undefined && st.tracked !== st.cwd) {
|
|
1077
|
+
// Handles are runtime-scoped: the same VALUE under a different runtime proves nothing about
|
|
1078
|
+
// which terminal it addresses. OBS-1108: it is therefore reconciled exactly like any other
|
|
1079
|
+
// replacement plus the checkout proof — never on the handle text (a 2.5.8 latch refused a live,
|
|
1080
|
+
// finished worker for hours) and never adopted without that ownership evidence.
|
|
1081
|
+
const reused = handle === old;
|
|
1082
|
+
if (reused || opts?.proveCheckout || (st.tracked !== undefined && st.tracked !== st.cwd)) {
|
|
1038
1083
|
// FX-N01: a nested checkout shares its worktreePath with every sibling task's terminal, so
|
|
1039
1084
|
// the candidate must prove the checkout itself (read-only) before it is addressed as ours.
|
|
1040
1085
|
const proof = await this.checkoutProven(handle, st.cwd, this.cliCwd(st), env.runtimeId);
|
|
1041
1086
|
if (!proof.proven) {
|
|
1042
|
-
throw this.latched(family, st,
|
|
1087
|
+
throw this.latched(family, st, reused
|
|
1088
|
+
? `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`
|
|
1089
|
+
: `candidate ${handle} does not prove checkout ${st.cwd} (${proof.reason}); the sole same-titled tab on ${home} is not adopted`, env.raw);
|
|
1043
1090
|
}
|
|
1044
1091
|
}
|
|
1045
1092
|
st.handle = handle;
|
|
@@ -1430,13 +1477,29 @@ export class OrcaDriver {
|
|
|
1430
1477
|
}
|
|
1431
1478
|
}
|
|
1432
1479
|
else if (current) {
|
|
1433
|
-
|
|
1480
|
+
let state = boardState(current.record);
|
|
1481
|
+
const { placer, token: heldToken } = current.record;
|
|
1482
|
+
if ((state === "reserved" || state === "claimed") && placer === process.pid && held.has(heldToken)) {
|
|
1483
|
+
// OBS-1172: THIS process held the placement after an indeterminate split receipt. Reconciled
|
|
1484
|
+
// again, never split over: adopted once proven, still held while unknown, and tombstoned
|
|
1485
|
+
// only once proven to have started nothing.
|
|
1486
|
+
const settled = await this.settleIndeterminate(cwd, path, runId, heldToken, launchingHandle, 0);
|
|
1487
|
+
if (settled.kind === "held") {
|
|
1488
|
+
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}`, "");
|
|
1489
|
+
}
|
|
1490
|
+
held.delete(heldToken);
|
|
1491
|
+
if (settled.kind === "adopted")
|
|
1492
|
+
return settled.slot;
|
|
1493
|
+
current = await tombstoneReservation(path, cwd, runId, heldToken, this.time);
|
|
1494
|
+
if (!current)
|
|
1495
|
+
throw new OrcaError("split", `Orca narrator placement remains unresolved for ${name}: the held reservation changed underneath; ${kept}`, "");
|
|
1496
|
+
state = boardState(current.record);
|
|
1497
|
+
}
|
|
1434
1498
|
if (state === "reserved" || state === "claimed") {
|
|
1435
1499
|
// A reservation or claim with no bound pane is the placing narrator's normal intermediate
|
|
1436
1500
|
// state while that narrator lives (awaiting its receipt, listing, claim or bind): refused
|
|
1437
1501
|
// untouched, its observer never asked to stop. Only a DEAD placer (or one unrecorded) makes
|
|
1438
1502
|
// it a crash to recover.
|
|
1439
|
-
const { placer } = current.record;
|
|
1440
1503
|
if (typeof placer !== "number" || pidLive(placer)) {
|
|
1441
1504
|
throw new OrcaError("split", `Orca narrator placement remains unresolved for ${name} (${state}, no bound pane) while placing narrator ${placer ?? "unrecorded"} lives; ${kept}`, "");
|
|
1442
1505
|
}
|
|
@@ -1445,14 +1508,11 @@ export class OrcaDriver {
|
|
|
1445
1508
|
// The placing driver crashed between the observer's claim and its bind. The record is
|
|
1446
1509
|
// replaced only once that observer is proven gone: dead outright, or live and stopped with
|
|
1447
1510
|
// its acknowledgement awaited on injected time — unacknowledged, it is refused untouched.
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
}
|
|
1453
|
-
catch (error) {
|
|
1454
|
-
throw new OrcaError("split", `Orca narrator placement refused for ${name}: claimed board's live observer unacknowledged; ${kept}; ${errorText(error)}`, "");
|
|
1455
|
-
}
|
|
1511
|
+
try {
|
|
1512
|
+
await this.stopObserver(current.record);
|
|
1513
|
+
}
|
|
1514
|
+
catch (error) {
|
|
1515
|
+
throw new OrcaError("split", `Orca narrator placement refused for ${name}: claimed board's live observer unacknowledged; ${kept}; ${errorText(error)}`, "");
|
|
1456
1516
|
}
|
|
1457
1517
|
const retired = { ...current.record, retired: true };
|
|
1458
1518
|
current = { raw: await casBoard("split", path, current.raw, retired, this.time), record: retired };
|
|
@@ -1466,10 +1526,13 @@ export class OrcaDriver {
|
|
|
1466
1526
|
if (state === "bound") {
|
|
1467
1527
|
// The bound record is the durable truth of placement: the child handle PLUS the split
|
|
1468
1528
|
// envelope's runtime identity. A handle listed by a later runtime is a different pane.
|
|
1469
|
-
|
|
1529
|
+
// OBS-1131: it is adopted only while its observer lives — a resumed daemon retires the
|
|
1530
|
+
// board of a dead observer here (tombstone, stop request, handle-bound close) instead of
|
|
1531
|
+
// answering it and later spending a reopen to discover the death.
|
|
1532
|
+
if (observerLive(current.record) && listedOnRuntime(await this.listAll(cwd), current.record.pane, current.record.runtimeId)) {
|
|
1470
1533
|
return watchSlot(cwd, name, current.record.pane);
|
|
1471
1534
|
}
|
|
1472
|
-
// Lost
|
|
1535
|
+
// Lost or dead: tombstone before anything else.
|
|
1473
1536
|
const retired = { ...current.record, retired: true };
|
|
1474
1537
|
current = { raw: await casBoard("split", path, current.raw, retired, this.time), record: retired };
|
|
1475
1538
|
}
|
|
@@ -1477,12 +1540,7 @@ export class OrcaDriver {
|
|
|
1477
1540
|
// observer must stop and ack on injected time before the handle-bound close; timeout keeps
|
|
1478
1541
|
// the tombstone and does not replace it.
|
|
1479
1542
|
try {
|
|
1480
|
-
|
|
1481
|
-
await stopWatchBoard(current.record, this.time);
|
|
1482
|
-
}
|
|
1483
|
-
else {
|
|
1484
|
-
requestWatchBoardStop(current.record);
|
|
1485
|
-
}
|
|
1543
|
+
await this.stopObserver(current.record);
|
|
1486
1544
|
}
|
|
1487
1545
|
catch (error) {
|
|
1488
1546
|
throw new OrcaError("split", `Orca narrator placement refused for ${name}: retired board observer unacknowledged; ${kept}; ${errorText(error)}`, "");
|
|
@@ -1498,37 +1556,43 @@ export class OrcaDriver {
|
|
|
1498
1556
|
// judged replaceable above.
|
|
1499
1557
|
const token = randomUUID();
|
|
1500
1558
|
await casBoard("split", path, current?.raw, { repo: realpathSync(cwd), runId, driver: this.id, workspace: ORCA_SPACE, pane: "", name, token, placer: process.pid }, this.time);
|
|
1501
|
-
//
|
|
1502
|
-
//
|
|
1503
|
-
//
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1559
|
+
// OBS-1172: a split whose receipt never arrived intact — a transport failure or timeout, an
|
|
1560
|
+
// unparseable or handle-less body, a child the listing does not place in the launching tab —
|
|
1561
|
+
// may still have started the board. It is reconciled before any failure: a uniquely proven
|
|
1562
|
+
// live pane is adopted and bound; a split proven to have started nothing is tombstoned so the
|
|
1563
|
+
// next call splits afresh; anything else is HELD — the reservation stays, and no later call
|
|
1564
|
+
// splits a second board over it.
|
|
1565
|
+
const indeterminate = async (reason, raw) => {
|
|
1566
|
+
const settled = await this.settleIndeterminate(cwd, path, runId, token, launchingHandle, CLAIM_WINDOW_MS);
|
|
1567
|
+
if (settled.kind === "adopted")
|
|
1568
|
+
return settled.slot;
|
|
1569
|
+
if (settled.kind === "absent") {
|
|
1570
|
+
await tombstoneReservation(path, cwd, runId, token, this.time).catch(() => undefined);
|
|
1571
|
+
throw new OrcaError("split", `Orca narrator placement failed for ${name}: ${reason}; ${kept}`, raw);
|
|
1510
1572
|
}
|
|
1511
|
-
|
|
1573
|
+
held.add(token);
|
|
1574
|
+
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);
|
|
1512
1575
|
};
|
|
1513
1576
|
// Exactly one terminal split of the launching handle, horizontal, carrying token and command.
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1577
|
+
let env;
|
|
1578
|
+
try {
|
|
1579
|
+
env = await this.call("split", [
|
|
1580
|
+
"terminal", "split",
|
|
1581
|
+
"--terminal", launchingHandle,
|
|
1582
|
+
"--direction", "horizontal",
|
|
1583
|
+
"--command", `${WATCH_OWNER_ENV}=${shq(token)} ${command}`,
|
|
1584
|
+
], cwd);
|
|
1585
|
+
}
|
|
1586
|
+
catch (error) {
|
|
1587
|
+
return await indeterminate(`split receipt is unknown (${error instanceof OrcaError ? error.reason : errorText(error)})`, error instanceof OrcaError ? error.raw : "");
|
|
1588
|
+
}
|
|
1589
|
+
const malformed = "split receipt is unknown, malformed or handle-less";
|
|
1525
1590
|
let child;
|
|
1526
1591
|
try {
|
|
1527
1592
|
child = splitReceipt(env);
|
|
1528
1593
|
}
|
|
1529
1594
|
catch {
|
|
1530
|
-
await
|
|
1531
|
-
throw new OrcaError("split", `Orca narrator placement failed for ${name}: split receipt is unknown, malformed or handle-less; ${kept}`, env.raw);
|
|
1595
|
+
return await indeterminate(malformed, env.raw);
|
|
1532
1596
|
}
|
|
1533
1597
|
// Parent tabId must name the launching terminal's tab, and the child must actually appear
|
|
1534
1598
|
// there. A handle-only object, a tabId for some other tab, or a handle the list does not
|
|
@@ -1538,21 +1602,19 @@ export class OrcaDriver {
|
|
|
1538
1602
|
listing = await this.listAll(cwd);
|
|
1539
1603
|
}
|
|
1540
1604
|
catch (error) {
|
|
1541
|
-
await
|
|
1542
|
-
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);
|
|
1605
|
+
return await indeterminate(malformed, error instanceof OrcaError ? error.raw : env.raw);
|
|
1543
1606
|
}
|
|
1544
1607
|
const launchingTabId = terminalTabId(listing, launchingHandle);
|
|
1545
1608
|
if (!launchingTabId
|
|
1546
1609
|
|| child.tabId !== launchingTabId
|
|
1547
1610
|
|| child.handle === launchingHandle
|
|
1548
1611
|
|| terminalTabId(listing, child.handle) !== launchingTabId) {
|
|
1549
|
-
await
|
|
1550
|
-
throw new OrcaError("split", `Orca narrator placement failed for ${name}: split receipt is unknown, malformed or handle-less; ${kept}`, env.raw);
|
|
1612
|
+
return await indeterminate(malformed, env.raw);
|
|
1551
1613
|
}
|
|
1552
1614
|
const childHandle = child.handle;
|
|
1553
1615
|
// The narrator writes nothing until the observer's single claim is visible, so neither write
|
|
1554
1616
|
// can erase the other; after it the observer never writes the record again.
|
|
1555
|
-
const claimDeadline = this.time.now() +
|
|
1617
|
+
const claimDeadline = this.time.now() + CLAIM_WINDOW_MS;
|
|
1556
1618
|
let claim;
|
|
1557
1619
|
for (;;) {
|
|
1558
1620
|
const check = readBoard(path, cwd, runId);
|
|
@@ -1569,7 +1631,7 @@ export class OrcaDriver {
|
|
|
1569
1631
|
if (!claim) {
|
|
1570
1632
|
// The receipt's handle may be closed; the reservation is tombstoned naming that pane so the
|
|
1571
1633
|
// next narrator can prove it gone (or close it) and split afresh.
|
|
1572
|
-
await
|
|
1634
|
+
await tombstoneReservation(path, cwd, runId, token, this.time, { pane: childHandle, runtimeId: env.runtimeId }).catch(() => undefined);
|
|
1573
1635
|
let pane;
|
|
1574
1636
|
try {
|
|
1575
1637
|
await this.closeRecordedPane(cwd, name, childHandle, env.runtimeId);
|
|
@@ -1584,6 +1646,67 @@ export class OrcaDriver {
|
|
|
1584
1646
|
return watchSlot(cwd, name, childHandle);
|
|
1585
1647
|
});
|
|
1586
1648
|
}
|
|
1649
|
+
/** OBS-1172: what an indeterminate split left behind, reconciled from the owner record and Orca's
|
|
1650
|
+
* terminal table — never from the receipt that did not arrive. The reservation's token reaches no
|
|
1651
|
+
* process but the split's own command, so a claim proves that split started a board; the owned
|
|
1652
|
+
* record (this driver, the watch title) is ours by that same token. The claimant must be live (pid
|
|
1653
|
+
* and arm presence), and it must name the terminal it runs in — a listed pane in the launching tab.
|
|
1654
|
+
* A claim that names none proves a board started, not WHICH pane hosts it: a sole neighbouring pane
|
|
1655
|
+
* (row titles are shell-controlled, never ownership keys) is not adopted, so the claim stays held.
|
|
1656
|
+
* No claim and no such pane: the split started nothing the table shows. Anything else is held. */
|
|
1657
|
+
async settleIndeterminate(cwd, path, runId, token, launchingHandle, waitMs) {
|
|
1658
|
+
const deadline = this.time.now() + waitMs;
|
|
1659
|
+
let claim;
|
|
1660
|
+
for (;;) {
|
|
1661
|
+
let check;
|
|
1662
|
+
try {
|
|
1663
|
+
check = readBoard(path, cwd, runId);
|
|
1664
|
+
}
|
|
1665
|
+
catch (error) {
|
|
1666
|
+
return { kind: "held", why: `the owner record is unreadable (${errorText(error)})` };
|
|
1667
|
+
}
|
|
1668
|
+
if (check?.record.token !== token || check.record.retired)
|
|
1669
|
+
return { kind: "held", why: "the owner record changed underneath" };
|
|
1670
|
+
if (boardState(check.record) === "claimed") {
|
|
1671
|
+
claim = check;
|
|
1672
|
+
break;
|
|
1673
|
+
}
|
|
1674
|
+
if (this.time.now() >= deadline)
|
|
1675
|
+
break;
|
|
1676
|
+
await this.time.sleep(20);
|
|
1677
|
+
}
|
|
1678
|
+
let listing;
|
|
1679
|
+
try {
|
|
1680
|
+
listing = await this.listAll(cwd);
|
|
1681
|
+
}
|
|
1682
|
+
catch (error) {
|
|
1683
|
+
return { kind: "held", why: `the terminal table is unreadable (${errorText(error)})` };
|
|
1684
|
+
}
|
|
1685
|
+
const tab = terminalTabId(listing, launchingHandle);
|
|
1686
|
+
if (!tab)
|
|
1687
|
+
return { kind: "held", why: `the launching terminal ${launchingHandle} is not listed` };
|
|
1688
|
+
const named = claim?.record.handle;
|
|
1689
|
+
const panes = terminalRows(listing).map((row) => str(row.handle)).filter((h) => h !== undefined && h !== launchingHandle && terminalTabId(listing, h) === tab && (named === undefined || h === named));
|
|
1690
|
+
if (!claim) {
|
|
1691
|
+
return panes.length === 0 ? { kind: "absent" }
|
|
1692
|
+
: { kind: "held", why: `no observer claimed the reservation and ${panes.length} pane(s) of unknown ownership share the launching tab` };
|
|
1693
|
+
}
|
|
1694
|
+
if (!observerLive(claim.record))
|
|
1695
|
+
return { kind: "held", why: `the observer that claimed it (pid ${claim.record.pid}) is not live` };
|
|
1696
|
+
if (named === undefined) {
|
|
1697
|
+
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` };
|
|
1698
|
+
}
|
|
1699
|
+
if (panes.length !== 1)
|
|
1700
|
+
return { kind: "held", why: `its observer reports ${named}, which the launching tab does not list` };
|
|
1701
|
+
const pane = panes[0];
|
|
1702
|
+
try {
|
|
1703
|
+
await casBoard("split", path, claim.raw, { ...claim.record, pane, runtimeId: listing.runtimeId }, this.time);
|
|
1704
|
+
}
|
|
1705
|
+
catch (error) {
|
|
1706
|
+
return { kind: "held", why: `the bind was refused (${errorText(error)})` };
|
|
1707
|
+
}
|
|
1708
|
+
return { kind: "adopted", slot: watchSlot(cwd, claim.record.name, pane) };
|
|
1709
|
+
}
|
|
1587
1710
|
boardPath(cwd, runId) {
|
|
1588
1711
|
return join(cwd, stateDirName(cwd), "supervision", `watch-board.${parseRunId(runId)}.json`);
|
|
1589
1712
|
}
|
|
@@ -1621,13 +1744,28 @@ export class OrcaDriver {
|
|
|
1621
1744
|
async retireAndClose(slot) {
|
|
1622
1745
|
await this.serial(async () => {
|
|
1623
1746
|
const retired = await this.retireBoard("close", slot);
|
|
1624
|
-
|
|
1625
|
-
await stopWatchBoard(retired, this.time);
|
|
1626
|
-
else
|
|
1627
|
-
requestWatchBoardStop(retired);
|
|
1747
|
+
await this.stopObserver(retired);
|
|
1628
1748
|
await this.closeRecordedPane(slot.cwd, slot.name, slot.id, retired.runtimeId);
|
|
1629
1749
|
});
|
|
1630
1750
|
}
|
|
1751
|
+
/** Stop a retired board's observer before its pane is closed. A live observer acknowledges on
|
|
1752
|
+
* injected time; a dead one never can, so it is only asked. OBS-1131: an acknowledgement timeout
|
|
1753
|
+
* is not proof of death — while the pid lives the owned pane stays protected — but a pid that died
|
|
1754
|
+
* during the wait is, and its pane is closed instead of leaked. */
|
|
1755
|
+
async stopObserver(record) {
|
|
1756
|
+
const pid = record.pid;
|
|
1757
|
+
if (typeof pid !== "number" || !pidLive(pid)) {
|
|
1758
|
+
requestWatchBoardStop(record);
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
try {
|
|
1762
|
+
await stopWatchBoard(record, this.time);
|
|
1763
|
+
}
|
|
1764
|
+
catch (error) {
|
|
1765
|
+
if (pidLive(pid) || readWatchBoard(record.repo, record.runId)?.token !== record.token)
|
|
1766
|
+
throw error;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1631
1769
|
/** WB-1 seam: the daemon reports this board lost. "Lost" can be a stale beat or missing presence
|
|
1632
1770
|
* under a still-live owner pid, so it is not proof of a dead observer — retirement keeps close's
|
|
1633
1771
|
* acknowledgement discipline (Leg-2 T9 P1). */
|
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 {
|
|
@@ -62,4 +62,5 @@ export interface AcceptanceGateOpts {
|
|
|
62
62
|
export declare function acceptanceGate(task: Task, worktree: string, baseRef: string, judge: {
|
|
63
63
|
adapter: WorkerAdapter;
|
|
64
64
|
model: string;
|
|
65
|
+
effort?: Effort;
|
|
65
66
|
}, via?: LlmVia, opts?: AcceptanceGateOpts): Promise<GateResult>;
|
package/dist/gates/acceptance.js
CHANGED
|
@@ -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,10 @@ 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
|
+
const raw = await (judge.effort
|
|
446
|
+
? runLlm(judge.adapter, judge.model, prompt, worktree, via, JUDGE_TIMEOUT_MS, judge.effort)
|
|
447
|
+
: runLlm(judge.adapter, judge.model, prompt, worktree, via, JUDGE_TIMEOUT_MS));
|
|
443
448
|
const extracted = extractVerdictJson(raw, nonce);
|
|
444
449
|
if (!extracted) {
|
|
445
450
|
const cause = classifyVerdictCause(raw, nonce, "pass");
|
package/dist/gates/baseline.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export declare function beginGateEvidence(cwd: string, gate: string, command: st
|
|
|
47
47
|
subjectCommit: string | null;
|
|
48
48
|
};
|
|
49
49
|
termination: {
|
|
50
|
-
kind: "unknown" | "signal" | "not-started" | "exit"
|
|
50
|
+
kind: "unknown" | "timeout" | "signal" | "not-started" | "exit";
|
|
51
51
|
exitCode: number | null;
|
|
52
52
|
signal: string | null;
|
|
53
53
|
timedOut: boolean | null;
|
|
@@ -127,9 +127,19 @@ export interface BaselineFileDuration {
|
|
|
127
127
|
file: string;
|
|
128
128
|
durationMs: number;
|
|
129
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* OBS-1123: WHICH capture a forgiveness rests on — the commit it measured and when it was published.
|
|
132
|
+
* Stamped by the daemon beside the measurement; absent on every baseline.json written before it, which
|
|
133
|
+
* readers must render as unknown provenance and never date from any other clock.
|
|
134
|
+
*/
|
|
135
|
+
export interface BaselineProvenance {
|
|
136
|
+
baseRef: string;
|
|
137
|
+
capturedAt: string;
|
|
138
|
+
}
|
|
130
139
|
export interface Baseline {
|
|
131
140
|
/** No command ran because the dependency inventory could not establish isolation. */
|
|
132
141
|
refusal?: string;
|
|
142
|
+
provenance?: BaselineProvenance;
|
|
133
143
|
/** Observational capture receipts stay outside the forgiveness-bearing command entries. */
|
|
134
144
|
evidenceReceipts?: Record<string, GateEvidenceReceipt>;
|
|
135
145
|
commands: Record<string, BaselineCommand>;
|
|
@@ -156,6 +166,7 @@ export declare function fingerprint(output: string): string[];
|
|
|
156
166
|
export declare function freshFailures(entry: BaselineCommand | undefined, raw: string): {
|
|
157
167
|
failing: string[];
|
|
158
168
|
unreadable: boolean;
|
|
169
|
+
forgiven: string[];
|
|
159
170
|
};
|
|
160
171
|
export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
|
|
161
172
|
/**
|
package/dist/gates/baseline.js
CHANGED
|
@@ -428,7 +428,9 @@ export function freshFailures(entry, raw) {
|
|
|
428
428
|
// OBS-42: diagnostic headings enrich fingerprints but cannot invalidate legacy baselines.
|
|
429
429
|
const current = fingerprint(raw);
|
|
430
430
|
const fresh = current.filter((f) => !known.has(f) && (!FAIL_ANCHOR_RE.test(f) || f.startsWith("FAIL ")));
|
|
431
|
-
|
|
431
|
+
// OBS-1123: the baseline-recorded half, named so a reader never has to infer it from the fresh half.
|
|
432
|
+
return { failing: fresh.filter((f) => f !== UNRECOGNIZED_FAILURE), unreadable: current.includes(UNRECOGNIZED_FAILURE),
|
|
433
|
+
forgiven: current.filter((f) => known.has(f)) };
|
|
432
434
|
}
|
|
433
435
|
/**
|
|
434
436
|
* Operator directive 2026-08-12 (D-OBS-10): tickmarkr is package-manager agnostic.
|
|
@@ -880,7 +882,10 @@ export async function compareToBaseline(cwd, commands, baseline, enabled, opts =
|
|
|
880
882
|
// below rather than reading as a verified green. Raise the ceiling by teaching isFailureShaped
|
|
881
883
|
// that runner's position rule (leading verdict + identifier, or identifier + separator + trailing
|
|
882
884
|
// verdict); loosening back to vocabulary re-opens OBS-278.
|
|
883
|
-
const { failing, unreadable } = freshFailures(entry, raw);
|
|
885
|
+
const { failing, unreadable, forgiven } = freshFailures(entry, raw);
|
|
886
|
+
// OBS-1123: which baseline-recorded fingerprints this verdict carried, and the capture that
|
|
887
|
+
// recorded them. Observational only — nothing below reads it to decide a verdict.
|
|
888
|
+
const provenance = baseline.provenance ? { baselineProvenance: baseline.provenance } : {};
|
|
884
889
|
// T9: classify the FRESH diff before charging it. The complete runner output can legitimately
|
|
885
890
|
// contain a baseline-recorded assertion beside a newly introduced infrastructure death; letting
|
|
886
891
|
// that known assertion outvote the fresh birpc line turns machine failure into a worker defect.
|
|
@@ -957,7 +962,9 @@ export async function compareToBaseline(cwd, commands, baseline, enabled, opts =
|
|
|
957
962
|
}
|
|
958
963
|
if (failing.length) {
|
|
959
964
|
const headlined = headlineDetails(raw, failing);
|
|
960
|
-
|
|
965
|
+
// A fresh red beside baseline-recorded ones names both halves, so no reader shows the new one as forgiven.
|
|
966
|
+
const meta = { ...headlined.meta, ...(classification ? { classification } : {}),
|
|
967
|
+
...(forgiven.length ? { freshFingerprints: failing, forgivenFingerprints: forgiven, ...provenance } : {}) };
|
|
961
968
|
record({ gate: name, pass: false, details: headlined.details, ...(Object.keys(meta).length ? { meta } : {}) });
|
|
962
969
|
continue;
|
|
963
970
|
}
|
|
@@ -981,7 +988,7 @@ export async function compareToBaseline(cwd, commands, baseline, enabled, opts =
|
|
|
981
988
|
gate: name,
|
|
982
989
|
pass: true,
|
|
983
990
|
details: `exit ${r.code} but only pre-existing failures (forgiven)${unreadable ? " — no failure shape recognized in this output, so a new failure from this runner is invisible to the baseline gate" : ""}`,
|
|
984
|
-
...(classification ? {
|
|
991
|
+
meta: { ...(classification ? { classification } : {}), forgivenFingerprints: forgiven, ...provenance },
|
|
985
992
|
});
|
|
986
993
|
}
|
|
987
994
|
return results;
|
package/dist/gates/cache.d.ts
CHANGED
|
@@ -36,9 +36,11 @@ export interface VerificationIdentity {
|
|
|
36
36
|
gate?: string;
|
|
37
37
|
/** which verifier produced it: a battery gate row or an integration-tip row (own report, own forgiveness rule) */
|
|
38
38
|
scope?: VerificationScope;
|
|
39
|
-
/** Checkout location is diagnostic metadata;
|
|
39
|
+
/** Checkout location is diagnostic metadata; build outputs additionally require an incarnation. */
|
|
40
40
|
worktree?: string;
|
|
41
41
|
tree: string;
|
|
42
|
+
/** Build success also promises local outputs, which die with the checkout. */
|
|
43
|
+
checkoutIncarnation?: string;
|
|
42
44
|
command: string;
|
|
43
45
|
baseline: string;
|
|
44
46
|
environment: string;
|
package/dist/gates/cache.js
CHANGED
|
@@ -3,7 +3,7 @@ import { execSync } from "node:child_process";
|
|
|
3
3
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join, relative, resolve } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
-
import { describeCapacity, inventoryDependencyLinks, resolvedCapacity, shGit, verificationProtocol } from "../run/git.js";
|
|
6
|
+
import { checkoutIncarnation, describeCapacity, inventoryDependencyLinks, resolvedCapacity, shGit, verificationProtocol } from "../run/git.js";
|
|
7
7
|
import { shq } from "../adapters/types.js";
|
|
8
8
|
export const DEFAULT_VERDICT_CACHE_BOUND = 128;
|
|
9
9
|
let testCacheBound;
|
|
@@ -114,6 +114,9 @@ export async function computeVerificationIdentity(params) {
|
|
|
114
114
|
const tree = params.tree ?? (await getWorktreeTree(params.worktree));
|
|
115
115
|
if (!tree)
|
|
116
116
|
return undefined;
|
|
117
|
+
const incarnation = params.gate === "build" ? checkoutIncarnation(params.worktree) : undefined;
|
|
118
|
+
if (params.gate === "build" && !incarnation)
|
|
119
|
+
return undefined;
|
|
117
120
|
const baseline = baselineIdentity(params.baseline);
|
|
118
121
|
const env = environmentFingerprint({
|
|
119
122
|
worktree: params.worktree,
|
|
@@ -129,6 +132,7 @@ export async function computeVerificationIdentity(params) {
|
|
|
129
132
|
scope: params.scope ?? "battery",
|
|
130
133
|
worktree: realpathSync(params.worktree),
|
|
131
134
|
tree,
|
|
135
|
+
...(incarnation ? { checkoutIncarnation: incarnation } : {}),
|
|
132
136
|
command: params.command,
|
|
133
137
|
baseline,
|
|
134
138
|
environment: env.fingerprint,
|
|
@@ -138,8 +142,10 @@ export async function computeVerificationIdentity(params) {
|
|
|
138
142
|
export function verificationIdentityKey(id) {
|
|
139
143
|
const cmdHash = createHash("sha256").update(id.command).digest("hex").slice(0, 16);
|
|
140
144
|
const gate = id.gate ?? "gate";
|
|
141
|
-
//
|
|
142
|
-
|
|
145
|
+
// Lint/test remain content-addressed. Builds also promise outputs in this physical checkout.
|
|
146
|
+
// Scope continues to separate each verifier's evidence policy.
|
|
147
|
+
const workingTree = createHash("sha256").update(id.tree)
|
|
148
|
+
.update(id.gate === "build" ? `\0checkout:${id.checkoutIncarnation ?? "unbound"}` : "").digest("hex");
|
|
143
149
|
return `${id.scope ?? "battery"}-${gate}-${workingTree}-${cmdHash}-${id.baseline}-${id.environment}`;
|
|
144
150
|
}
|
|
145
151
|
export function formatReusedDetails(originalDetails, id) {
|
|
@@ -179,6 +185,7 @@ export function reusedIdentity(id) {
|
|
|
179
185
|
gate: id.gate ?? "gate",
|
|
180
186
|
scope: id.scope ?? "battery",
|
|
181
187
|
tree: id.tree,
|
|
188
|
+
...(id.checkoutIncarnation ? { checkoutIncarnation: id.checkoutIncarnation } : {}),
|
|
182
189
|
...(id.worktree ? { worktree: id.worktree } : {}),
|
|
183
190
|
command: id.command,
|
|
184
191
|
baseline: id.baseline,
|