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.
Files changed (103) hide show
  1. package/README.md +14 -3
  2. package/dist/adapters/catalog-remote.js +89 -47
  3. package/dist/adapters/claude-code.js +9 -6
  4. package/dist/adapters/codex.js +7 -4
  5. package/dist/adapters/prompt.d.ts +3 -1
  6. package/dist/adapters/prompt.js +21 -3
  7. package/dist/adapters/registry.js +3 -3
  8. package/dist/adapters/types.d.ts +13 -4
  9. package/dist/adapters/types.js +16 -0
  10. package/dist/cli/commands/approve.d.ts +5 -1
  11. package/dist/cli/commands/approve.js +66 -23
  12. package/dist/cli/commands/compile.js +13 -3
  13. package/dist/cli/commands/doctor.d.ts +2 -0
  14. package/dist/cli/commands/doctor.js +11 -3
  15. package/dist/cli/commands/fleet.js +71 -8
  16. package/dist/cli/commands/plan.js +7 -3
  17. package/dist/cli/commands/report.js +18 -2
  18. package/dist/cli/commands/resume.js +4 -2
  19. package/dist/cli/commands/status.js +37 -22
  20. package/dist/cli/help.d.ts +4 -0
  21. package/dist/cli/help.js +11 -2
  22. package/dist/config/config.d.ts +20 -0
  23. package/dist/config/config.js +47 -8
  24. package/dist/config/fleet-overlay.d.ts +1 -0
  25. package/dist/config/fleet-overlay.js +56 -0
  26. package/dist/drivers/orca.d.ts +26 -1
  27. package/dist/drivers/orca.js +199 -61
  28. package/dist/eval/canary.d.ts +2 -1
  29. package/dist/eval/canary.js +2 -2
  30. package/dist/eval/dispatch.js +1 -0
  31. package/dist/gates/acceptance.d.ts +2 -1
  32. package/dist/gates/acceptance.js +7 -2
  33. package/dist/gates/baseline.d.ts +12 -1
  34. package/dist/gates/baseline.js +11 -4
  35. package/dist/gates/cache.d.ts +3 -1
  36. package/dist/gates/cache.js +10 -3
  37. package/dist/gates/llm.d.ts +5 -4
  38. package/dist/gates/llm.js +17 -14
  39. package/dist/gates/review.d.ts +8 -0
  40. package/dist/gates/review.js +47 -15
  41. package/dist/gates/run-gates.d.ts +6 -1
  42. package/dist/gates/run-gates.js +34 -17
  43. package/dist/gates/test-manifest.d.ts +14 -0
  44. package/dist/gates/test-manifest.js +33 -6
  45. package/dist/graph/graph.d.ts +6 -2
  46. package/dist/graph/graph.js +15 -4
  47. package/dist/graph/schema.d.ts +2 -0
  48. package/dist/graph/schema.js +2 -0
  49. package/dist/plan/scope.js +2 -2
  50. package/dist/route/preference.d.ts +20 -2
  51. package/dist/route/preference.js +48 -13
  52. package/dist/route/role-pick.d.ts +16 -0
  53. package/dist/route/role-pick.js +15 -0
  54. package/dist/route/router.d.ts +14 -0
  55. package/dist/route/router.js +39 -16
  56. package/dist/run/consult.d.ts +13 -1
  57. package/dist/run/consult.js +19 -14
  58. package/dist/run/daemon.d.ts +46 -2
  59. package/dist/run/daemon.js +964 -190
  60. package/dist/run/git.d.ts +44 -1
  61. package/dist/run/git.js +103 -5
  62. package/dist/run/host-health.d.ts +20 -0
  63. package/dist/run/host-health.js +64 -0
  64. package/dist/run/journal.d.ts +126 -3
  65. package/dist/run/journal.js +418 -36
  66. package/dist/run/merge.d.ts +3 -1
  67. package/dist/run/merge.js +3 -2
  68. package/dist/run/operator-state.d.ts +24 -2
  69. package/dist/run/operator-state.js +41 -5
  70. package/dist/run/operator-summary.d.ts +3 -0
  71. package/dist/run/operator-summary.js +3 -1
  72. package/dist/run/protocol.d.ts +31 -1
  73. package/dist/run/protocol.js +3 -1
  74. package/dist/run/stall.d.ts +38 -2
  75. package/dist/run/stall.js +276 -6
  76. package/dist/run/supervision.d.ts +7 -1
  77. package/dist/run/supervision.js +5 -2
  78. package/dist/tui/cockpit/board.d.ts +1 -1
  79. package/dist/tui/cockpit/board.js +30 -22
  80. package/dist/tui/cockpit/decision-actions.d.ts +8 -5
  81. package/dist/tui/cockpit/decision-actions.js +55 -32
  82. package/dist/tui/cockpit/derive.d.ts +2 -0
  83. package/dist/tui/cockpit/derive.js +17 -2
  84. package/dist/tui/cockpit/live-runtime.d.ts +10 -0
  85. package/dist/tui/cockpit/live-runtime.js +50 -3
  86. package/dist/tui/cockpit/live-store.d.ts +1 -0
  87. package/dist/tui/cockpit/live-store.js +31 -8
  88. package/dist/tui/cockpit/run-cockpit.d.ts +3 -0
  89. package/dist/tui/cockpit/run-cockpit.js +28 -2
  90. package/dist/tui/cockpit/run-view.d.ts +11 -7
  91. package/dist/tui/cockpit/run-view.js +69 -15
  92. package/dist/tui/cockpit/setup-cockpit.d.ts +4 -0
  93. package/dist/tui/cockpit/setup-cockpit.js +6 -3
  94. package/dist/tui/ink/fleet-app.d.ts +15 -3
  95. package/dist/tui/ink/fleet-app.js +91 -22
  96. package/package.json +2 -1
  97. package/schema/config.schema.json +818 -0
  98. package/skills/tickmarkr-loop/SKILL.md +8 -2
  99. package/skills/tickmarkr-overseer/SKILL.md +85 -6
  100. package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +88 -0
  101. package/skills/tickmarkr-overseer/scripts/context-statusline.sh +81 -0
  102. package/skills/tickmarkr-overseer/scripts/grade-ci.sh +36 -34
  103. package/skills/tickmarkr-overseer/scripts/watch-journal.sh +6 -4
@@ -5,6 +5,7 @@ import { GLYPHS } from "../../brand.js";
5
5
  import { formatOwnedName, parseOwnedName } from "../../drivers/types.js";
6
6
  import { GATE_NAMES } from "../../graph/schema.js";
7
7
  import { projectActivity } from "../../run/activity.js";
8
+ import { effectiveDecisions, withPhysicalLine } from "../../run/journal.js";
8
9
  import { projectOperatorSummary } from "../../run/operator-summary.js";
9
10
  import { trackJournalRows } from "../../run/protocol.js";
10
11
  import { normalizeGateOutcome } from "../../run/outcome.js";
