tickmarkr 2.5.4 → 2.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/adapters/registry.js +6 -1
  2. package/dist/adapters/types.d.ts +3 -0
  3. package/dist/cli/commands/approve.d.ts +1 -0
  4. package/dist/cli/commands/approve.js +54 -6
  5. package/dist/cli/commands/doctor.js +44 -29
  6. package/dist/cli/commands/fleet.js +195 -33
  7. package/dist/cli/commands/init.js +196 -6
  8. package/dist/cli/commands/resume.js +4 -2
  9. package/dist/cli/commands/run.js +11 -2
  10. package/dist/cli/commands/verify.js +1 -1
  11. package/dist/cli/help.d.ts +2 -0
  12. package/dist/cli/help.js +3 -1
  13. package/dist/config/config.d.ts +14 -8
  14. package/dist/config/config.js +29 -26
  15. package/dist/config/fleet-overlay.d.ts +3 -9
  16. package/dist/config/fleet-overlay.js +68 -11
  17. package/dist/config/fleet-why.d.ts +7 -0
  18. package/dist/config/fleet-why.js +5 -0
  19. package/dist/drivers/index.d.ts +15 -1
  20. package/dist/drivers/index.js +38 -10
  21. package/dist/drivers/orca.d.ts +99 -10
  22. package/dist/drivers/orca.js +586 -97
  23. package/dist/gates/baseline.d.ts +3 -0
  24. package/dist/gates/baseline.js +2 -1
  25. package/dist/gates/cache.d.ts +100 -0
  26. package/dist/gates/cache.js +389 -0
  27. package/dist/gates/run-gates.d.ts +3 -0
  28. package/dist/gates/run-gates.js +117 -10
  29. package/dist/gates/test-manifest.d.ts +99 -0
  30. package/dist/gates/test-manifest.js +389 -0
  31. package/dist/gates/test-reporter.d.ts +4 -0
  32. package/dist/gates/test-reporter.js +49 -0
  33. package/dist/route/preference.d.ts +22 -1
  34. package/dist/route/preference.js +123 -25
  35. package/dist/route/router.js +31 -6
  36. package/dist/run/daemon.d.ts +13 -0
  37. package/dist/run/daemon.js +160 -70
  38. package/dist/run/git.d.ts +10 -1
  39. package/dist/run/git.js +43 -9
  40. package/dist/run/journal.d.ts +14 -2
  41. package/dist/run/journal.js +78 -10
  42. package/dist/run/lease.d.ts +14 -0
  43. package/dist/run/lease.js +87 -0
  44. package/dist/run/merge.d.ts +2 -0
  45. package/dist/run/merge.js +91 -3
  46. package/dist/run/operator-state.d.ts +11 -0
  47. package/dist/run/operator-state.js +17 -3
  48. package/dist/tui/cockpit/board.d.ts +96 -0
  49. package/dist/tui/cockpit/board.js +346 -0
  50. package/dist/tui/cockpit/decision-actions.js +2 -0
  51. package/dist/tui/cockpit/layout.d.ts +5 -1
  52. package/dist/tui/cockpit/layout.js +8 -3
  53. package/dist/tui/cockpit/live-runtime.js +71 -21
  54. package/dist/tui/cockpit/run-view.d.ts +7 -5
  55. package/dist/tui/cockpit/run-view.js +12 -11
  56. package/dist/tui/ink/fleet-app.d.ts +41 -27
  57. package/dist/tui/ink/fleet-app.js +204 -31
  58. package/package.json +1 -1
  59. package/skills/tickmarkr-overseer/SKILL.md +173 -113
@@ -9,7 +9,8 @@ import { render, useInput, useStdin } from "ink";
9
9
  import { useLayoutEffect, useSyncExternalStore } from "react";
10
10
  import { createLiveStore } from "./live-store.js";
11
11
  import { HomeView, deriveHomeView, selectNeedsYouTarget } from "./home-view.js";
12
- import { RunView, applyRunViewKey, initialRunViewSession, runGateCells, evidenceLookup, runTaskRow, GATE_CELL_LETTERS } from "./run-view.js";
12
+ import { RunView, applyRunViewKey, initialRunViewSession, runGateCells, evidenceLookup } from "./run-view.js";
13
+ import { renderBoardLines, stripBoardAnsi } from "./board.js";
13
14
  import { EvidenceView, deriveEvidenceView, defaultExportPath, planEvidenceExport, writeEvidenceExport } from "./evidence-view.js";
14
15
  import { ShellGridFlow, ShellJournalSelection } from "./components.js";
15
16
  import { CockpitShell, initialShellState } from "./shell.js";
@@ -18,12 +19,11 @@ import { shellBindings } from "./keys.js";
18
19
  import { createPointerReportReader, POINTER_TRACKING_ON, POINTER_TRACKING_OFF } from "./pointer.js";
19
20
  import { deriveRunDecisions, previewDecision, executeDecision, withDecisionPreview, withDecisionReceipt, decisionConfirmLines } from "./decision-actions.js";
20
21
  import { approvalRunOwner } from "../../cli/commands/approve.js";
