tickmarkr 2.5.8 → 2.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/adapters/qwen.js +30 -3
  2. package/dist/cli/commands/approve.js +15 -3
  3. package/dist/cli/commands/beat.d.ts +2 -0
  4. package/dist/cli/commands/beat.js +28 -29
  5. package/dist/cli/commands/compile.js +11 -0
  6. package/dist/cli/commands/fleet.js +61 -53
  7. package/dist/cli/commands/verify.d.ts +4 -1
  8. package/dist/cli/commands/verify.js +16 -5
  9. package/dist/cli/help.d.ts +2 -0
  10. package/dist/cli/help.js +6 -4
  11. package/dist/config/config.d.ts +41 -3
  12. package/dist/config/config.js +48 -17
  13. package/dist/config/fleet-overlay.js +47 -62
  14. package/dist/gates/baseline.d.ts +65 -0
  15. package/dist/gates/baseline.js +163 -9
  16. package/dist/gates/review.d.ts +6 -4
  17. package/dist/gates/review.js +62 -21
  18. package/dist/gates/run-gates.d.ts +4 -1
  19. package/dist/gates/run-gates.js +21 -11
  20. package/dist/gates/test-manifest.d.ts +11 -1
  21. package/dist/gates/test-manifest.js +41 -21
  22. package/dist/run/daemon.js +84 -41
  23. package/dist/run/journal.d.ts +17 -2
  24. package/dist/run/journal.js +99 -11
  25. package/dist/run/merge.d.ts +13 -2
  26. package/dist/run/merge.js +74 -12
  27. package/dist/run/protocol.d.ts +82 -0
  28. package/dist/run/protocol.js +35 -0
  29. package/dist/run/receipt-resolver.d.ts +18 -0
  30. package/dist/run/receipt-resolver.js +132 -0
  31. package/dist/run/supervision.d.ts +14 -1
  32. package/dist/run/supervision.js +122 -24
  33. package/dist/tui/cockpit/evidence-view.d.ts +10 -1
  34. package/dist/tui/cockpit/evidence-view.js +37 -5
  35. package/dist/tui/ink/fleet-app.d.ts +12 -22
  36. package/dist/tui/ink/fleet-app.js +520 -131
  37. package/package.json +1 -1
  38. package/skills/tickmarkr-overseer/SKILL.md +61 -36
  39. package/skills/tickmarkr-overseer/scripts/watch-context.sh +14 -2