@@ -53,7 +54,7 @@ export function evidenceLookup(rows, page) {
53
54
  return lookup;
54
55
  }
55
56
  export const GATE_CELL_LETTERS = {
56
- passed: "P", failed: "F", running: "R", "not-run": "-", disabled: "D", unknown: "?",
57
+ passed: "P", failed: "F", queued: "Q", running: "R", "not-run": "-", disabled: "D", unknown: "?",
57
58
  };
58
59
  /** The outcome selector's vocabulary — a classification of the row, never a word search. */
59
60
  export const OUTCOME_FILTERS = ["all", "infra failure", "work failure", "pass", "unknown"];
@@ -81,10 +82,8 @@ function outcomeLabel(outcome) {
81
82
  case "unavailable": return `unknown — ${outcome.reason}`;
82
83
  }
83
84
  }
84
- /**
85
- * The current attempt's seven cells in declaration order. `rows` supplies the later journal rows
86
- * that give a cell its inherited/satisfied label; only rows for this task after the evidence line count.
87
- */
85
+ /** Current-attempt cells in declaration order; later task rows supply inherited/satisfied labels.
86
+ * Only rows after each cell's own evidence line count. */
88
87
  export function runGateCells(task, evidence, rows = []) {
89
88
  return GATE_NAMES.map((gate) => {
90
89
  const cell = task.gates[gate] ?? { state: "unknown" };
@@ -94,12 +93,15 @@ export function runGateCells(task, evidence, rows = []) {
94
93
  const labels = [];
95
94
  let outcome;
96
95
  if (data === undefined) {
97
- labels.push(line === undefined ? { "not-run": "not run", disabled: "disabled by policy", running: "running", unknown: "unknown", passed: "passed", failed: "failed" }[cell.state] : `evidence #L${line} unavailable`);
96
+ labels.push(line === undefined ? { "not-run": "not run", disabled: "disabled by policy", queued: "queued", running: "running", unknown: "unknown", passed: "passed", failed: "failed" }[cell.state] : `evidence #L${line} unavailable`);
97
+ }
98
+ else if (cell.state === "queued") {
99
+ labels.push(`queued — ${row?.event?.event ?? "wait"}${typeof data.count === "number" ? ` (${data.count} suites)` : ""}`);
98
100
  }
99
101
  else if (data.disabled === true) {
100
102
  labels.push("disabled by policy");
101
103
  }
102
- else if (row?.event?.event === "gate-start") {
104
+ else if (["gate-start", "gate-phase-start", "phase-start"].includes(row?.event?.event ?? "")) {
103
105
  labels.push("running");
104
106
  }
105
107
  else {
@@ -121,10 +123,10 @@ export function runGateCells(task, evidence, rows = []) {
121
123
  const e = later.event;
122
124
  if (e.event === "gate-reused" && e.data.gate === gate)
123
125
  labels.push(`inherited from ${str(e.data.commit) ?? "unknown commit"} · #L${later.line}`);
124
- if (e.event === "task-approved" && e.data.release === "gate-satisfied" && e.data.gate === gate)
126
+ if (e.event === "task-approved" && e.data.release === "gate-satisfied" && e.data.gate === gate && !undecidedLines(evidence.later(0)).has(later.line))
125
127
  labels.push(`satisfied by approval #L${later.line}`);
126
128
  }
127
- const verdict = typeof data?.details === "string" ? data.details.split("\n") : [];
129
+ const verdict = row?.event?.event === "gate-result" && typeof data?.details === "string" ? data.details.split("\n") : [];
128
130
  return { gate, state: cell.state, letter: GATE_CELL_LETTERS[cell.state], ...(line === undefined ? {} : { line }), ...(outcome ? { outcome } : {}), outcomeClass: outcomeClassOf(outcome, cell.state), labels, verdict };
129
131
  });
130
132
  }
@@ -190,7 +192,7 @@ const readLabel = (f) => `${f.label} ${f.line === undefined ? `(${NO_ROW})` : `#
190
192
  * Nothing is inferred from a title, a provider or a task id: an unrecorded field says so.
191
193
  */
192
194
  export function projectRunTasks(snapshot, rows, graph, decisions, runId) {
193
- const ordered = [...rows].filter((r) => r.event !== undefined).sort((a, b) => a.line - b.line);
195
+ const ordered = decidedRows(rows);
194
196
  const graphTask = (id) => graph?.tasks.find((t) => t.id === id);
195
197
  const activityTasks = snapshot.tasks.map((t) => ({ id: t.id, gates: graphTask(t.id)?.gates ?? [...GATE_NAMES], deps: graphTask(t.id)?.deps ?? [], status: t.state }));
196
198
  const tracked = trackJournalRows(runId, ordered.map((r) => ({ raw: r.event, sourceIndex: r.line })));
@@ -207,7 +209,16 @@ export function projectRunTasks(snapshot, rows, graph, decisions, runId) {
207
209
  ...(own.length === 0 ? {} : { phase: activity.get(t.id)?.state, lastEvidenceAt: own.at(-1).event.ts }),
208
210
  ...(responsibility ? { responsible: { role: str(responsibility.event.data.role), agent: str(responsibility.event.data.agent) } } : {}),
209
211
  };
210
- }), decisions.map((d) => ({ taskId: d.taskId, park: { kind: d.park.kind, tombstone: d.park.tombstone, ...(d.park.reason === undefined ? {} : { reason: d.park.reason }) }, verbs: d.verbs, ...(d.diagnostic === undefined ? {} : { diagnostic: d.diagnostic }) }))).map((s) => [s.taskId, s]));
212
+ }), decisions.map((d) => ({
213
+ taskId: d.taskId,
214
+ park: {
215
+ kind: d.park.kind, tombstone: d.park.tombstone, line: d.park.line,
216
+ ...(d.park.ts === undefined ? {} : { ts: d.park.ts }),
217
+ ...(d.park.reason === undefined ? {} : { reason: d.park.reason }),
218
+ },
219
+ verbs: d.verbs,
220
+ ...(d.diagnostic === undefined ? {} : { diagnostic: d.diagnostic }),
221
+ }))).map((s) => [s.taskId, s]));
211
222
  return snapshot.tasks.map((t) => {
212
223
  const own = byTask(t.id);
213
224
  const act = activity.get(t.id);
@@ -271,7 +282,7 @@ export function projectRunTasks(snapshot, rows, graph, decisions, runId) {
271
282
  const stalled = launched !== undefined && !returned && nudges.length > 0 && pages.length > 0
272
283
  ? { label: `${STALL_MARKER} · launch #L${launched} · nudge failed ${nudges.map((l) => `#L${l}`).join(",")} · paged ${pages.map((l) => `#L${l}`).join(",")} · no worker-result`, line: launched }
273
284
  : undefined;
274
- return { taskId: t.id, identity, phase, build, blocker, nextAction, ...(stalled ? { stalled } : {}) };
285
+ return { taskId: t.id, identity, phase, build, blocker, nextAction, ...(stalled ? { stalled } : {}), ...(t.dispatches === 0 ? { neverDispatched: true } : {}) };
275
286
  });
276
287
  }
277
288
  /** The line at which each projected value last changed, folding the task's rows (and run-level resets) prefix by prefix. */
@@ -298,8 +309,16 @@ function acceptedChanges(runId, tracked, taskId, task) {
298
309
  }
299
310
  return out;
300
311
  }