21
- import { GATE_NAMES } from "../../graph/schema.js";
22
22
  import { GLYPHS } from "../../brand.js";
23
23
  import { Journal } from "../../run/journal.js";
24
- import { observeNamedRun } from "../../run/supervision.js";
24
+ import { observeNamedRun, WATCH_OWNER_ENV } from "../../run/supervision.js";
25
25
  import { formatOwnedName } from "../../drivers/types.js";
26
- import { cellWidth, fitCells, wrapCells } from "./width.js";
26
+ import { cellWidth, wrapCells } from "./width.js";
27
27
  import { resolveShellColourMode } from "./theme.js";
28
28
  let timelineUsers = 0;
29
29
  let timelineObserver;
@@ -100,7 +100,10 @@ export async function runConsolidatedCockpit(options) {
100
100
  const source = store;
101
101
  if (options.observeRun !== false)
102
102
  observation = observeNamedRun(cwd, runId, options.environment);
103
- let state = { ...initialShellState(), view: options.initialView ?? "home" };
103
+ // BD-1 (RULING-231-19 §2): the daemon-placed board carries the watch owner token and mounts
104
+ // rail-less — a zero shortcut budget the plan honours at every width. The manual cockpit keeps its rail.
105
+ const railless = Boolean((options.environment ?? process.env)[WATCH_OWNER_ENV]);
106
+ let state = { ...initialShellState(), view: options.initialView ?? "home", ...(railless ? { shortcutColumns: 0 } : {}) };
104
107
  let runSession = initialRunViewSession();
105
108
  if (options.initialParks) {
106
109
  runSession = { ...runSession, selection: Math.max(0, source.snapshot().operator.tasks.findIndex(t => t.state === "human" || t.state === "blocked")) };
@@ -172,6 +175,29 @@ export async function runConsolidatedCockpit(options) {
172
175
  state = { ...state, overlay: [result.status === "focused" ? `Focused ${task.id}` : `Pane ${result.status}`, result.reason, "Enter opens task evidence after Esc"], overlayOffset: 0 };
173
176
  publish();
174
177
  };
178
+ // The board is taller than a small body; a selection moved by keys or a park opened from Home
179
+ // scrolls the shell's viewport just far enough that the selected row (and the rows that wrap
180
+ // under it) is seen. The rows are the very lines the Run view draws at this body width.
181
+ const followSelection = () => {
182
+ const snap = source.snapshot();
183
+ const task = snap.operator.tasks[runSession.selection];
184
+ if (!task)
185
+ return;
186
+ const g = geometry();
187
+ const lines = renderBoardLines({ runId, snapshot: snap.operator, graph: snap.graph.value, now: (options.now ?? Date.now)(), selection: task.id, keys: false, colour: false }, g.bodyColumns);
188
+ const first = lines.findIndex(l => l.startsWith(" ❯ "));
189
+ if (first < 0)
190
+ return;
191
+ let last = first;
192
+ while (last + 1 < lines.length && lines[last + 1].startsWith(" ".repeat(9)))
193
+ last++;
194
+ const top = first + 1, bottom = last + 1; // the RUN summary line sits above the board
195
+ const scroll = state.scroll;
196
+ if (top < scroll)
197
+ state = { ...state, scroll: top };
198
+ else if (bottom >= scroll + g.bodyRows)
199
+ state = { ...state, scroll: bottom - g.bodyRows + 1 };
200
+ };
175
201
  const openDetail = () => {
176
202
  const snap = source.snapshot();
177
203
  const task = state.view === "run" ? snap.operator.tasks[runSession.selection] : undefined;
@@ -189,6 +215,7 @@ export async function runConsolidatedCockpit(options) {
189
215
  // from the snapshot. The guard prevents the shell from dispatching its echo.
190
216
  let leafInput;
191
217
  let forwarding = false;
218
+ let lastRawInput = "";
192
219
  const forwardLeaf = (bytes) => {
193
220
  forwarding = true;
194
221
  try {
@@ -347,12 +374,15 @@ export async function runConsolidatedCockpit(options) {
347
374
  const task = snap.operator.tasks[runSession.selection];
348
375
  const cells = task ? runGateCells(task, evidenceLookup(snap.journal.history, source.page)) : [];
349
376
  runSession = applyRunViewKey(runSession, event, { tasks: snap.operator.tasks, decisions, verdictLines: cells[runSession.verdictGate]?.verdict.length ?? 0 }).session;
377
+ if (k.upArrow || k.downArrow)
378
+ followSelection();
350
379
  if (k.pageDown || k.pageUp)
351
380
  state = {
352
381
  ...state,
353
- // The Run leaf owns verdict paging; the shell only moves its
354
- // viewport far enough for that leaf's verdict panel to be seen.
355
- scroll: k.pageDown ? geometry().bodyRows : 0,
382
+ // The Run leaf owns verdict paging; the shell only moves its viewport so that leaf's
383
+ // verdict panel is seen — it sits below the board and the selected task's cells, so the
384
+ // viewport goes to the leaf's end (clamped by the shell), not one page down.
385
+ scroll: k.pageDown ? Number.MAX_SAFE_INTEGER : 0,
356
386
  };
357
387
  if (runSession.decisions.menu)
358
388
  state = { ...state, overlay: ["Actions — Enter reviews; Esc cancels", ...runSession.decisions.menu.verbs], overlayOffset: 0 };
@@ -501,16 +531,35 @@ export async function runConsolidatedCockpit(options) {
501
531
  useSyncExternalStore(listener => { listeners.add(listener); return () => { listeners.delete(listener); }; }, () => revision);
502
532
  const stdinContext = useStdin();
503
533
  useLayoutEffect(() => {
504
- leafInput = stdinContext.internal_eventEmitter;
505
- return () => { leafInput = undefined; };
534
+ const emitter = stdinContext.internal_eventEmitter;
535
+ leafInput = emitter;
536
+ // Ink strips an escape sequence's ESC before useInput sees it, so an unnamed CSI and typed
537
+ // "[..." look alike there. Its parser emits every escape sequence as its own event, so the RAW
538
+ // event says which it was. Registered in a layout effect, this listener runs before useInput's.
539
+ const raw = (data) => { lastRawInput = data; };
540
+ emitter?.on("input", raw);
541
+ return () => { leafInput = undefined; emitter?.off("input", raw); };
506
542
  }, [stdinContext.internal_eventEmitter]);
507
543
  const renderedView = state.view;
508
544
  const leafActive = state.editor === undefined && !state.overlay && !state.help && !state.reviewing;
509
545
  useInput((text, k) => {
510
546
  if (forwarding || (leafActive && renderedView !== "run" && (k.return || (renderedView === "evidence" && text === "e"))))
511
547
  return;
548
+ // OBS-1002: Ink hands a coalesced chunk ("a\r", "na\r" — a paste, key repeat under load, a
549
+ // `pane run` message) to the handler as ONE string with no key flags, and a single-key map
550
+ // ignores it. Split every non-escape chunk into one event per code point; an escape sequence
551
+ // (arrows, page keys) is one key and already carries its flags.
552
+ const flagged = k.ctrl || k.meta || k.upArrow || k.downArrow || k.leftArrow || k.rightArrow || k.pageUp || k.pageDown || k.return || k.escape || k.tab || k.backspace || k.delete;
553
+ const points = !flagged && !lastRawInput.startsWith("\x1b") && [...text].length > 1 ? [...text] : [text];
512
554
  try {
513
- key({ input: text, key: k }, true);
555
+ for (const point of points) {
556
+ const event = points.length === 1 ? { input: text, key: k }
557
+ : point === "\r" || point === "\n" ? { input: "", key: { return: true } }
558
+ : point === "\t" ? { input: "", key: { tab: true } }
559
+ : point === "\x1b" ? { input: "", key: { escape: true } }
560
+ : { input: point, key: { shift: /[A-Z]/.test(point) } };
561
+ key(event, true);
562
+ }
514
563
  }
515
564
  catch (error) {
516
565
  stop(error);
@@ -564,10 +613,9 @@ export async function runConsolidatedCockpit(options) {
564
613
  return _jsx(CockpitShell, { snapshot: options.observeRun === false ? { ...snap, journal: { ...snap.journal, source: "no run/journal.jsonl" } } : snap, state: state, columns: p.columns, rows: p.rows, version: options.binaryVersion, colour: colour, onCommit: plan => {
565
614
  committed = plan;
566
615
  const targets = [];
567
- const taskRows = snap.operator.tasks.map((task, index) => ({
568
- id: task.id,
569
- text: fitCells(`${index === Math.min(runSession.selection, snap.operator.tasks.length - 1) ? `${GLYPHS.pointer} ` : " "}${runTaskRow(task, GATE_NAMES.map(gate => ({ letter: GATE_CELL_LETTERS[task.gates[gate].state] })))}`, Math.max(1, plan.bodyColumns - 4)),
570
- }));
616
+ // A board row opens with the prototype's four-cell indent or the focus marker, then the id;
617
+ // the first painted row naming a task is that task's target (the effort fold names it again, later).
618
+ const boardRowId = (text) => /^(?: {4}| {2}❯ )(\S+)/u.exec(stripBoardAnsi(text))?.[1];
571
619
  const usedTasks = new Set();
572
620
  for (const row of plan.paintedRows) {
573
621
  if (row.row < plan.bodyRow && (row.text.startsWith("1 Home") || row.text.startsWith("1H"))) {
@@ -599,10 +647,12 @@ export async function runConsolidatedCockpit(options) {
599
647
  continue;
600
648
  }
601
649
  if (state.view === "run") {
602
- const task = taskRows.find(task => !usedTasks.has(task.id) && row.text === task.text);
603
- if (task) {
604
- usedTasks.add(task.id);
605
- targets.push({ ...row, kind: "task", id: task.id });
650
+ const drawn = boardRowId(row.text);
651
+ // A narrow body clips the row inside a long id; the drawn prefix still names one task.
652
+ const id = drawn === undefined ? undefined : (snap.operator.tasks.find(task => task.id === drawn) ?? snap.operator.tasks.find(task => task.id.startsWith(drawn)))?.id;
653
+ if (id !== undefined && !usedTasks.has(id)) {
654
+ usedTasks.add(id);
655
+ targets.push({ ...row, kind: "task", id });
606
656
  }
607
657
  }
608
658
  else {
@@ -625,8 +675,8 @@ export async function runConsolidatedCockpit(options) {
625
675
  }
626
676
  }
627
677
  committedTargets = targets;
628
- }, children: state.view === "home" ? _jsx(ShellGridFlow, { width: p.bodyColumns, columns: p.columns < 120 ? 2 : 3, rowHeight: p.rows < 20 ? 1 : p.columns < 120 ? 2 : 3, children: _jsx(HomeView, { width: p.bodyColumns, model: homeModel, focused: focused, onOpenPark: id => { runSession = { ...runSession, selection: Math.max(0, snap.operator.tasks.findIndex(t => t.id === id)) }; state = { ...state, view: "run", scroll: 0 }; publish(); }, onOpenDiagnostic: id => { state = { ...state, overlay: [id], overlayOffset: 0 }; publish(); }, onOpenEvidence: openEvidence, onSelectNeedsYou: reportHomeNeedsYouSelection }, String(homeModel.needsYou.length > 0)) }) :
629
- state.view === "run" ? _jsx(RunView, { snapshot: snap.operator, rows: snap.journal.history, page: source.page, graph: snap.graph.value, decisions: visibleDecisions, session: runSession, columns: p.bodyColumns, run: approvalRunOwner(cwd, runId) }) :
678
+ }, children: state.view === "home" ? _jsx(ShellGridFlow, { width: p.bodyColumns, columns: p.columns < 120 ? 2 : 3, rowHeight: p.rows < 20 ? 1 : p.columns < 120 ? 2 : 3, children: _jsx(HomeView, { width: p.bodyColumns, model: homeModel, focused: focused, onOpenPark: id => { runSession = { ...runSession, selection: Math.max(0, snap.operator.tasks.findIndex(t => t.id === id)) }; state = { ...state, view: "run", scroll: 0 }; followSelection(); publish(); }, onOpenDiagnostic: id => { state = { ...state, overlay: [id], overlayOffset: 0 }; publish(); }, onOpenEvidence: openEvidence, onSelectNeedsYou: reportHomeNeedsYouSelection }, String(homeModel.needsYou.length > 0)) }) :
679
+ state.view === "run" ? _jsx(RunView, { snapshot: snap.operator, rows: snap.journal.history, page: source.page, graph: snap.graph.value, decisions: visibleDecisions, session: runSession, columns: p.bodyColumns, run: approvalRunOwner(cwd, runId), now: options.now }) :
630
680
  _jsx(ShellJournalSelection.Provider, { value: reportEvidenceSelection, children: _jsx(EvidenceView, { width: p.bodyColumns, model: { ...model, journal: evidenceRows }, focusEvidence: selectedEvidence, focused: focused, onExport: beginExport, onSelect: id => { selectedEvidence = id; openDetail(); publish(); } }, `${state.query}:${evidenceNavigation}`) }) });
631
681
  }
632
682
  app = render(_jsx(App, {}), { stdin, stdout: output, debug: options.debug, exitOnCtrlC: false, patchConsole: false });
@@ -1,4 +1,4 @@
1
- import type { ReactElement } from "react";
1
+ import { type ReactElement } from "react";
2
2
  import type { ApprovalRunOwner } from "../../cli/commands/approve.js";
3
3
  import { type GateName, type RunGraph } from "../../graph/schema.js";
4
4
  import type { JournalEvent } from "../../run/journal.js";
@@ -83,8 +83,8 @@ export declare function applyRunViewKey(session: RunViewSession, event: RunViewK
83
83
  * a closed run whose record lacks a bucket says so rather than reading absence as empty.
84
84
  */
85
85
  export declare function runSummaryLine(s: OperatorSnapshot): string;
86
- /** One matrix row: id, state, the seven letters in declaration order, the recorded attempt. */
87
- export declare function runTaskRow(t: OperatorTask, cells: readonly Pick<RunGateCell, "letter">[]): string;
86
+ /** The board's footer keys merged with the cockpit's own — every one of them handled by this view or the shell. */
87
+ export declare const RUN_VIEW_KEYS: readonly string[];
88
88
  export interface RunViewProps {
89
89
  readonly snapshot: OperatorSnapshot;
90
90
  /** Journal rows the store retains (C2 `journal.history`); evidence outside them reaches `page`. */
@@ -95,6 +95,8 @@ export interface RunViewProps {
95
95
  readonly session: RunViewSession;
96
96
  readonly columns: number;
97
97
  readonly run: ApprovalRunOwner;
98
+ /** The clock the board header reads; injectable so a pinned frame does not drift. */
99
+ readonly now?: () => number;
98
100
  }
99
- /** The Run body. Every line is measured through the width module; nothing overflows its column. */
100
- export declare function RunView({ snapshot, rows, page, graph, decisions, session, columns, run }: RunViewProps): ReactElement;
101
+ /** The Run body: the approved board (BD-1) over the fold, then the selected task's detail panels. */
102
+ export declare function RunView({ snapshot, rows, page, graph, decisions, session, columns, run, now }: RunViewProps): ReactElement;
@@ -1,9 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box } from "ink";
3
+ import { useContext } from "react";
3
4
  import { GLYPHS } from "../../brand.js";
4
5
  import { GATE_NAMES } from "../../graph/schema.js";
5
6
  import { normalizeGateOutcome } from "../../run/outcome.js";
6
- import { BodyText, Panel } from "./components.js";
7
+ import { BOARD_KEYS, boardFooter, clipBoard, renderBoardLines } from "./board.js";
8
+ import { BodyText, Panel, ShellTheme } from "./components.js";
7
9
  import { applyDecisionKey, decisionConfirmLines, decisionKeybar, decisionReceiptLines, initialDecisionSession, } from "./decision-actions.js";
8
10
  import { fitCells, wrapCells } from "./width.js";
9
11
  /** Bounded history first, then the store's page accessor for an evicted line. */
@@ -170,15 +172,13 @@ export function runSummaryLine(s) {
170
172
  }
171
173
  const attemptLabel = (t) => (t.attempt !== undefined ? `attempt ${t.attempt}` : t.dispatches === 0 ? "never dispatched" : "attempt unknown");
172
174
  const parkOf = (t) => (t.state === "human" ? `human · ${t.parkKind ?? "unknown kind"}` : t.state);
173
- /** One matrix row: id, state, the seven letters in declaration order, the recorded attempt. */
174
- export function runTaskRow(t, cells) {
175
- return `${t.id.padEnd(4)} ${parkOf(t).padEnd(20)} ${cells.map((c) => c.letter).join(" ")} ${attemptLabel(t)}`;
176
- }
177
- const MATRIX_HEADER = `${"Task".padEnd(4)} ${"state".padEnd(20)} ${GATE_NAMES.map((g) => g.slice(0, 2)).join(" ")} latest attempt`;
178
- const MATRIX_LEGEND = "P pass · F fail · R running · - not run · D disabled · ? unknown | declaration order, not a timeline · acceptance/review concurrent, optional";
179
- /** The Run body. Every line is measured through the width module; nothing overflows its column. */
180
- export function RunView({ snapshot, rows, page, graph, decisions, session, columns, run }) {
175
+ /** The board's footer keys merged with the cockpit's own — every one of them handled by this view or the shell. */
176
+ export const RUN_VIEW_KEYS = [...BOARD_KEYS.slice(0, 2), "←→ verdict gate", "PgUp/PgDn page", "x outcome", "a actions", "o pane", "Tab focus", "? keys", BOARD_KEYS[2]];
177
+ /** The Run body: the approved board (BD-1) over the fold, then the selected task's detail panels. */
178
+ export function RunView({ snapshot, rows, page, graph, decisions, session, columns, run, now = Date.now }) {
181
179
  const inner = Math.max(1, Math.floor(columns) - 4);
180
+ const colour = useContext(ShellTheme).mode !== "none";
181
+ const width = Math.max(1, Math.floor(columns));
182
182
  const fit = (text) => fitCells(text, inner);
183
183
  const decisionLines = (lines) => lines.flatMap((line) => wrapCells(line, inner));
184
184
  const lookup = evidenceLookup(rows, page);
@@ -194,9 +194,10 @@ export function RunView({ snapshot, rows, page, graph, decisions, session, colum
194
194
  const decision = task === undefined ? undefined : decisions.find((d) => d.taskId === task.id);
195
195
  const { menu, confirming, receipt, notice } = session.decisions;
196
196
  const blocked = task === undefined ? [] : graph?.tasks.filter((g) => g.deps.includes(task.id)).map((g) => g.id) ?? decision?.blocks ?? [];
197
- return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsxs(Panel, { title: `RUN / ${snapshot.lifecycle}`, focused: true, children: [_jsx(BodyText, { emphasis: "dim", children: fit(runSummaryLine(snapshot)) }), _jsx(BodyText, { emphasis: "dim", children: fit(` ${MATRIX_HEADER}`) }), tasks.map((t, i) => (_jsx(BodyText, { emphasis: i === selection ? "strong" : "normal", children: fit(`${i === selection ? `${GLYPHS.pointer} ` : " "}${runTaskRow(t, GATE_NAMES.map((gate) => ({ letter: GATE_CELL_LETTERS[t.gates[gate].state] })))}`) }, t.id))), tasks.length === 0 && _jsx(BodyText, { children: fit("no tasks recorded — no plan") }), _jsx(BodyText, { emphasis: "dim", children: fit(MATRIX_LEGEND) })] }), task !== undefined && (_jsxs(Panel, { title: `SELECTED / ${task.id}${task.title ? ` · ${task.title}` : ""}`, children: [_jsx(BodyText, { children: fit(`state ${parkOf(task)} · ${attemptLabel(task)} · dispatches ${task.dispatches} · path ${task.path ?? "unknown"} · pane ${task.pane ?? "unknown"} · alarm ${task.alarmMs === undefined ? "unknown" : `${task.alarmMs}ms`}${task.mergeEvidence ? ` · merged #L${task.mergeEvidence.line}` : ""}`) }), _jsx(BodyText, { emphasis: "dim", children: fit(`outcome filter ${session.outcomeFilter} (x cycles) · ${shown.length} of ${cells.length} gates shown`) }), shown.map((c) => (_jsx(BodyText, { children: fit(`${c.letter} ${c.gate.padEnd(10)} ${c.labels.join(" · ")}${c.line === undefined ? "" : ` · #L${c.line}`}`) }, c.gate)))] })), verdictCell !== undefined && (_jsxs(Panel, { title: `VERDICT / ${verdictCell.gate}${verdictCell.line === undefined ? "" : ` #L${verdictCell.line}`} · ${verdict.length === 0 ? "no verdict text" : `lines ${from + 1}–${from + window.length} of ${verdict.length}`}`, children: [window.map((line, i) => _jsx(BodyText, { children: fit(`${String(from + i + 1).padStart(4)} ${line}`) }, `${from + i}:${line}`)), _jsx(BodyText, { emphasis: "dim", children: fit("←→ gate · PageUp/PageDown page") })] })), task?.state === "human" && (_jsx(Panel, { title: `PARK / ${task.id}`, children: decision === undefined
197
+ const board = renderBoardLines({ runId: run.runId, snapshot, graph, now: now(), live: run.live, selection: task?.id, keys: false, colour }, width);
198
+ return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(BodyText, { emphasis: "dim", children: fitCells(`RUN / ${snapshot.lifecycle} · ${runSummaryLine(snapshot)}`, width) }), board.map((line, i) => _jsx(BodyText, { children: line || " " }, `${i}:${line}`)), tasks.length === 0 && _jsx(BodyText, { children: fitCells("no tasks recorded — no plan", width) }), task !== undefined && (_jsxs(Panel, { title: `SELECTED / ${task.id}${task.title ? ` · ${task.title}` : ""}`, children: [_jsx(BodyText, { children: fit(`state ${parkOf(task)} · ${attemptLabel(task)} · dispatches ${task.dispatches} · path ${task.path ?? "unknown"} · pane ${task.pane ?? "unknown"} · alarm ${task.alarmMs === undefined ? "unknown" : `${task.alarmMs}ms`}${task.mergeEvidence ? ` · merged #L${task.mergeEvidence.line}` : ""}`) }), _jsx(BodyText, { emphasis: "dim", children: fit(`outcome filter ${session.outcomeFilter} (x cycles) · ${shown.length} of ${cells.length} gates shown`) }), shown.map((c) => (_jsx(BodyText, { children: fit(`${c.letter} ${c.gate.padEnd(10)} ${c.labels.join(" · ")}${c.line === undefined ? "" : ` · #L${c.line}`}`) }, c.gate)))] })), verdictCell !== undefined && (_jsxs(Panel, { title: `VERDICT / ${verdictCell.gate}${verdictCell.line === undefined ? "" : ` #L${verdictCell.line}`} · ${verdict.length === 0 ? "no verdict text" : `lines ${from + 1}–${from + window.length} of ${verdict.length}`}`, children: [window.map((line, i) => _jsx(BodyText, { children: fit(`${String(from + i + 1).padStart(4)} ${line}`) }, `${from + i}:${line}`)), _jsx(BodyText, { emphasis: "dim", children: fit("←→ gate · PageUp/PageDown page") })] })), task?.state === "human" && (_jsx(Panel, { title: `PARK / ${task.id}`, children: decision === undefined
198
199
  ? _jsx(BodyText, { children: fit("park evidence unavailable — refresh before deciding") })
199
200
  : (_jsxs(_Fragment, { children: [_jsx(BodyText, { children: fit(`#L${decision.park.line} ${decision.park.kind ?? "unknown kind"}${decision.park.failedGate ? ` · failed gate ${decision.park.failedGate}` : ""} · ${decision.park.reason ?? "no reason recorded"}`) }), _jsx(BodyText, { children: fit(`blocks ${blocked.length === 0 ? "nothing" : blocked.join(", ")} · attempts ${decision.attempts}`) }), _jsx(BodyText, { children: fit(decision.verbs.length === 0 ? `diagnostic: ${decision.diagnostic ?? "no verb"}` : `decisions: ${decision.verbs.join(" · ")} (a Actions)`) }), _jsx(BodyText, { emphasis: "dim", children: fit(run.live ? "matching live daemon enacts a release at its next task boundary" : run.blockingRunId ? `other live run ${run.blockingRunId} holds the lock; resume after it ends` : "no live owner: a release records permission; resume dispatches") })] })) })), menu !== null && (_jsx(Panel, { title: `ACTIONS / ${menu.taskId}`, focused: true, children: menu.verbs.length === 0
200
201
  ? _jsx(BodyText, { children: fit(`no decision — ${decision?.diagnostic ?? "no verb for this park"}`) })
201
- : menu.verbs.map((verb, i) => _jsx(BodyText, { emphasis: i === menu.selection ? "strong" : "normal", children: fit(`${i === menu.selection ? `${GLYPHS.pointer} ` : " "}${verb}`) }, verb)) })), confirming !== null && (_jsx(Panel, { title: `CONFIRM ${confirming.command.verb.toUpperCase()}`, focused: true, children: decisionLines(decisionConfirmLines(confirming)).map((line, i) => _jsx(BodyText, { children: line }, `${i}:${line}`)) })), receipt !== null && (_jsx(Panel, { title: receipt.ok ? "RECEIPT · appended" : "RECEIPT · refused", children: decisionLines(decisionReceiptLines(receipt)).map((line, i) => _jsx(BodyText, { children: line }, `${i}:${line}`)) })), notice !== null && _jsx(BodyText, { children: notice }), _jsx(BodyText, { emphasis: "dim", children: fitCells(["↑↓ Task", "←→ Verdict gate", "PgUp/PgDn Page", "x Outcome", decisionKeybar(session.decisions, decision)].filter(Boolean).join(" · "), Math.max(1, Math.floor(columns))) })] }));
202
+ : menu.verbs.map((verb, i) => _jsx(BodyText, { emphasis: i === menu.selection ? "strong" : "normal", children: fit(`${i === menu.selection ? `${GLYPHS.pointer} ` : " "}${verb}`) }, verb)) })), confirming !== null && (_jsx(Panel, { title: `CONFIRM ${confirming.command.verb.toUpperCase()}`, focused: true, children: decisionLines(decisionConfirmLines(confirming)).map((line, i) => _jsx(BodyText, { children: line }, `${i}:${line}`)) })), receipt !== null && (_jsx(Panel, { title: receipt.ok ? "RECEIPT · appended" : "RECEIPT · refused", children: decisionLines(decisionReceiptLines(receipt)).map((line, i) => _jsx(BodyText, { children: line }, `${i}:${line}`)) })), notice !== null && _jsx(BodyText, { children: notice }), _jsx(BodyText, { emphasis: "dim", children: clipBoard(boardFooter([...RUN_VIEW_KEYS, decisionKeybar(session.decisions, decision)].filter(Boolean), colour), width) })] }));
202
203
  }
@@ -1,10 +1,12 @@
1
1
  import { type AuthHealth, type WorkerAdapter } from "../../adapters/types.js";
2
- import type { MapEntry, RoutingMode, Tier } from "../../config/config.js";
2
+ import type { MapEntry, RoutingMode, Tier, TickmarkrConfig } from "../../config/config.js";
3
3
  import { type Shape } from "../../graph/schema.js";
4
4
  export type FleetSteeringKey = "review" | "consult";
5
5
  export type FleetEditorState = {
6
6
  denyAdapters: string[];
7
7
  denyModels: string[];
8
+ denyWorkersAdapters: string[];
9
+ denyWorkersModels: string[];
8
10
  classifications: FleetClassification[];
9
11
  selectedMode: RoutingMode;
10
12
  map: Record<string, MapEntry>;
@@ -56,6 +58,15 @@ export type FleetModelEvidence = {
56
58
  probeMs?: number;
57
59
  /** OBS-519: doctor's failed model-auth verdict reason — the row must not render like a healthy one */
58
60
  unauthed?: string;
61
+ /** OBS-972/FL-1: a floating alias's resolved concrete model id (e.g. "opus" → "claude-opus-5") */
62
+ identity?: string;
63
+ };
64
+ /** The four staged deny sets every preview and the staged routing policy are computed from. */
65
+ export type FleetStagedDeny = {
66
+ adapters: string[];
67
+ models: string[];
68
+ workersAdapters: string[];
69
+ workersModels: string[];
59
70
  };
60
71
  export type FleetModelGroup = {
61
72
  adapter: string;
@@ -99,35 +110,41 @@ export type FleetCandidateOption = {
99
110
  };
100
111
  };
101
112
  export declare function formatDoctorAge(ageMs: number | null): string;
102
- export declare function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, entry, initialJudge, judgeSeats, viewRows, frameColumns, frameRows, }: {
113
+ export type FleetReach = "in" | "out-workers" | "out-all" | "out-allow" | "unknown";
114
+ /** The staged routing policy, or the loader's refusal of the staged overlay. */
115
+ export type FleetStagedRouting = {
116
+ ok: true;
117
+ routing: TickmarkrConfig["routing"];
118
+ } | {
119
+ ok: false;
120
+ error: string;
121
+ };
122
+ export declare function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels, initialDenyWorkersAdapters, initialDenyWorkersModels, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, stagedRouting, entry, initialJudge, judgeSeats, viewRows, frameColumns, frameRows, }: {
103
123
  ageMs: number | null;
104
124
  agents: AgentCli[];
105
125
  initialDenyAdapters: string[];
106
126
  initialDenyModels: string[];
127
+ initialDenyWorkersAdapters?: string[];
128
+ initialDenyWorkersModels?: string[];
107
129
  modelGroups: FleetModelGroup[];
108
130
  initialMode: RoutingMode;
109
131
  modeOptions: FleetModeOption[];
110
132
  initialMap: Record<string, MapEntry>;
111
- modePreview: (mode: RoutingMode, map: Record<string, MapEntry>, deny: {
112
- adapters: string[];
113
- models: string[];
114
- }) => string[];
115
- shapeRows: (mode: RoutingMode, map: Record<string, MapEntry>, deny: {
116
- adapters: string[];
117
- models: string[];
118
- }) => FleetShapeRow[];
119
- candidatesForShape: (shape: Shape, mode: RoutingMode, map: Record<string, MapEntry>, deny: {
120
- adapters: string[];
121
- models: string[];
122
- }) => {
133
+ modePreview: (mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny) => string[];
134
+ shapeRows: (mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny) => FleetShapeRow[];
135
+ candidatesForShape: (shape: Shape, mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny) => {
123
136
  rows: FleetCandidateOption[];
124
137
  excludedNote?: string;
138
+ ledger?: string[];
125
139
  };
126
140
  preferOptionsForShape: (shape: Shape, current: string[]) => string[];
127
141
  initialSteering: Record<FleetSteeringKey, string[] | undefined>;
128
142
  steeringOptionsFor: (which: FleetSteeringKey, current: string[]) => string[];
129
143
  reviewOverlay: (state: FleetEditorState) => FleetOverlayReview;
130
144
  reloadGuard: (bytes: string) => string | null;
145
+ /** the routing policy the staged deny sets load as (fleet: the config loader over the candidate
146
+ * bytes); absent ⇒ the four staged sets alone, with no allowlist */
147
+ stagedRouting?: (deny: FleetStagedDeny) => FleetStagedRouting;
131
148
  entry?: "presets" | "probe";
132
149
  /** resolved config judge as "adapter:model" — shown on the (keep default) picker row */
133
150
  initialJudge?: string;
@@ -140,36 +157,33 @@ export declare function FleetApp({ ageMs, agents, initialDenyAdapters, initialDe
140
157
  /** terminal height — set only on a real TTY so the footer sticks to the bottom */
141
158
  frameRows?: number;
142
159
  }): import("react").JSX.Element;
143
- export declare function runFleetInkEditor({ ageMs, adapters, health, initialDenyAdapters, initialDenyModels, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, entry, initialJudge, judgeSeats, initialInput, input, output, debug, }: {
160
+ export declare function runFleetInkEditor({ ageMs, adapters, health, initialDenyAdapters, initialDenyModels, initialDenyWorkersAdapters, initialDenyWorkersModels, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, stagedRouting, entry, initialJudge, judgeSeats, initialInput, input, output, debug, }: {
144
161
  ageMs: number | null;
145
162
  adapters: WorkerAdapter[];
146
163
  health: Record<string, AuthHealth>;
147
164
  initialDenyAdapters: string[];
148
165
  initialDenyModels: string[];
166
+ initialDenyWorkersAdapters?: string[];
167
+ initialDenyWorkersModels?: string[];
149
168
  modelGroups: FleetModelGroup[];
150
169
  initialMode: RoutingMode;
151
170
  modeOptions: FleetModeOption[];
152
171
  initialMap: Record<string, MapEntry>;
153
- modePreview: (mode: RoutingMode, map: Record<string, MapEntry>, deny: {
154
- adapters: string[];
155
- models: string[];
156
- }) => string[];
157
- shapeRows: (mode: RoutingMode, map: Record<string, MapEntry>, deny: {
158
- adapters: string[];
159
- models: string[];
160
- }) => FleetShapeRow[];
161
- candidatesForShape: (shape: Shape, mode: RoutingMode, map: Record<string, MapEntry>, deny: {
162
- adapters: string[];
163
- models: string[];
164
- }) => {
172
+ modePreview: (mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny) => string[];
173
+ shapeRows: (mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny) => FleetShapeRow[];
174
+ candidatesForShape: (shape: Shape, mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny) => {
165
175
  rows: FleetCandidateOption[];
166
176
  excludedNote?: string;
177
+ ledger?: string[];
167
178
  };
168
179
  preferOptionsForShape: (shape: Shape, current: string[]) => string[];
169
180
  initialSteering: Record<FleetSteeringKey, string[] | undefined>;
170
181
  steeringOptionsFor: (which: FleetSteeringKey, current: string[]) => string[];
171
182
  reviewOverlay: (state: FleetEditorState) => FleetOverlayReview;
172
183
  reloadGuard: (bytes: string) => string | null;
184
+ /** the routing policy the staged deny sets load as (fleet: the config loader over the candidate
185
+ * bytes); absent ⇒ the four staged sets alone, with no allowlist */
186
+ stagedRouting?: (deny: FleetStagedDeny) => FleetStagedRouting;
173
187
  /** "presets" = init's entry: Esc in the browser is HOME to the preset overlay, not quit */
174
188
  entry?: "presets" | "probe";
175
189
  initialJudge?: string;