@@ -1,5 +1,5 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
2
- import { randomUUID } from "node:crypto";
1
+ import { existsSync, linkSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
2
+ import { createHash, randomUUID } from "node:crypto";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { parseRunId } from "./journal.js";
5
5
  import { stateDirName, tickmarkrDir } from "../graph/graph.js";
@@ -86,6 +86,63 @@ const supervisionDir = (repoRoot) => join(repoRoot, stateDirName(repoRoot), "sup
86
86
  export const supervisionBeatPath = (repoRoot, tier) => join(supervisionDir(repoRoot), `${tier}.beat`);
87
87
  /** Where a watcher records that it STOOD DOWN. Its own file: the beat keeps meaning only "alive". */
88
88
  export const supervisionStandDownPath = (repoRoot, tier) => join(supervisionDir(repoRoot), `${tier}.standdown`);
89
+ export const supervisionArmPath = (repoRoot, tier) => join(supervisionDir(repoRoot), `${tier}.arm`);
90
+ function readArm(repoRoot, tier) {
91
+ try {
92
+ const arm = JSON.parse(readFileSync(supervisionArmPath(repoRoot, tier), "utf8"));
93
+ if (typeof arm.armId !== "string" || !arm.armId.trim() ||
94
+ typeof arm.armEpoch !== "string" || Number.isNaN(Date.parse(arm.armEpoch)) ||
95
+ typeof arm.markerFence !== "string")
96
+ throw new Error("invalid arm");
97
+ return arm;
98
+ }
99
+ catch (error) {
100
+ if (error.code === "ENOENT")
101
+ return undefined;
102
+ throw new Error(`${tier} arm is unreadable`, { cause: error });
103
+ }
104
+ }
105
+ function markerFence(repoRoot, tier) {
106
+ const marker = readStandDown(repoRoot, tier);
107
+ if (marker === "UNREADABLE")
108
+ throw new Error(`${tier} stand-down is unreadable`);
109
+ return marker === "NONE" ? "NONE" : marker.fence;
110
+ }
111
+ /** Explicit arming acknowledges the current marker without deleting it. */
112
+ export function newSupervisionArm(repoRoot, tier, armId = randomUUID(), initial = false) {
113
+ // Acknowledge only the marker observed when arming begins. In particular, do not
114
+ // sample it after stamping the epoch: a stand-down published in that gap is a
115
+ // later act, and must fence this arm rather than be silently acknowledged by it.
116
+ const fence = initial ? "NONE" : markerFence(repoRoot, tier);
117
+ const previous = readArm(repoRoot, tier);
118
+ const arm = {
119
+ armId, armEpoch: new Date(Math.max(Date.now(), previous ? Date.parse(previous.armEpoch) + 1 : 0)).toISOString(),
120
+ markerFence: fence,
121
+ };
122
+ tickmarkrDir(repoRoot);
123
+ const path = supervisionArmPath(repoRoot, tier);
124
+ if (!initial) {
125
+ atomicRecord(path, arm);
126
+ return arm;
127
+ }
128
+ // Only one legacy first tick may install the initial arm. A losing process reuses the winner.
129
+ mkdirSync(dirname(path), { recursive: true });
130
+ const tmp = `${path}.${randomUUID()}.tmp`;
131
+ try {
132
+ writeFileSync(tmp, JSON.stringify(arm) + "\n");
133
+ try {
134
+ linkSync(tmp, path);
135
+ }
136
+ catch (error) {
137
+ if (error.code !== "EEXIST")
138
+ throw error;
139
+ }
140
+ }
141
+ finally {
142
+ rmSync(tmp, { force: true });
143
+ }
144
+ return readArm(repoRoot, tier);
145
+ }
89
146
  /** The independent latch a stand-down cannot overwrite or remove. */
90
147
  const supervisionObligationPath = (repoRoot, tier) => join(supervisionDir(repoRoot), `${tier}.clear-owed`);
91
148
  // SUP-06: PRESENCE — one file per ARMED WATCHER, because a tier may legitimately have more than one.
@@ -190,6 +247,22 @@ function dischargeClearObligation(repoRoot, tier, observation) {
190
247
  rmSync(supervisionObligationPath(repoRoot, tier), { force: true });
191
248
  }
192
249
  }
250
+ function raiseIfCrossed(repoRoot, tier, observation) {
251
+ if (observation.pct >= observation.thresholdPct) {
252
+ raiseClearObligation(repoRoot, tier, observation.armId, observation.thresholdPct);
253
+ }
254
+ }
255
+ // The latch is orthogonal to liveness: a stood-down tier still raises a crossing and still lets a
256
+ // different arm below threshold discharge it. Otherwise the context instrument goes silent after the
257
+ // first hand-off — the watcher script stands its tier down on every exit.
258
+ function observeWithoutBeat(repoRoot, tier, observation) {
259
+ if (observation !== undefined) {
260
+ tickmarkrDir(repoRoot);
261
+ raiseIfCrossed(repoRoot, tier, observation);
262
+ dischargeClearObligation(repoRoot, tier, observation);
263
+ }
264
+ return false;
265
+ }
193
266
  function validateObservation(observation) {
194
267
  if (!observation.armId.trim())
195
268
  throw new Error("a supervision observation needs a non-empty arm identity");
@@ -202,7 +275,7 @@ function validateObservation(observation) {
202
275
  // WRITER — a watcher's own call, on its own tier, every SUPERVISION_BEAT_MS. Never a reader's: the
203
276
  // purity fence (status --watch leaves the state dir byte-identical) is the test that catches a reader
204
277
  // that beats on the watcher's behalf, which would report every dead tier as healthy.
205
- function writeSupervisionBeat(repoRoot, tier, seat, armId, observation) {
278
+ function writeSupervisionBeat(repoRoot, tier, seat, armId, observation, arm, loop = false) {
206
279
  // A seat tier may not be armed anonymously, and the refusal belongs HERE rather than only in the
207
280
  // verb: any caller that could write a seatless record could arm a tier nobody occupies.
208
281
  if (isSeatTier(tier) && !seat?.trim()) {
@@ -215,19 +288,42 @@ function writeSupervisionBeat(repoRoot, tier, seat, armId, observation) {
215
288
  mkdirSync(dirname(p), { recursive: true });
216
289
  // Raise before the beat so a later beat failure cannot hide a duty; discharge only after the
217
290
  // below-threshold observation exists on disk.
218
- if (observation !== undefined && observation.pct >= observation.thresholdPct) {
219
- raiseClearObligation(repoRoot, tier, observation.armId, observation.thresholdPct);
220
- }
221
- writeFileSync(p, JSON.stringify({
291
+ if (observation !== undefined)
292
+ raiseIfCrossed(repoRoot, tier, observation);
293
+ atomicRecord(p, {
222
294
  tier, ...(seat ? { seat } : {}), ...(armId ? { armId } : {}),
223
295
  ...(observation !== undefined ? { pct: observation.pct, thresholdPct: observation.thresholdPct } : {}),
224
- exitedWriterPid: process.pid, beatAt: new Date().toISOString(),
225
- }) + "\n");
296
+ ...arm,
297
+ ...(loop ? { pid: process.pid } : { exitedWriterPid: process.pid }),
298
+ beatAt: new Date().toISOString(),
299
+ });
226
300
  if (observation !== undefined)
227
301
  dischargeClearObligation(repoRoot, tier, observation);
228
302
  }
229
- export function beatSupervision(repoRoot, tier, seat, observation) {
230
- writeSupervisionBeat(repoRoot, tier, seat, undefined, observation);
303
+ export function beatSupervision(repoRoot, tier, seat, observation, options = {}) {
304
+ // `armId` only NAMES a first legacy arm; an existing arm is never refused for a name mismatch —
305
+ // the recorded id is a UUID no operator can know, and a mismatch is not a stand-down.
306
+ if (isSeatTier(tier) && !seat?.trim()) {
307
+ throw new Error(`${tier} is a per-seat tier — a beat must declare the seat identity it speaks for`);
308
+ }
309
+ if (observation !== undefined)
310
+ validateObservation(observation);
311
+ let arm = options.arm ?? readArm(repoRoot, tier);
312
+ if (!arm) {
313
+ if (markerFence(repoRoot, tier) !== "NONE")
314
+ return observeWithoutBeat(repoRoot, tier, observation);
315
+ arm = newSupervisionArm(repoRoot, tier, options.armId ?? observation?.armId, true);
316
+ // A marker published during initial creation must not be acknowledged implicitly.
317
+ if (arm.markerFence !== "NONE")
318
+ return observeWithoutBeat(repoRoot, tier, observation);
319
+ }
320
+ const current = readArm(repoRoot, tier);
321
+ if (current?.armId !== arm.armId || current.armEpoch !== arm.armEpoch ||
322
+ markerFence(repoRoot, tier) !== arm.markerFence)
323
+ return observeWithoutBeat(repoRoot, tier, observation);
324
+ writeSupervisionBeat(repoRoot, tier, seat, arm.armId, observation, arm, options.loop);
325
+ // The immutable fence travels WITH the beat, so a rename racing stand-down cannot outrank it.
326
+ return markerFence(repoRoot, tier) === arm.markerFence;
231
327
  }
232
328
  // THE WATCHER-FACING ENTRY POINT — the loop SUPERVISION_BEAT_MS actually drives. A supervising seat
233
329
  // calls this once at the top of its watch and holds the handle for the duration; a seat that dies,
@@ -348,11 +444,12 @@ function readBeat(repoRoot, tier) {
348
444
  // is not evidence that nobody armed the tier). On a SEAT tier it is the opposite: a record naming no
349
445
  // seat leaves the tier armed and unattributable, which reads as coverage no seat is providing, so
350
446
  // it is UNREADABLE — something is there and no beat any reader can attribute comes out of it.
351
- const { seat, armId } = beatMetadata(p);
447
+ const { seat, armId, markerFence } = beatMetadata(p);
352
448
  if (isSeatTier(tier) && seat === undefined)
353
449
  return "UNREADABLE";
354
450
  return {
355
451
  mtimeMs: st.mtimeMs,
452
+ ...(markerFence !== undefined ? { markerFence } : {}),
356
453
  ...(seat !== undefined ? { seat } : {}),
357
454
  ...(armId !== undefined ? { armId } : {}),
358
455
  };
@@ -363,7 +460,8 @@ function beatMetadata(path) {
363
460
  const rec = JSON.parse(readFileSync(path, "utf8"));
364
461
  const seat = typeof rec?.seat === "string" && rec.seat.trim() ? rec.seat : undefined;
365
462
  const armId = typeof rec?.armId === "string" && rec.armId.trim() ? rec.armId : undefined;
366
- return { ...(seat !== undefined ? { seat } : {}), ...(armId !== undefined ? { armId } : {}) };
463
+ const markerFence = typeof rec?.markerFence === "string" ? rec.markerFence : undefined;
464
+ return { ...(markerFence !== undefined ? { markerFence } : {}), ...(seat !== undefined ? { seat } : {}), ...(armId !== undefined ? { armId } : {}) };
367
465
  }
368
466
  catch {
369
467
  return {};
@@ -414,8 +512,11 @@ function readStandDown(repoRoot, tier) {
414
512
  return "UNREADABLE";
415
513
  let seat;
416
514
  let armId;
515
+ let fence;
417
516
  try {
418
- const rec = JSON.parse(readFileSync(p, "utf8"));
517
+ const bytes = readFileSync(p, "utf8");
518
+ fence = createHash("sha256").update(bytes).digest("hex");
519
+ const rec = JSON.parse(bytes);
419
520
  if (rec?.tier !== tier)
420
521
  return "UNREADABLE";
421
522
  if (typeof rec.disarmedAt !== "string" || Number.isNaN(Date.parse(rec.disarmedAt)))
@@ -431,25 +532,22 @@ function readStandDown(repoRoot, tier) {
431
532
  return "UNREADABLE";
432
533
  } // unparseable or unreadable bytes — not a stand-down anyone can read
433
534
  return {
434
- mtimeMs: st.mtimeMs,
535
+ mtimeMs: st.mtimeMs, fence,
435
536
  ...(seat !== undefined ? { seat } : {}),
436
537
  ...(armId !== undefined ? { armId } : {}),
437
538
  };
438
539
  }
439
- // THE TIER'S STATE — what every surface and every operator reads. A valid stand-down outranks the beat:
440
- // the watcher that wrote it is gone ON PURPOSE, and its last beat ages out exactly like a dead one's
441
- // would. It outranks the beat it FOLLOWED and no other — a later timestamp OR a FRESH different
442
- // armed-watcher identity is another arm, so a marker whose rename lost that race cannot mask a live
443
- // watcher. Once that foreign beat is stale, a newer clean hand-off must win: otherwise overlapping
444
- // boards closed in last-beater-first order would leave the tier reporting a death forever.
540
+ // CLI beats carry the marker generation acknowledged at arm time, never at tick time. A new
541
+ // marker dominates every tick of that arm regardless of rename order or beat mtime. The separate
542
+ // presence-based lifecycle retains overlapping watch owners and their last-owner stand-down.
445
543
  export function supervisionStatus(repoRoot, tier, now = Date.now()) {
446
544
  const beat = readBeat(repoRoot, tier);
447
545
  const standDown = readStandDown(repoRoot, tier);
448
546
  if (standDown === "UNREADABLE")
449
547
  return withClearObligation(repoRoot, tier, { tier, state: "UNREADABLE" });
450
- const beatOutranksStandDown = standDown !== "NONE" && typeof beat === "object" && (beat.mtimeMs > standDown.mtimeMs || (now - beat.mtimeMs <= SUPERVISION_STALE_MS &&
451
- beat.armId !== undefined && standDown.armId !== undefined && beat.armId !== standDown.armId &&
452
- existsSync(supervisionPresencePath(repoRoot, tier, beat.armId))));
548
+ const beatOutranksStandDown = standDown !== "NONE" && typeof beat === "object" && (beat.markerFence !== undefined ? beat.markerFence === standDown.fence :
549
+ beat.armId !== undefined && existsSync(supervisionPresencePath(repoRoot, tier, beat.armId)) && (beat.mtimeMs > standDown.mtimeMs || (now - beat.mtimeMs <= SUPERVISION_STALE_MS &&
550
+ standDown.armId !== undefined && beat.armId !== standDown.armId)));
453
551
  if (standDown !== "NONE" && !beatOutranksStandDown) {
454
552
  // the seat that stood down is named by the marker, falling back to whatever its last beat named
455
553
  const seat = standDown.seat ?? (typeof beat === "object" ? beat.seat : undefined);
@@ -27,6 +27,10 @@ export interface EvidenceRow {
27
27
  /** Durable artifact locations this row's detail names (e.g. a saved raw-review capture); empty
28
28
  * when none were recorded — rendered as "missing", never silently omitted. */
29
29
  readonly artifacts: readonly string[];
30
+ /** The row's execution-evidence receipt, each artifact resolved through the shared resolver against
31
+ * the run root it was minted in (`originRunRoot` for a reused verdict) — reference then verified
32
+ * availability; empty when the row carries no receipt. */
33
+ readonly receipts: readonly string[];
30
34
  readonly taskId?: string;
31
35
  readonly gate?: string;
32
36
  }
@@ -34,7 +38,10 @@ export interface EvidenceRow {
34
38
  export declare function parseArtifactLocations(details: string): string[];
35
39
  /** Pure C2-journal-to-Evidence-rows mapping: the original journal position IS the evidence identity
36
40
  * — assigned once here, from the full unfiltered event list, never recomputed from a display slice. */
37
- export declare function deriveEvidenceJournal(eventsOrRows: readonly (JournalEvent | TrackedJournalRow)[], source?: string): EvidenceRow[];
41
+ export declare function deriveEvidenceJournal(eventsOrRows: readonly (JournalEvent | TrackedJournalRow)[], source?: string, runRoot?: string): EvidenceRow[];
42
+ /** The live cockpit names its journal by absolute path (`<state>/runs/<runId>/journal.jsonl`); that
43
+ * directory IS the run root. A relative or foreign source names no root — nothing is guessed. */
44
+ export declare function runRootOf(source: string | undefined): string | undefined;
38
45
  /** The most recent review verdict recorded for a task — by original journal position, so a merged
39
46
  * task's review opens the row it actually happened on rather than any on-screen index. */
40
47
  export declare function selectTaskReview(journal: readonly EvidenceRow[], taskId: string): EvidenceRow | undefined;
@@ -42,6 +49,8 @@ export interface EvidenceViewInput {
42
49
  readonly events?: readonly JournalEvent[];
43
50
  readonly rows?: readonly TrackedJournalRow[];
44
51
  readonly source?: string;
52
+ /** The displayed run's root (its journal directory); receipts minted by this run resolve under it. */
53
+ readonly runRoot?: string;
45
54
  readonly costs?: readonly ChannelCost[];
46
55
  /** Caller-supplied Report-tab text (e.g. from `renderMarkdownRecord`/`textReport`) — kept as a pure
47
56
  * input so this leaf never reads config/telemetry itself. */
@@ -1,11 +1,12 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { existsSync, renameSync, statSync, unlinkSync } from "node:fs";
3
- import { basename, dirname, join } from "node:path";
2
+ import { existsSync, realpathSync, renameSync, statSync, unlinkSync } from "node:fs";
3
+ import { basename, dirname, isAbsolute, join } from "node:path";
4
4
  import { Box, useInput } from "ink";
5
5
  import { useEffect, useRef, useState } from "react";
6
6
  import { buildOperatorRecord, formatOperatorRecordRow } from "../../report/operator-record.js";
7
7
  import { EMPTY_OPERATOR_PAGE_SUMMARY, foldOperatorPages, operatorPageRow } from "../../run/operator-page-summary.js";
8
8
  import { formatJournalNarration } from "../../run/journal.js";
9
+ import { formatReceiptResolution, resolveReceipt } from "../../run/receipt-resolver.js";
9
10
  import { BodyText, JournalRowPanel, Panel } from "./components.js";
10
11
  import { cellWidth, sliceCells } from "./width.js";
11
12
  // review.ts:485 emits `; raw saved: <path>) — failing closed`, the path immediately followed by
@@ -30,7 +31,7 @@ function stateFor(e) {
30
31
  }
31
32
  /** Pure C2-journal-to-Evidence-rows mapping: the original journal position IS the evidence identity
32
33
  * — assigned once here, from the full unfiltered event list, never recomputed from a display slice. */
33
- export function deriveEvidenceJournal(eventsOrRows, source = "journal.jsonl") {
34
+ export function deriveEvidenceJournal(eventsOrRows, source = "journal.jsonl", runRoot) {
34
35
  const result = [];
35
36
  for (let i = 0; i < eventsOrRows.length; i++) {
36
37
  const item = eventsOrRows[i];
@@ -61,6 +62,7 @@ export function deriveEvidenceJournal(eventsOrRows, source = "journal.jsonl") {
61
62
  artifacts.push(p);
62
63
  }
63
64
  const gate = typeof e.data?.gate === "string" ? e.data.gate : (e.event === "review-leg2" ? "review" : undefined);
65
+ const receipts = resolveRowReceipt(e.data, runRoot ?? runRootOf(rowSource));
64
66
  result.push({
65
67
  evidence: { source: rowSource, line, id },
66
68
  time: e.ts,
@@ -68,12 +70,42 @@ export function deriveEvidenceJournal(eventsOrRows, source = "journal.jsonl") {
68
70
  text: formatJournalNarration(e),
69
71
  fullText: details || JSON.stringify(e.data, null, 2),
70
72
  artifacts,
73
+ receipts,
71
74
  ...(e.taskId ? { taskId: e.taskId } : {}),
72
75
  ...(gate ? { gate } : {}),
73
76
  });
74
77
  }
75
78
  return result;
76
79
  }
80
+ /** The live cockpit names its journal by absolute path (`<state>/runs/<runId>/journal.jsonl`); that
81
+ * directory IS the run root. A relative or foreign source names no root — nothing is guessed. */
82
+ export function runRootOf(source) {
83
+ return source && isAbsolute(source) && basename(source) === "journal.jsonl" ? dirname(source) : undefined;
84
+ }
85
+ // A reused verdict's receipt stays bound to the run root that minted it; the current run's root
86
+ // answers only for evidence this run produced. No root known → nothing can be verified. A receipt
87
+ // the schema rejects is still a receipt the row carries: each reference it names goes through the
88
+ // resolver and is shown with its reason, never dropped. D-279: the boundary is the run root — the
89
+ // host's ancestors are canonicalised (macOS `/var` → `/private/var`) but the root's own name is
90
+ // passed as recorded, so a root that IS a symlink stays visible and the resolver refuses it.
91
+ function resolveRowReceipt(data, runRoot) {
92
+ const receipt = data?.evidenceReceipt;
93
+ if (!receipt || typeof receipt !== "object")
94
+ return [];
95
+ const refs = [receipt.stdout, receipt.stderr];
96
+ const recorded = typeof data?.originRunRoot === "string" && data.originRunRoot ? data.originRunRoot : runRoot;
97
+ let root;
98
+ try {
99
+ root = recorded ? join(realpathSync(dirname(recorded)), basename(recorded)) : undefined;
100
+ }
101
+ catch {
102
+ root = undefined;
103
+ }
104
+ return refs.map(ref => {
105
+ const path = typeof ref?.path === "string" ? ref.path : "";
106
+ return formatReceiptResolution(root ? resolveReceipt(ref, root) : { ok: false, path, reason: "missing" });
107
+ });
108
+ }
77
109
  /** The most recent review verdict recorded for a task — by original journal position, so a merged
78
110
  * task's review opens the row it actually happened on rather than any on-screen index. */
79
111
  export function selectTaskReview(journal, taskId) {
@@ -100,7 +132,7 @@ export function deriveEvidenceView(input) {
100
132
  const row = operatorPageRow(event, tracked ? item.line : index + 1, tracked ? item.source ?? source : source);
101
133
  return row ? [row] : [];
102
134
  })).groups,
103
- journal: deriveEvidenceJournal(items, source),
135
+ journal: deriveEvidenceJournal(items, source, input.runRoot),
104
136
  channels: buildOperatorRecord(events, input.costs ?? []),
105
137
  reportLines: input.reportLines ?? [],
106
138
  statsLines: input.statsLines ?? [],
@@ -229,7 +261,7 @@ export function EvidenceView({ model, width, focused = true, focusEvidence, onSe
229
261
  return { id: row.evidence.id, time, state: row.state, text: clipped };
230
262
  });
231
263
  return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsxs(BodyText, { emphasis: "strong", children: ["EVIDENCE ", TABS.map((t) => (t === tab ? `[${TAB_TITLE[t]}]` : TAB_TITLE[t])).join(" "), tab === "journal" ? ` | Follow ${follow ? "on" : "off"}` : "", selectedRow ? ` | selected ${selectedRow.evidence.id}` : ""] }), tab === "journal" && (_jsxs(_Fragment, { children: [_jsx(JournalRowPanel, { title: "RAW JOURNAL", rows: journalRows, selection: selectedIndex >= 0 ? selectedIndex : undefined }), _jsx(Panel, { title: "OPERATOR PAGE GROUPS", children: model.operatorPages.length ? model.operatorPages.map((group, index) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(BodyText, { emphasis: "strong", children: [group.taskId, " \u00B7 ", group.park, " \u00B7 ", group.status] }), _jsxs(BodyText, { children: ["first ", group.firstEvidenceAt] }), _jsxs(BodyText, { children: ["last ", group.lastEvidenceAt] }), _jsxs(BodyText, { children: ["visible ", group.observedCount, " \u00B7 suppressed ", group.suppressedCount, group.rawOnly ? " · raw-only (suppression unrecorded)" : ""] }), _jsx(BodyText, { children: "Raw source lines (\u2191\u2193 select, Enter open):" }), group.lines.map(line => _jsxs(BodyText, { children: ["#L", line] }, line))] }, `${group.key}:${index}`))) : _jsx(BodyText, { emphasis: "dim", children: "no operator pages" }) }), _jsx(Panel, { title: "SELECTED EVIDENCE", children: selectedRow
232
- ? (_jsxs(_Fragment, { children: [_jsxs(BodyText, { emphasis: "dim", children: ["Original journal ", selectedRow.evidence.id] }), selectedRow.fullText.split("\n").map((line, i) => _jsx(BodyText, { children: line }, i)), _jsxs(BodyText, { emphasis: "dim", children: ["Artifacts: ", selectedRow.artifacts.length ? selectedRow.artifacts.join(", ") : "missing"] })] }))
264
+ ? (_jsxs(_Fragment, { children: [_jsxs(BodyText, { emphasis: "dim", children: ["Original journal ", selectedRow.evidence.id] }), selectedRow.fullText.split("\n").map((line, i) => _jsx(BodyText, { children: line }, i)), _jsxs(BodyText, { emphasis: "dim", children: ["Artifacts: ", selectedRow.artifacts.length ? selectedRow.artifacts.join(", ") : "missing"] }), selectedRow.receipts.map((line, i) => _jsxs(BodyText, { emphasis: "dim", children: ["Receipt: ", line] }, `r${i}`))] }))
233
265
  : _jsx(BodyText, { emphasis: "dim", children: "no history" }) })] })), tab === "report" && (_jsx(Panel, { title: "REPORT", children: model.reportLines.length
234
266
  ? model.reportLines.map((line, i) => _jsx(BodyText, { children: line }, i))
235
267
  : _jsx(BodyText, { emphasis: "dim", children: "no history" }) })), tab === "stats" && (_jsx(Panel, { title: "STATS", children: model.statsLines.length
@@ -1,14 +1,10 @@
1
1
  import { type AuthHealth, type WorkerAdapter } from "../../adapters/types.js";
2
- import type { MapEntry, RoutingMode, Tier, TickmarkrConfig } from "../../config/config.js";
2
+ import { type DenyScopeKey, type InitialDenyProps, type MapEntry, type RoutingMode, type StagedDenyKey, type Tier, type TickmarkrConfig } from "../../config/config.js";
3
3
  import { type Shape } from "../../graph/schema.js";
4
4
  export type FleetSteeringKey = "review" | "consult";
5
- export type FleetEditorState = {
6
- denyAdapters: string[];
7
- denyModels: string[];
5
+ export type FleetEditorState = Record<DenyScopeKey, string[]> & {
8
6
  /** OBS-1046: the staged routing.allow complement, distinct from the authored deny lists above */
9
7
  allowOut?: string[];
10
- denyWorkersAdapters: string[];
11
- denyWorkersModels: string[];
12
8
  classifications: FleetClassification[];
13
9
  selectedMode: RoutingMode;
14
10
  map: Record<string, MapEntry>;
@@ -63,15 +59,15 @@ export type FleetModelEvidence = {
63
59
  /** OBS-972/FL-1: a floating alias's resolved concrete model id (e.g. "opus" → "claude-opus-5") */
64
60
  identity?: string;
65
61
  };
66
- /** The four staged deny sets every preview and the staged routing policy are computed from. */
67
- export type FleetStagedDeny = {
68
- adapters: string[];
69
- models: string[];
70
- workersAdapters: string[];
71
- workersModels: string[];
62
+ /** OBS-1099 add.1: the staged deny list of every schema-enumerated scope — keyed by the scope
63
+ * key minus its `deny` prefix (denyWorkersAdapters → workersAdapters) — every preview and the
64
+ * staged routing policy are computed from; a scope the schema gains is a key here, not a hand field. */
65
+ export type FleetStagedDeny = Record<StagedDenyKey, string[]> & {
72
66
  /** OBS-1046: the staged routing.allow complement; absent ⇒ the session's initial one */
73
67
  allowOut?: string[];
74
68
  };
69
+ /** every scope's staged list, empty — the "every policy scope open" probe the picker lifts against */
70
+ export declare const openStagedDeny: () => FleetStagedDeny;
75
71
  export type FleetModelGroup = {
76
72
  adapter: string;
77
73
  vendor?: string;
@@ -112,6 +108,8 @@ export type FleetCandidateOption = {
112
108
  via: string;
113
109
  model: string;
114
110
  };
111
+ /** ranked only after the shape's advisory floor was dropped (candidates.ts) — never lift-eligible */
112
+ belowFloor?: boolean;
115
113
  };
116
114
  export declare function formatDoctorAge(ageMs: number | null): string;
117
115
  export type FleetReach = "in" | "out-workers" | "out-all" | "out-allow" | "unknown";
@@ -123,15 +121,11 @@ export type FleetStagedRouting = {
123
121
  ok: false;
124
122
  error: string;
125
123
  };
126
- export declare function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels, initialAllowOut, initialDenyWorkersAdapters, initialDenyWorkersModels, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, stagedRouting, entry, initialJudge, judgeSeats, viewRows, frameColumns, frameRows, }: {
124
+ export declare function FleetApp({ ageMs, agents, initialAllowOut, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, stagedRouting, entry, initialJudge, judgeSeats, viewRows, frameColumns, frameRows, ...initialDenyProps }: InitialDenyProps & {
127
125
  ageMs: number | null;
128
126
  agents: AgentCli[];
129
- initialDenyAdapters: string[];
130
- initialDenyModels: string[];
131
127
  /** OBS-1046: adapter ids and adapter:model keys routing.allow leaves out of the fleet */
132
128
  initialAllowOut?: string[];
133
- initialDenyWorkersAdapters?: string[];
134
- initialDenyWorkersModels?: string[];
135
129
  modelGroups: FleetModelGroup[];
136
130
  initialMode: RoutingMode;
137
131
  modeOptions: FleetModeOption[];
@@ -163,16 +157,12 @@ export declare function FleetApp({ ageMs, agents, initialDenyAdapters, initialDe
163
157
  /** terminal height — set only on a real TTY so the footer sticks to the bottom */
164
158
  frameRows?: number;
165
159
  }): import("react").JSX.Element;
166
- export declare function runFleetInkEditor({ ageMs, adapters, health, initialDenyAdapters, initialDenyModels, initialAllowOut, initialDenyWorkersAdapters, initialDenyWorkersModels, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, stagedRouting, entry, initialJudge, judgeSeats, initialInput, input, output, debug, }: {
160
+ export declare function runFleetInkEditor({ ageMs, adapters, health, initialAllowOut, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, stagedRouting, entry, initialJudge, judgeSeats, initialInput, input, output, debug, ...initialDenyProps }: InitialDenyProps & {
167
161
  ageMs: number | null;
168
162
  adapters: WorkerAdapter[];
169
163
  health: Record<string, AuthHealth>;
170
- initialDenyAdapters: string[];
171
- initialDenyModels: string[];
172
164
  /** OBS-1046: adapter ids and adapter:model keys routing.allow leaves out of the fleet */
173
165
  initialAllowOut?: string[];
174
- initialDenyWorkersAdapters?: string[];
175
- initialDenyWorkersModels?: string[];
176
166
  modelGroups: FleetModelGroup[];
177
167
  initialMode: RoutingMode;
178
168
  modeOptions: FleetModeOption[];