301
- /** One wrapped line per task: every field with its line, and the stall marker when the harvest is suspect. */
302
- export const projectionLine = (p) => [p.taskId, readLabel(p.identity), readLabel(p.phase), readLabel(p.build), readLabel(p.blocker), readLabel(p.nextAction), ...(p.stalled ? [p.stalled.label] : [])].join(" · ");
312
+ /**
313
+ * OBS-1104: a never-dispatched task has no identity, phase, or build row. Those fold into one
314
+ * missing-evidence clause. A substantive or journal-cited blocker and next action stay — a
315
+ * pre-dispatch park and a dependency wait are recorded facts, and dropping them hides the park token.
316
+ */
317
+ export const MISSING_EVIDENCE = "missing evidence";
318
+ const UNRECORDED_BLOCKER = new Set(["blocker none", "blocker unknown"]);
319
+ const UNRECORDED_NEXT = new Set(["next none"]);
320
+ const keptWhenUndispatched = (field, unrecorded) => field.line !== undefined || !unrecorded.has(field.label);
321
+ export const projectionLine = (p) => p.neverDispatched === true ? undispatchedProjectionLine(p) : recordedProjectionLine(p);
303
322
  /**
304
323
  * Where the current attempt's pane is recorded — and only that. The reading is exact-name: the
305
324
  * launch row's slot name must be the owned name of THIS task, attempt and run. No terminal is ever
@@ -357,6 +376,29 @@ export function paneLocator(task, rows, runId) {
357
376
  }
358
377
  /** The board's footer keys merged with the cockpit's own — every one of them handled by this view or the shell. */
359
378
  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]];
379
+ function recordedProjectionLine(p) {
380
+ return [p.taskId, readLabel(p.identity), readLabel(p.phase), readLabel(p.build), readLabel(p.blocker), readLabel(p.nextAction), ...(p.stalled ? [p.stalled.label] : [])].join(" · ");
381
+ }
382
+ function undispatchedProjectionLine(p) {
383
+ return [
384
+ p.taskId,
385
+ MISSING_EVIDENCE,
386
+ ...(keptWhenUndispatched(p.blocker, UNRECORDED_BLOCKER) ? [readLabel(p.blocker)] : []),
387
+ ...(keptWhenUndispatched(p.nextAction, UNRECORDED_NEXT) ? [readLabel(p.nextAction)] : []),
388
+ ...(p.stalled ? [p.stalled.label] : []),
389
+ ].join(" · ");
390
+ }
391
+ /**
392
+ * Rows the projection panel paints. A never-dispatched clause is shorter than the field line it
393
+ * replaces; the shell's content counter is the panel's wrapped height, and the pinned run frames
394
+ * record that counter. Blank rows keep the block as tall as the field lines were.
395
+ */
396
+ function projectionBlockRows(projections, wrap) {
397
+ const shown = projections.flatMap((p) => wrap(projectionLine(p)).map((line, i) => ({ key: `${p.taskId}:${i}`, line, strong: p.stalled !== undefined && i === 0 })));
398
+ const prior = projections.flatMap((p) => wrap(recordedProjectionLine(p)));
399
+ const pad = Math.max(0, prior.length - shown.length);
400
+ return [...shown, ...Array.from({ length: pad }, (_, i) => ({ key: `projection-pad:${i}`, line: " ", strong: false }))];
401
+ }
360
402
  /** The Run body: the approved board (BD-1) over the fold, then the selected task's detail panels. */
361
403
  export function RunView({ snapshot, rows, page, graph, decisions, session, columns, run, now = Date.now }) {
362
404
  const inner = Math.max(1, Math.floor(columns) - 4);
@@ -382,9 +424,21 @@ export function RunView({ snapshot, rows, page, graph, decisions, session, colum
382
424
  const { menu, confirming, receipt, notice } = session.decisions;
383
425
  const blocked = task === undefined ? [] : graph?.tasks.filter((g) => g.deps.includes(task.id)).map((g) => g.id) ?? decision?.blocks ?? [];
384
426
  const board = renderBoardLines({ runId: run.runId, snapshot, graph, now: now(), live: run.live, selection: task?.id, keys: false, colour }, width);
385
- 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: [wrap(`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}` : ""} · locator ${(() => { const l = paneLocator(task, allRows, run.runId); return l.status === "recorded" ? `recorded ${l.reason}` : `unavailable — ${l.reason}`; })()}`).map((line, i) => _jsx(BodyText, { children: line }, `${i}:${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)))] })), projections.length > 0 && (_jsx(Panel, { title: "PROJECTION / every task \u00B7 identity \u00B7 phase \u00B7 build \u00B7 blocker \u00B7 next action, each with its journal line", children: projections.flatMap((p) => wrap(projectionLine(p)).map((line, i) => _jsx(BodyText, { emphasis: p.stalled && i === 0 ? "strong" : "normal", children: line }, `${p.taskId}:${i}`))) })), 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
427
+ 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: [wrap(`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}` : ""} · locator ${(() => { const l = paneLocator(task, allRows, run.runId); return l.status === "recorded" ? `recorded ${l.reason}` : `unavailable — ${l.reason}`; })()}`).map((line, i) => _jsx(BodyText, { children: line }, `${i}:${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)))] })), projections.length > 0 && (_jsx(Panel, { title: "PROJECTION / every task \u00B7 identity \u00B7 phase \u00B7 build \u00B7 blocker \u00B7 next action, each with its journal line", children: projectionBlockRows(projections, wrap).map((row) => _jsx(BodyText, { emphasis: row.strong ? "strong" : "normal", children: row.line }, row.key)) })), 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
386
428
  ? _jsx(BodyText, { children: fit("park evidence unavailable — refresh before deciding") })
387
429
  : (_jsxs(_Fragment, { children: [wrap(`#L${decision.park.line} ${decision.park.kind ?? "unknown kind"}${decision.park.failedGate ? ` · failed gate ${decision.park.failedGate}` : ""} · ${decision.park.reason ?? "no reason recorded"}`).map((line, i) => _jsx(BodyText, { children: line }, `park:${i}`)), _jsx(BodyText, { children: fit(`blocks ${blocked.length === 0 ? "nothing" : blocked.join(", ")} · attempts ${decision.attempts}`) }), wrap(decision.verbs.length === 0 ? `diagnostic: ${decision.diagnostic ?? "no verb"}` : `decisions: ${decision.verbs.join(" · ")} (a Actions)`).map((line, i) => _jsx(BodyText, { children: line }, `verbs:${i}`)), _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
388
430
  ? _jsx(BodyText, { children: fit(`no decision — ${decision?.diagnostic ?? "no verb for this park"}`) })
389
431
  : 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) })] }));
390
432
  }
433
+ // OBS-1178: a refused or unsound decision released and satisfied nothing, so neither the projections nor
434
+ // a gate cell's satisfied label read it; the one decision fold judges every row by its physical line.
435
+ function undecidedLines(rows) {
436
+ const ordered = rows.filter((r) => r.event !== undefined).sort((a, b) => a.line - b.line);
437
+ const effective = effectiveDecisions(ordered.map((r) => withPhysicalLine(r.event, r.line)));
438
+ return new Set(ordered.filter((r) => r.event.event === "task-approved" && !effective.has(r.line)).map((r) => r.line));
439
+ }
440
+ /** Readable rows in physical-line order, minus the decisions that are not effective. */
441
+ function decidedRows(rows) {
442
+ const undecided = undecidedLines(rows);
443
+ return rows.filter((r) => r.event !== undefined && !undecided.has(r.line)).sort((a, b) => a.line - b.line);
444
+ }
@@ -71,6 +71,8 @@ export type ParkedDecision = {
71
71
  * so it is drawn read-only and neither verb is offered on it.
72
72
  */
73
73
  readonly tombstone: boolean;
74
+ /** OBS-1178: the park's `<line>@<ts>` token — the confirmed write binds to it via `--park`. */
75
+ readonly park?: string;
74
76
  };
75
77
  /** The parks a verb can release — the rows the surface draws live verbs on. */
76
78
  export declare function actionableDecisions(decisions: readonly ParkedDecision[]): readonly ParkedDecision[];
@@ -80,6 +82,8 @@ export type SetupDecisionVerb = "approve" | "waive" | "uphold" | "recheck";
80
82
  export type SetupDecisionCommand = {
81
83
  readonly verb: SetupDecisionVerb;
82
84
  readonly taskId: string;
85
+ /** OBS-1178: the park token the confirm inset displayed — approve refuses it once a newer park opens. */
86
+ readonly park?: string;
83
87
  };
84
88
  /** What the journal file gained this session — one entry per confirmed write. */
85
89
  export type SetupDecisionWrite = {
@@ -5,7 +5,7 @@ import { join } from "node:path";
5
5
  import { cloneElement, createElement, useState, } from "react";
6
6
  import { parse } from "yaml";
7
7
  import { approvalDispositionForRelease, approvalEnactment, approvalRunOwner, approve, } from "../../cli/commands/approve.js";
8
- import { ATTEMPT_CAP_RELEASE, GATE_SATISFIED_RELEASE, Journal, RECHECK_RELEASE, REVIEW_UPHELD_RELEASE, } from "../../run/journal.js";
8
+ import { ATTEMPT_CAP_RELEASE, bindingToken, GATE_SATISFIED_RELEASE, Journal, RECHECK_RELEASE, REVIEW_UPHELD_RELEASE, } from "../../run/journal.js";
9
9
  import { BANNER, GLYPHS, PLAIN_BANNER, } from "../../brand.js";
10
10
  import { DEFAULT_CONFIG, INTEGRITY_FLOOR_SHAPES, TIER_RANK, TickmarkrConfigSchema, unifiedYamlDiff, } from "../../config/config.js";
11
11
  import { GATE_NAMES, SHAPES } from "../../graph/schema.js";
@@ -576,7 +576,7 @@ function failedGateForNewestPark(events, taskId, parkedIndex) {
576
576
  }
577
577
  export function deriveParkedDecisions(journal) {
578
578
  const statuses = journal.replayStatuses();
579
- const events = journal.read();
579
+ const { events, lines } = journal.readSourced();
580
580
  const decisions = [];
581
581
  for (const [taskId, status] of statuses) {
582
582
  if (status !== "human")
@@ -606,6 +606,7 @@ export function deriveParkedDecisions(journal) {
606
606
  attempts,
607
607
  failedGate,
608
608
  tombstone: isTombstonePark(kind, reason),
609
+ ...(typeof parked?.ts === "string" ? { park: bindingToken({ line: lines[parkedIndex], ts: parked.ts }) } : {}),
609
610
  });
610
611
  }
611
612
  return decisions;
@@ -677,7 +678,7 @@ export function applySetupDecisionsKey(session, event, parked) {
677
678
  session: {
678
679
  ...session,
679
680
  selection,
680
- confirming: { verb, taskId: decision.taskId },
681
+ confirming: { verb, taskId: decision.taskId, ...(decision.park === undefined ? {} : { park: decision.park }) },
681
682
  },
682
683
  };
683
684
  }
@@ -704,6 +705,7 @@ export async function executeSetupDecision(command, { cwd, runId, by }) {
704
705
  runId,
705
706
  command.taskId,
706
707
  ...decisionFlag(command.verb),
708
+ ...(command.park === undefined ? [] : ["--park", command.park]),
707
709
  "--by",
708
710
  by,
709
711
  ], cwd);
@@ -800,6 +802,7 @@ function decisionEffectLines(command, decision, run) {
800
802
  export function setupDecisionConfirmLines(command, decision, actor, journalFile, run) {
801
803
  return [
802
804
  `task ${command.taskId}${decision === undefined ? "" : ` · ${decision.kind} · attempt ${decision.attempts}`}`,
805
+ ...(command.park === undefined ? [] : [`park ${command.park} · bound; refused if a newer park opens`]),
803
806
  `actor ${actor}`,
804
807
  ...decisionEffectLines(command, decision, run),
805
808
  `file ${journalFile} · append only`,
@@ -1,11 +1,13 @@
1
1
  import { type AuthHealth, type WorkerAdapter } from "../../adapters/types.js";
2
2
  import { type DenyScopeKey, type InitialDenyProps, type MapEntry, type RoutingMode, type StagedDenyKey, type Tier, type TickmarkrConfig } from "../../config/config.js";
3
- import { type Shape } from "../../graph/schema.js";
3
+ import { type Effort, type Shape } from "../../graph/schema.js";
4
4
  export type FleetSteeringKey = "review" | "consult";
5
5
  export type FleetEditorState = Record<DenyScopeKey, string[]> & {
6
6
  /** OBS-1046: the staged routing.allow complement, distinct from the authored deny lists above */
7
7
  allowOut?: string[];
8
8
  classifications: FleetClassification[];
9
+ /** OBS-1182: staged launch effort per adapter → model; absent = the CLI's own default */
10
+ efforts?: Record<string, Record<string, Effort>>;
9
11
  selectedMode: RoutingMode;
10
12
  map: Record<string, MapEntry>;
11
13
  judgeSeat?: {
@@ -22,6 +24,8 @@ export type FleetOverlayReview = {
22
24
  after: string;
23
25
  diff: string;
24
26
  path: string;
27
+ /** OBS-1144: dead pool entries the written config would carry, each with its reason. */
28
+ notes?: string[];
25
29
  };
26
30
  export type FleetEditorResult = {
27
31
  kind: "write";
@@ -121,11 +125,13 @@ export type FleetStagedRouting = {
121
125
  ok: false;
122
126
  error: string;
123
127
  };
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 & {
128
+ export declare function FleetApp({ ageMs, agents, initialAllowOut, initialEfforts, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories, reviewOverlay, reloadGuard, stagedRouting, entry, initialJudge, judgeSeats, viewRows, frameColumns, frameRows, ...initialDenyProps }: InitialDenyProps & {
125
129
  ageMs: number | null;
126
130
  agents: AgentCli[];
127
131
  /** OBS-1046: adapter ids and adapter:model keys routing.allow leaves out of the fleet */
128
132
  initialAllowOut?: string[];
133
+ /** OBS-1182: configured launch effort per adapter → model */
134
+ initialEfforts?: Record<string, Record<string, Effort>>;
129
135
  modelGroups: FleetModelGroup[];
130
136
  initialMode: RoutingMode;
131
137
  modeOptions: FleetModeOption[];
@@ -140,6 +146,8 @@ export declare function FleetApp({ ageMs, agents, initialAllowOut, modelGroups,
140
146
  preferOptionsForShape: (shape: Shape, current: string[]) => string[];
141
147
  initialSteering: Record<FleetSteeringKey, string[] | undefined>;
142
148
  steeringOptionsFor: (which: FleetSteeringKey, current: string[]) => string[];
149
+ /** OBS-1052(3): advisory review no-verdict history lines — display only, never a routing input */
150
+ reviewAdvisories?: string[];
143
151
  reviewOverlay: (state: FleetEditorState) => FleetOverlayReview;
144
152
  reloadGuard: (bytes: string) => string | null;
145
153
  /** the routing policy the staged deny sets load as (fleet: the config loader over the candidate
@@ -157,12 +165,14 @@ export declare function FleetApp({ ageMs, agents, initialAllowOut, modelGroups,
157
165
  /** terminal height — set only on a real TTY so the footer sticks to the bottom */
158
166
  frameRows?: number;
159
167
  }): import("react").JSX.Element;
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 & {
168
+ export declare function runFleetInkEditor({ ageMs, adapters, health, initialAllowOut, initialEfforts, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories, reviewOverlay, reloadGuard, stagedRouting, entry, initialJudge, judgeSeats, initialInput, input, output, debug, ...initialDenyProps }: InitialDenyProps & {
161
169
  ageMs: number | null;
162
170
  adapters: WorkerAdapter[];
163
171
  health: Record<string, AuthHealth>;
164
172
  /** OBS-1046: adapter ids and adapter:model keys routing.allow leaves out of the fleet */
165
173
  initialAllowOut?: string[];
174
+ /** OBS-1182: configured launch effort per adapter → model */
175
+ initialEfforts?: Record<string, Record<string, Effort>>;
166
176
  modelGroups: FleetModelGroup[];
167
177
  initialMode: RoutingMode;
168
178
  modeOptions: FleetModeOption[];
@@ -177,6 +187,8 @@ export declare function runFleetInkEditor({ ageMs, adapters, health, initialAllo
177
187
  preferOptionsForShape: (shape: Shape, current: string[]) => string[];
178
188
  initialSteering: Record<FleetSteeringKey, string[] | undefined>;
179
189
  steeringOptionsFor: (which: FleetSteeringKey, current: string[]) => string[];
190
+ /** OBS-1052(3): advisory review no-verdict history lines — display only, never a routing input */
191
+ reviewAdvisories?: string[];
180
192
  reviewOverlay: (state: FleetEditorState) => FleetOverlayReview;
181
193
  reloadGuard: (bytes: string) => string | null;
182
194
  /** the routing policy the staged deny sets load as (fleet: the config loader over the candidate
@@ -3,12 +3,13 @@ import { Box, render, Text, useApp, useInput } from "ink";
3
3
  import { useRef, useState } from "react";
4
4
  import { MODEL_ID_RE } from "../../adapters/types.js";
5
5
  import { retiredModelReason } from "../../adapters/model-lints.js";
6
- import { DENY_SCOPES, denyBlockFrom, denyEntriesAt, initialDenyPropOf, stagedDenyKeyOf } from "../../config/config.js";
6
+ import { DENY_SCOPES, EFFORT_ADAPTERS, denyBlockFrom, denyEntriesAt, initialDenyPropOf, stagedDenyKeyOf } from "../../config/config.js";
7
7
  import { fleetFirstTouchProvenance } from "../../config/fleet-overlay.js";
8
8
  import { exclusionReason } from "../../config/fleet-why.js";
9
9
  import { entryMatchesChannel, exclusionCollector } from "../../route/preference.js";
10
- import { TIERS } from "../../graph/schema.js";
10
+ import { EFFORTS, TIERS } from "../../graph/schema.js";
11
11
  import { windowRows } from "./components.js";
12
+ import { wrapCells } from "../cockpit/width.js";
12
13
  import { clip, clipPathTail, ElisionMark, enterAltScreen, fmtCtx, fmtMs, fmtUsdPair, Glyph, INK, inkInput, inkOutput, KeyBar, OverlayPanel, padCell, padCellStart, Pointer, SearchRow, } from "./frame.js";
13
14
  const STEERING_KEYS = ["review", "consult"];
14
15
  /** every scope's staged list, empty — the "every policy scope open" probe the picker lifts against */
@@ -31,12 +32,18 @@ function splitSeat(seat) {
31
32
  const at = seat.indexOf(":");
32
33
  return { adapter: seat.slice(0, at), model: seat.slice(at + 1) };
33
34
  }
35
+ // a picker ledger row is labelled "adapter/model — …" (fleet.ts channelLedger); this is its seat id
36
+ function ledgerChannelId(line) {
37
+ const head = line.split(" — ")[0];
38
+ const slash = head.indexOf("/");
39
+ return slash === -1 ? undefined : `${head.slice(0, slash)}:${head.slice(slash + 1)}`;
40
+ }
34
41
  const REACH_CHOICES = [
35
42
  { id: "in", label: "in", gloss: "every seat may route here" },
36
43
  { id: "out-workers", label: "out · workers", gloss: "workers skip it; judge/review/consult unaffected" },
37
44
  { id: "out-all", label: "out · all seats", gloss: "no seat routes here" },
38
45
  ];
39
- export function FleetApp({ ageMs, agents, initialAllowOut = [], modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, stagedRouting, entry = "probe", initialJudge = "", judgeSeats = [], viewRows = Number.POSITIVE_INFINITY, frameColumns = 100, frameRows, ...initialDenyProps }) {
46
+ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts = {}, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories = [], reviewOverlay, reloadGuard, stagedRouting, entry = "probe", initialJudge = "", judgeSeats = [], viewRows = Number.POSITIVE_INFINITY, frameColumns = 100, frameRows, ...initialDenyProps }) {
40
47
  const { exit } = useApp();
41
48
  const [, setRevision] = useState(0);
42
49
  // the props are the fleet command's contract, one per schema-enumerated scope (initialDeny…),
@@ -60,6 +67,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], modelGroups, ini
60
67
  allowOut: new Set(initialAllowOut),
61
68
  ...Object.fromEntries(DENY_SCOPES.map((scope) => [scope.key, new Set(initialDeny[scope.key])])),
62
69
  classifications: [],
70
+ efforts: structuredClone(initialEfforts),
63
71
  selectedMode: initialMode,
64
72
  map: structuredClone(initialMap),
65
73
  steering: structuredClone(initialSteering),
@@ -100,6 +108,12 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], modelGroups, ini
100
108
  if (JSON.stringify(ui.steering[key]) !== JSON.stringify(initialSteering[key]))
101
109
  n += 1;
102
110
  }
111
+ for (const adapter of new Set([...Object.keys(initialEfforts), ...Object.keys(ui.efforts)])) {
112
+ for (const model of new Set([...Object.keys(initialEfforts[adapter] ?? {}), ...Object.keys(ui.efforts[adapter] ?? {})])) {
113
+ if (initialEfforts[adapter]?.[model] !== ui.efforts[adapter]?.[model])
114
+ n += 1;
115
+ }
116
+ }
103
117
  return n;
104
118
  };
105
119
  // ── derived data ───────────────────────────────────────────────────────────
@@ -367,6 +381,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], modelGroups, ini
367
381
  }),
368
382
  }
369
383
  : classification),
384
+ efforts: structuredClone(ui.efforts),
370
385
  selectedMode: ui.selectedMode,
371
386
  map: ui.map,
372
387
  ...(ui.judgeSeat ? { judgeSeat: splitSeat(ui.judgeSeat) } : {}),
@@ -704,10 +719,9 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], modelGroups, ini
704
719
  // same picker reopens over the recomputed policy — a shared or adapter-wide entry is refused by
705
720
  // name and the row stays greyed
706
721
  const liftFromPicker = (overlay, line) => {
707
- const head = line.split(" — ")[0];
708
- const slash = head.indexOf("/");
709
- const self = slash === -1 ? undefined
710
- : displayedChannels().find((channel) => channel.adapter === head.slice(0, slash) && channel.model === head.slice(slash + 1));
722
+ const lineId = ledgerChannelId(line);
723
+ const self = lineId === undefined ? undefined
724
+ : displayedChannels().find((channel) => `${channel.adapter}:${channel.model}` === lineId);
711
725
  if (!self) {
712
726
  ui.notice = `nothing to lift on this row — ${line}`;
713
727
  bump();
@@ -747,7 +761,12 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], modelGroups, ini
747
761
  bump();
748
762
  };
749
763
  const reviewWindow = (overlay) => {
750
- const lines = overlay.review.diff.split("\n");
764
+ // OBS-1144: notes are pre-wrapped to the diff's width and lead the SAME scrolled buffer, so a
765
+ // note never sits past the terminal's last row, unreachable, while y still saves.
766
+ const notes = (overlay.review.notes ?? [])
767
+ .flatMap((note) => wrapCells(`! ${note}`, bodyW - 6, { continuationPrefix: " " }))
768
+ .map((text) => ({ text, note: true }));
769
+ const lines = [...notes, ...overlay.review.diff.split("\n").map((text) => ({ text, note: false }))];
751
770
  const cap = reviewCapacity();
752
771
  const maxScroll = Math.max(lines.length - cap, 0);
753
772
  const scroll = Math.min(Math.max(overlay.scroll, 0), maxScroll);
@@ -1059,16 +1078,39 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], modelGroups, ini
1059
1078
  bump();
1060
1079
  return;
1061
1080
  }
1062
- if (input === " " && rows[overlay.at]) {
1063
- const id = rows[overlay.at].id;
1081
+ if (input === " ") {
1082
+ const offered = rows[overlay.at];
1083
+ const greyed = offered ? undefined : ledger[overlay.at - rows.length];
1084
+ const id = offered?.id ?? (greyed === undefined ? undefined : ledgerChannelId(greyed));
1085
+ if (id === undefined)
1086
+ return;
1064
1087
  const at = overlay.chain.indexOf(id);
1065
- if (at === -1)
1088
+ // OBS-1145 add.1: Space drops ANY carried member, admitted or not, and leaves its deny
1089
+ // alone; only an offered row joins — a greyed one still enters through its reach (Enter)
1090
+ if (at !== -1)
1091
+ overlay.chain.splice(at, 1);
1092
+ else if (offered)
1066
1093
  overlay.chain.push(id);
1067
1094
  else
1068
- overlay.chain.splice(at, 1);
1095
+ ui.notice = `${id} is not offered — Enter lifts it first; Space pools offered rows only`;
1069
1096
  bump();
1070
1097
  return;
1071
1098
  }
1099
+ // a selection is a POOL, and its mode is a decision, not a default — the tiny
1100
+ // poolmode overlay asks it; Esc there restores this picker with the chain intact
1101
+ // OBS-525: an existing pool's mode seeds the cursor so a round-trip keeps it
1102
+ const askPoolMode = () => setOverlay({ kind: "poolmode", picker: overlay, at: ui.map[overlay.shape]?.pool?.mode === "ordered" ? 1 : 0 });
1103
+ // OBS-1145 add.1: Tab applies the chain from ANY row — in an all-greyed picker every row is
1104
+ // ✗ and Enter lifts it, so a Space drop there had no commit path; Tab lifts no deny
1105
+ if (key.tab) {
1106
+ if (overlay.chain.length)
1107
+ askPoolMode();
1108
+ else {
1109
+ ui.notice = `the ${overlay.shape} pool is empty — Esc keeps it; a on the shape reverts it to auto`;
1110
+ bump();
1111
+ }
1112
+ return;
1113
+ }
1072
1114
  if (key.return) {
1073
1115
  const greyed = ledger[overlay.at - rows.length];
1074
1116
  if (!rows[overlay.at] && greyed !== undefined) {
@@ -1076,10 +1118,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], modelGroups, ini
1076
1118
  return;
1077
1119
  }
1078
1120
  if (overlay.chain.length) {
1079
- // a selection is a POOL, and its mode is a decision, not a default — the tiny
1080
- // poolmode overlay asks it; Esc there restores this picker with the chain intact
1081
- // OBS-525: an existing pool's mode seeds the cursor so a round-trip keeps it
1082
- setOverlay({ kind: "poolmode", picker: overlay, at: ui.map[overlay.shape]?.pool?.mode === "ordered" ? 1 : 0 });
1121
+ askPoolMode();
1083
1122
  return;
1084
1123
  }
1085
1124
  if (rows[overlay.at]) {
@@ -1448,6 +1487,29 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], modelGroups, ini
1448
1487
  setOverlay({ kind: "lift", id: `${row.adapter}:${row.model}`, covering: row.covering });
1449
1488
  return;
1450
1489
  }
1490
+ if (hotkey === "e" && row) {
1491
+ // OBS-1182: e cycles launch effort default → low → medium → high → default; the seat's
1492
+ // identity (adapter:model) never changes, only tiers.<adapter>.modelOverrides.<model>.effort
1493
+ if (!EFFORT_ADAPTERS.includes(row.adapter)) {
1494
+ ui.notice = `${row.adapter} takes no effort level — effort is launch metadata only ${EFFORT_ADAPTERS.join(" and ")} accept`;
1495
+ }
1496
+ else if (!row.tier) {
1497
+ ui.notice = `${row.adapter}:${row.model} is unclassified — classify it before setting its effort`;
1498
+ }
1499
+ else {
1500
+ const current = ui.efforts[row.adapter]?.[row.model];
1501
+ const next = EFFORTS[current === undefined ? 0 : EFFORTS.indexOf(current) + 1];
1502
+ const models = { ...ui.efforts[row.adapter] };
1503
+ if (next === undefined)
1504
+ delete models[row.model];
1505
+ else
1506
+ models[row.model] = next;
1507
+ ui.efforts = { ...ui.efforts, [row.adapter]: models };
1508
+ ui.notice = `${row.adapter}:${row.model} effort → ${next ?? "CLI default"} — w writes tiers.${row.adapter}.modelOverrides.${row.model}.effort`;
1509
+ }
1510
+ bump();
1511
+ return;
1512
+ }
1451
1513
  if (hotkey === "t" && row) {
1452
1514
  if (row.tier) {
1453
1515
  ui.notice = "tier reassignment on classified models is not supported in v1 — edit config directly";
@@ -1657,6 +1719,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], modelGroups, ini
1657
1719
  const parts = [
1658
1720
  `${row.adapter}:${row.model}`,
1659
1721
  row.tier ?? "unclassified",
1722
+ ui.efforts[row.adapter]?.[row.model] ? `effort ${ui.efforts[row.adapter][row.model]}` : "",
1660
1723
  ui.lastEdit?.id === `${row.adapter}:${row.model}` ? ui.lastEdit.text : "",
1661
1724
  identityDetail(row),
1662
1725
  row.tier ? reachDetail(row) : "",
@@ -1734,7 +1797,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], modelGroups, ini
1734
1797
  if (overlay.kind === "review") {
1735
1798
  const { visible, scroll, cap, lines } = reviewWindow(overlay);
1736
1799
  const below = lines.length - scroll - visible.length;
1737
- return (_jsxs(OverlayPanel, { title: `review · ${overlay.review.path}`, width: bodyW, children: [_jsx(Text, { dimColor: true, wrap: "truncate", children: clip("everything staged lands in this one diff — y writes, n discards, ↑↓ scroll, Esc keeps editing", bodyW - 4) }), scroll > 0 && _jsx(ElisionMark, { count: scroll, side: "above" }), visible.map((line, index) => (_jsx(Text, { wrap: "truncate", color: line.startsWith("+") ? INK.brand : line.startsWith("-") ? INK.fail : undefined, dimColor: line.startsWith("@") || line.startsWith("---") || line.startsWith("+++"), children: clip(line, bodyW - 6) || " " }, `${scroll + index}:${line}`))), below > 0 && _jsx(ElisionMark, { count: below, side: "below", hint: `↓ scrolls · PgDn jumps ${cap} lines` })] }));
1800
+ return (_jsxs(OverlayPanel, { title: clipPathTail(`review · ${overlay.review.path}`, bodyW - 4), width: bodyW, children: [_jsx(Text, { dimColor: true, wrap: "truncate", children: clip("everything staged lands in this one diff — y writes, n discards, ↑↓ scroll, Esc keeps editing", bodyW - 4) }), scroll > 0 && _jsx(ElisionMark, { count: scroll, side: "above" }), visible.map(({ text: line, note }, index) => (_jsx(Text, { wrap: "truncate", color: note ? INK.warn : line.startsWith("+") ? INK.brand : line.startsWith("-") ? INK.fail : undefined, dimColor: !note && (line.startsWith("@") || line.startsWith("---") || line.startsWith("+++")), children: clip(line, bodyW - 6) || " " }, `${scroll + index}:${line}`))), below > 0 && _jsx(ElisionMark, { count: below, side: "below", hint: `↓ scrolls · PgDn jumps ${cap} lines` })] }));
1738
1801
  }
1739
1802
  if (overlay.kind === "classify") {
1740
1803
  const subject = overlay.bulk
@@ -1767,10 +1830,13 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], modelGroups, ini
1767
1830
  ];
1768
1831
  const { visible, start, above, below } = windowRows(items, overlay.at, Math.max(1, capacity - noteLines.length + 1));
1769
1832
  const chained = overlay.chain.length > 0;
1770
- return (_jsxs(OverlayPanel, { title: chained ? `pool · ${overlay.shape}` : `pin · ${overlay.shape}`, width: bodyW, children: [_jsx(Text, { dimColor: true, wrap: "truncate", children: clip("Enter pins one channel · Space selects a pool in order — Enter then asks its mode (pool replaces pin)", bodyW - 4) }), _jsx(SearchRow, { filter: ui.filter, active: true }), noteLines.map((line, index) => _jsx(Text, { dimColor: true, wrap: "truncate", children: clip(line, bodyW - 4) }, `excluded-${index}`)), above > 0 && _jsx(ElisionMark, { count: above, side: "above" }), visible.map(({ candidate, line }, index) => {
1833
+ return (_jsxs(OverlayPanel, { title: chained ? `pool · ${overlay.shape}` : `pin · ${overlay.shape}`, width: bodyW, children: [_jsx(Text, { dimColor: true, wrap: "truncate", children: clip("Enter pins one channel · Space pools in order · Tab applies the pool from any row — Enter on an offered row too (pool replaces pin)", bodyW - 4) }), _jsx(SearchRow, { filter: ui.filter, active: true }), noteLines.map((line, index) => _jsx(Text, { dimColor: true, wrap: "truncate", children: clip(line, bodyW - 4) }, `excluded-${index}`)), above > 0 && _jsx(ElisionMark, { count: above, side: "above" }), visible.map(({ candidate, line }, index) => {
1771
1834
  const selected = start + index === overlay.at;
1772
1835
  if (candidate === undefined) {
1773
- return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Pointer, { on: selected }), _jsx(Text, { dimColor: true, children: "✗ " }), _jsx(Text, { bold: selected, dimColor: !selected, children: clip(line, bodyW - 10) })] }, `ledger-${start + index}`));
1836
+ // OBS-1145 add.1: a carried chain member the picker cannot offer keeps its ordinal
1837
+ const id = ledgerChannelId(line);
1838
+ const at = id === undefined ? -1 : overlay.chain.indexOf(id);
1839
+ return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Pointer, { on: selected }), at !== -1 && _jsx(Text, { color: INK.brand, children: `${at + 1} ` }), _jsx(Text, { dimColor: true, children: "✗ " }), _jsx(Text, { bold: selected, dimColor: !selected, children: clip(line, bodyW - (at === -1 ? 10 : 12)) })] }, `ledger-${start + index}`));
1774
1840
  }
1775
1841
  const at = overlay.chain.indexOf(candidate.id);
1776
1842
  return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Pointer, { on: selected }), at === -1 ? _jsx(Text, { dimColor: true, children: "· " }) : _jsx(Text, { color: INK.brand, children: `${at + 1} ` }), _jsx(Text, { bold: selected, children: clip(candidate.label, bodyW - 10) })] }, candidate.id));
@@ -1840,7 +1906,8 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], modelGroups, ini
1840
1906
  return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Shapes" }), _jsx(Text, { dimColor: true, children: ` routed under ${ui.selectedMode}` })] }), _jsx(Text, { children: " " }), rows.map((row, index) => renderShapeRow(row, ui.focus === "list" && index === ui.listAt))] }));
1841
1907
  }
1842
1908
  const rows = steeringList();
1843
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Steering" }), _jsx(Text, { dimColor: true, children: " review · consult · judge" })] }), _jsx(Text, { children: " " }), rows.map((row, index) => renderPlainRow(row.id, row.label, ui.focus === "list" && index === ui.listAt))] }));
1909
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Steering" }), _jsx(Text, { dimColor: true, children: " review · consult · judge" })] }), _jsx(Text, { children: " " }), rows.map((row, index) => renderPlainRow(row.id, row.label, ui.focus === "list" && index === ui.listAt)), reviewAdvisories.length ? _jsx(Text, { children: " " }) : null, reviewAdvisories.flatMap((line) => wrapCells(`! ${line}`, bodyW - 4, { continuationPrefix: " " }))
1910
+ .map((text, index) => _jsx(Text, { color: INK.warn, children: text }, `advisory-${index}`))] }));
1844
1911
  })();
1845
1912
  // ── footer ─────────────────────────────────────────────────────────────────
1846
1913
  const detailLines = () => {
@@ -1911,6 +1978,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], modelGroups, ini
1911
1978
  if (overlay.kind === "candidates") {
1912
1979
  return [
1913
1980
  { key: "Enter", label: overlay.chain.length ? "apply pool" : "pin · lift greyed" },
1981
+ ...(overlay.chain.length ? [{ key: "Tab", label: "apply from any row" }] : []),
1914
1982
  { key: "Space", label: "pool in order" },
1915
1983
  { key: "type", label: "search" },
1916
1984
  { key: "Esc", label: "cancel" },
@@ -1955,6 +2023,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], modelGroups, ini
1955
2023
  { key: "Enter", label: "assign/classify" },
1956
2024
  { key: "Space", label: "reach picker" },
1957
2025
  { key: "l", label: "lift entry" },
2026
+ { key: "e", label: "effort" },
1958
2027
  { key: "m", label: "presets" },
1959
2028
  { key: "w", label: "review + write" },
1960
2029
  { key: "←", label: "rail" },
@@ -1985,7 +2054,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], modelGroups, ini
1985
2054
  ? _jsx(Text, { color: INK.warn, children: ` ! ${clip(ui.notice, width - 4)}` })
1986
2055
  : details.map((line) => _jsx(Text, { dimColor: true, children: ` ${clip(line, width - 2)}` }, line)), _jsx(KeyBar, { binds: keyBinds() })] }));
1987
2056
  }
1988
- export async function runFleetInkEditor({ ageMs, adapters, health, initialAllowOut = [], modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, stagedRouting, entry = "probe", initialJudge = "", judgeSeats = [], initialInput = [], input, output, debug = false, ...initialDenyProps }) {
2057
+ export async function runFleetInkEditor({ ageMs, adapters, health, initialAllowOut = [], initialEfforts, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories = [], reviewOverlay, reloadGuard, stagedRouting, entry = "probe", initialJudge = "", judgeSeats = [], initialInput = [], input, output, debug = false, ...initialDenyProps }) {
1989
2058
  const agents = adapters.flatMap((adapter) => {
1990
2059
  const state = health[adapter.id];
1991
2060
  if (!state?.installed)
@@ -2009,7 +2078,7 @@ export async function runFleetInkEditor({ ageMs, adapters, health, initialAllowO
2009
2078
  // rebuilt from output.columns/rows so a pane resize re-renders at the new size. Same root
2010
2079
  // element type on every rerender ⇒ React keeps the component instance, so staged edits,
2011
2080
  // cursor, and overlays all survive the resize.
2012
- const editorElement = () => (_jsx(FleetApp, { ageMs: ageMs, agents: agents, ...initialDenyProps, initialAllowOut: initialAllowOut, modelGroups: declaredModelGroups, initialMode: initialMode, modeOptions: modeOptions, initialMap: initialMap, modePreview: modePreview, shapeRows: shapeRows, candidatesForShape: candidatesForShape, preferOptionsForShape: preferOptionsForShape, initialSteering: initialSteering, steeringOptionsFor: steeringOptionsFor, reviewOverlay: reviewOverlay, reloadGuard: reloadGuard, stagedRouting: stagedRouting, entry: entry, initialJudge: initialJudge, judgeSeats: judgeSeats,
2081
+ const editorElement = () => (_jsx(FleetApp, { ageMs: ageMs, agents: agents, ...initialDenyProps, initialAllowOut: initialAllowOut, initialEfforts: initialEfforts, modelGroups: declaredModelGroups, initialMode: initialMode, modeOptions: modeOptions, initialMap: initialMap, modePreview: modePreview, shapeRows: shapeRows, candidatesForShape: candidatesForShape, preferOptionsForShape: preferOptionsForShape, initialSteering: initialSteering, steeringOptionsFor: steeringOptionsFor, reviewAdvisories: reviewAdvisories, reviewOverlay: reviewOverlay, reloadGuard: reloadGuard, stagedRouting: stagedRouting, entry: entry, initialJudge: initialJudge, judgeSeats: judgeSeats,
2013
2082
  // v1.90.9: cap every list to the terminal minus chrome so the cursor and header can
2014
2083
  // never scroll off-screen (omp's 218-model list did exactly that pre-cap).
2015
2084
  // OBS-523: chrome is ≤12 rows worst case (header, borders, padding, scope+search,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "2.6.0",
3
+ "version": "2.6.2",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -80,6 +80,7 @@
80
80
  "scripts/assert-test-file-count.sh",
81
81
  "scripts/emit-schema.ts",
82
82
  "scripts/probe-rig.mjs",
83
+ "scripts/run-ci-vitest.sh",
83
84
  "specs/export-selftest.spec.md"
84
85
  ],
85
86
  "prefixes": [