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
@@ -4,6 +4,8 @@ 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
6
  import { fleetFirstTouchProvenance } from "../../config/fleet-overlay.js";
7
+ import { exclusionReason } from "../../config/fleet-why.js";
8
+ import { entryMatchesChannel, exclusionCollector } from "../../route/preference.js";
7
9
  import { TIERS } from "../../graph/schema.js";
8
10
  import { windowRows } from "./components.js";
9
11
  import { clip, clipPathTail, ElisionMark, enterAltScreen, fmtCtx, fmtMs, fmtUsdPair, Glyph, INK, inkInput, inkOutput, KeyBar, OverlayPanel, padCell, padCellStart, Pointer, SearchRow, } from "./frame.js";
@@ -23,7 +25,7 @@ function splitSeat(seat) {
23
25
  const at = seat.indexOf(":");
24
26
  return { adapter: seat.slice(0, at), model: seat.slice(at + 1) };
25
27
  }
26
- export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, entry = "probe", initialJudge = "", judgeSeats = [], viewRows = Number.POSITIVE_INFINITY, frameColumns = 100, frameRows, }) {
28
+ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels, initialDenyWorkersAdapters = [], initialDenyWorkersModels = [], modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, stagedRouting, entry = "probe", initialJudge = "", judgeSeats = [], viewRows = Number.POSITIVE_INFINITY, frameColumns = 100, frameRows, }) {
27
29
  const { exit } = useApp();
28
30
  const [, setRevision] = useState(0);
29
31
  const uiRef = useRef(null);
@@ -40,8 +42,11 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
40
42
  showAll: false,
41
43
  presetsSeen: false,
42
44
  notice: "",
45
+ lastEdit: null,
43
46
  deny: new Set(initialDenyAdapters),
44
47
  denyModels: new Set(initialDenyModels),
48
+ denyWorkersAdapters: new Set(initialDenyWorkersAdapters),
49
+ denyWorkersModels: new Set(initialDenyWorkersModels),
45
50
  classifications: [],
46
51
  selectedMode: initialMode,
47
52
  map: structuredClone(initialMap),
@@ -59,6 +64,8 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
59
64
  // chip, and the empty-`w` notice all read it. Counts staged EDITS, not diff hunks.
60
65
  const initialDenySet = new Set(initialDenyAdapters);
61
66
  const initialDenyModelSet = new Set(initialDenyModels);
67
+ const initialDenyWorkersAdapterSet = new Set(initialDenyWorkersAdapters);
68
+ const initialDenyWorkersModelSet = new Set(initialDenyWorkersModels);
62
69
  const stagedCount = () => {
63
70
  let n = ui.classifications.length
64
71
  + (ui.selectedMode !== initialMode ? 1 : 0)
@@ -75,6 +82,18 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
75
82
  for (const model of initialDenyModelSet)
76
83
  if (!ui.denyModels.has(model))
77
84
  n += 1;
85
+ for (const adapter of ui.denyWorkersAdapters)
86
+ if (!initialDenyWorkersAdapterSet.has(adapter))
87
+ n += 1;
88
+ for (const adapter of initialDenyWorkersAdapterSet)
89
+ if (!ui.denyWorkersAdapters.has(adapter))
90
+ n += 1;
91
+ for (const model of ui.denyWorkersModels)
92
+ if (!initialDenyWorkersModelSet.has(model))
93
+ n += 1;
94
+ for (const model of initialDenyWorkersModelSet)
95
+ if (!ui.denyWorkersModels.has(model))
96
+ n += 1;
78
97
  for (const shape of new Set([...Object.keys(initialMap), ...Object.keys(ui.map)])) {
79
98
  // {} ≡ absent: pin-then-auto leaves an empty entry that writes nothing — not staged work
80
99
  if (JSON.stringify(initialMap[shape] ?? {}) !== JSON.stringify(ui.map[shape] ?? {}))
@@ -87,17 +106,55 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
87
106
  return n;
88
107
  };
89
108
  // ── derived data ───────────────────────────────────────────────────────────
90
- const enabledGroups = () => modelGroups.filter((group) => !ui.deny.has(group.adapter));
109
+ // OBS-994/FL-1 repair: a flatly (or workers-) denied adapter's channels stay VISIBLE here —
110
+ // every channel needs a reach cell, and Space below can only cycle a scope it can see.
111
+ const enabledGroups = () => modelGroups;
91
112
  const scopedGroups = () => {
92
113
  if (ui.adapterAt === -1)
93
114
  return enabledGroups();
94
115
  const group = modelGroups[ui.adapterAt];
95
- return group && !ui.deny.has(group.adapter) ? [group] : [];
116
+ return group ? [group] : [];
117
+ };
118
+ // LEG2-T3 finding 1: a channel's reach and every reason come from the exclusion collector over
119
+ // the STAGED policy (fleet loads the candidate bytes; see stagedRouting) and the recorded
120
+ // identity — never literal membership of the staged sets, which missed identity, bare-model and
121
+ // family entries and could not tell an allowlist exclusion from a deny.
122
+ let routingMemo = null;
123
+ const stagedPolicy = () => {
124
+ const deny = stagedDeny();
125
+ const key = JSON.stringify(deny);
126
+ if (routingMemo?.key !== key) {
127
+ routingMemo = {
128
+ key,
129
+ policy: stagedRouting
130
+ ? stagedRouting(deny)
131
+ : { ok: true, routing: { deny: { adapters: deny.adapters, models: deny.models, workers: { adapters: deny.workersAdapters, models: deny.workersModels } } } },
132
+ };
133
+ }
134
+ return routingMemo.policy;
135
+ };
136
+ const reachFor = (adapter, model, identity) => {
137
+ const policy = stagedPolicy();
138
+ if (!policy.ok)
139
+ return { reach: "unknown", reasons: [`preview unavailable (${policy.error})`], uncoveredAlias: false };
140
+ const channel = { adapter, model, ...(identity !== undefined ? { identity } : {}) };
141
+ const worker = exclusionCollector(channel, policy.routing, "worker");
142
+ const allSeats = exclusionCollector(channel, policy.routing, "judge");
143
+ const reach = allSeats.some((scope) => scope.by === "deny") ? "out-all"
144
+ : allSeats.length ? "out-allow"
145
+ : worker.length ? "out-workers"
146
+ : "in";
147
+ return {
148
+ reach,
149
+ reasons: worker.map(exclusionReason),
150
+ uncoveredAlias: identity !== undefined && !worker.some((scope) => scope.by === "deny"),
151
+ };
96
152
  };
97
153
  const groupRows = (group) => {
98
154
  const rows = group.rows.map((row) => {
99
155
  const staged = ui.classifications.find((classification) => classification.adapter === group.adapter
100
156
  && (classification.model === row.model || classification.model === row.classifyModel));
157
+ const reach = reachFor(group.adapter, row.model, row.evidence?.identity);
101
158
  return {
102
159
  adapter: group.adapter,
103
160
  model: row.model,
@@ -110,18 +167,21 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
110
167
  variants: row.variants,
111
168
  foldedModels: row.foldedModels,
112
169
  score: row.score,
113
- denied: ui.denyModels.has(`${group.adapter}:${row.model}`),
170
+ denied: reach.reach !== "in",
171
+ ...reach,
114
172
  };
115
173
  });
116
174
  const known = new Set(rows.flatMap((row) => row.classifyModel ? [row.model, row.classifyModel] : [row.model]));
117
175
  for (const staged of ui.classifications) {
118
176
  if (staged.adapter === group.adapter && !known.has(staged.model)) {
177
+ const reach = reachFor(group.adapter, staged.model);
119
178
  rows.push({
120
179
  adapter: group.adapter,
121
180
  model: staged.model,
122
181
  tier: staged.tier,
123
182
  channel: group.channel,
124
- denied: ui.denyModels.has(`${group.adapter}:${staged.model}`),
183
+ denied: reach.reach !== "in",
184
+ ...reach,
125
185
  });
126
186
  }
127
187
  }
@@ -151,8 +211,14 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
151
211
  return groups.flatMap(groupRows)
152
212
  .filter((row) => ui.showAll || row.tier !== undefined || retiredModelReason(row.model) === null).length;
153
213
  };
154
- // three preview callbacks share it in lockstep — the staged deny truth every preview ranks under
155
- const stagedDeny = () => ({ adapters: [...ui.deny].sort(), models: [...ui.denyModels].sort() });
214
+ // three preview callbacks share it in lockstep — the staged deny truth every preview ranks under.
215
+ // LEG2-T3 finding 2: all four scopes travel distinct; the preview loads them as written.
216
+ const stagedDeny = () => ({
217
+ adapters: [...ui.deny].sort(),
218
+ models: [...ui.denyModels].sort(),
219
+ workersAdapters: [...ui.denyWorkersAdapters].sort(),
220
+ workersModels: [...ui.denyWorkersModels].sort(),
221
+ });
156
222
  const shapeList = () => shapeRows(ui.selectedMode, ui.map, stagedDeny());
157
223
  const steeringList = () => [
158
224
  ...STEERING_KEYS.map((key) => ({
@@ -179,6 +245,8 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
179
245
  const editorState = () => ({
180
246
  denyAdapters: [...ui.deny].sort(),
181
247
  denyModels: [...ui.denyModels].sort(),
248
+ denyWorkersAdapters: [...ui.denyWorkersAdapters].sort(),
249
+ denyWorkersModels: [...ui.denyWorkersModels].sort(),
182
250
  classifications: ui.classifications.map((classification) => classification.vendor && classification.channel
183
251
  ? {
184
252
  ...classification,
@@ -605,12 +673,13 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
605
673
  }
606
674
  if (overlay.kind === "candidates") {
607
675
  const rows = overlay.rows.filter((candidate) => matches(candidate.label, ui.filter));
676
+ const ledger = overlay.ledger.filter((line) => matches(line, ui.filter));
608
677
  if (key.escape) {
609
678
  setOverlay(null);
610
679
  return;
611
680
  }
612
681
  if (key.downArrow) {
613
- overlay.at = Math.min(overlay.at + 1, Math.max(rows.length - 1, 0));
682
+ overlay.at = Math.min(overlay.at + 1, Math.max(rows.length + ledger.length - 1, 0));
614
683
  bump();
615
684
  return;
616
685
  }
@@ -644,7 +713,8 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
644
713
  }
645
714
  return;
646
715
  }
647
- editFilter((f) => overlay.rows.filter((candidate) => matches(candidate.label, f)).length, (max) => {
716
+ editFilter((f) => overlay.rows.filter((candidate) => matches(candidate.label, f)).length
717
+ + overlay.ledger.filter((line) => matches(line, f)).length, (max) => {
648
718
  overlay.at = Math.min(overlay.at, max);
649
719
  });
650
720
  return;
@@ -931,13 +1001,71 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
931
1001
  beginClassification(row.adapter, row.model);
932
1002
  return;
933
1003
  }
1004
+ // OBS-994/FL-1: membership can't fix an auth failure either — the row's reach stays
1005
+ // whatever it is and the operator is pointed at doctor instead of a toggle that lies.
1006
+ if (row.evidence?.unauthed !== undefined) {
1007
+ ui.notice = `${row.adapter}:${row.model} is unauthed — re-probe with tickmarkr doctor before its fleet reach can change`;
1008
+ bump();
1009
+ return;
1010
+ }
934
1011
  const id = `${row.adapter}:${row.model}`;
935
- const next = new Set(ui.denyModels);
936
- if (next.has(id))
937
- next.delete(id);
938
- else
939
- next.add(id);
940
- ui.denyModels = next;
1012
+ // OBS-994/FL-1, LEG2-T3 finding 3: Space cycles in → out(workers) → out(all) → in by editing
1013
+ // only THIS channel's own entries in the per-model scopes — its adapter:model key, or an
1014
+ // identity/bare-model entry no other browser row matches. An adapter-wide or shared entry
1015
+ // is never promoted or cleared from a channel row: that would move sibling channels and
1016
+ // wipe reasons that are not this row's to clear. The row's detail line names the entry each
1017
+ // press edited; a refused press names the shared entry in the notice instead.
1018
+ const self = { adapter: row.adapter, model: row.model, identity: row.evidence?.identity };
1019
+ const others = modelGroups.flatMap((group) => group.rows
1020
+ .filter((other) => group.adapter !== row.adapter || other.model !== row.model)
1021
+ .map((other) => ({ adapter: group.adapter, model: other.model, identity: other.evidence?.identity })));
1022
+ // Judge c3 (R107): exactly ONE selected entry per press — the channel's adapter:model key when
1023
+ // it is a candidate, else the first candidate in sorted order. The cycle is three states over
1024
+ // two scopes, so the out-workers → out-all press re-scopes that one entry from
1025
+ // routing.deny.workers.models to routing.deny.models; every other press adds or clears one
1026
+ // entry in one scope. The channel's other entries keep their reasons for later presses.
1027
+ // LEG2-T3 round 2 finding 1: promotion widens the selected ban as spelled — a bare model id
1028
+ // then excludes every seat serving that model — while a CLEAR only takes an entry no other
1029
+ // row matches, so no sibling is ever admitted by a channel toggle.
1030
+ const select = (entries, ownOnly) => {
1031
+ const candidates = [...entries].filter((entry) => entryMatchesChannel(entry, self, true)
1032
+ && (!ownOnly || !others.some((other) => entryMatchesChannel(entry, other, true))));
1033
+ return candidates.includes(id) ? id : candidates.sort()[0];
1034
+ };
1035
+ if (row.reach === "unknown") {
1036
+ ui.notice = `${id} reach is unknown — Space does not toggle it until the staged overlay loads: ${row.reasons.join("; ")}`;
1037
+ bump();
1038
+ return;
1039
+ }
1040
+ if (row.reach === "in") {
1041
+ ui.denyWorkersModels = new Set([...ui.denyWorkersModels, id]);
1042
+ ui.lastEdit = { id, text: `space: added ${id} to routing.deny.workers.models` };
1043
+ }
1044
+ else if (row.reach === "out-workers") {
1045
+ const entry = select(ui.denyWorkersModels, false) ?? id;
1046
+ ui.denyWorkersModels = new Set([...ui.denyWorkersModels].filter((staged) => staged !== entry));
1047
+ ui.denyModels = new Set([...ui.denyModels, entry]);
1048
+ ui.lastEdit = { id, text: `space: moved ${entry} to routing.deny.models` };
1049
+ }
1050
+ else {
1051
+ // round 2 finding 2: either flat list may carry this channel's own reason
1052
+ const fromModels = select(ui.denyModels, true);
1053
+ const fromAdapters = fromModels === undefined ? select(ui.deny, true) : undefined;
1054
+ const entry = fromModels ?? fromAdapters;
1055
+ if (entry === undefined) {
1056
+ const shared = ui.deny.has(row.adapter)
1057
+ ? `every ${row.adapter} channel is out together — take the adapter back in on the rail`
1058
+ : "a shared entry covers other channels too";
1059
+ ui.notice = `${id} stays out — ${row.reasons.join("; ")} — Space edits only this channel's own entries; ${shared}`;
1060
+ bump();
1061
+ return;
1062
+ }
1063
+ if (fromModels !== undefined)
1064
+ ui.denyModels = new Set([...ui.denyModels].filter((staged) => staged !== entry));
1065
+ else
1066
+ ui.deny = new Set([...ui.deny].filter((staged) => staged !== entry));
1067
+ ui.lastEdit = { id, text: `space: cleared ${entry} from routing.deny.${fromModels !== undefined ? "models" : "adapters"}` };
1068
+ }
941
1069
  bump();
942
1070
  return;
943
1071
  }
@@ -1051,6 +1179,7 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
1051
1179
  shape,
1052
1180
  rows: picked.rows,
1053
1181
  excludedNote: picked.excludedNote,
1182
+ ledger: picked.ledger ?? [],
1054
1183
  // OBS-525: an existing pool round-trips — reopening the picker starts from the staged
1055
1184
  // channels instead of an empty selection that would silently replace the pool with a pin.
1056
1185
  chain: ui.map[shape]?.pool?.channels.slice() ?? [],
@@ -1116,10 +1245,37 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
1116
1245
  return "sub";
1117
1246
  return usd;
1118
1247
  };
1248
+ // OBS-994/FL-1: the reach line — every collector scope that excludes this row, each with its
1249
+ // config path and matching entry (an unauthed row instead points at the doctor re-probe).
1250
+ const reachDetail = (row) => {
1251
+ if (row.evidence?.unauthed !== undefined)
1252
+ return "unauthed — re-probe with tickmarkr doctor (Space does not toggle reach)";
1253
+ const reasons = row.reasons.join("; ");
1254
+ if (row.reach === "out-all")
1255
+ return `reach: out · all seats — ${reasons}`;
1256
+ if (row.reach === "out-allow")
1257
+ return `reach: out · all seats · allow — ${reasons}`;
1258
+ if (row.reach === "out-workers")
1259
+ return `reach: out · workers only (judge/review/consult unaffected) — ${reasons}`;
1260
+ if (row.reach === "unknown")
1261
+ return `reach: unknown — ${reasons}`;
1262
+ return "reach: in — Space cycles to out · workers";
1263
+ };
1264
+ const reachCell = (row) => row.reach === "in" || row.reach === "unknown" ? row.reach : `out ${row.reach === "out-workers" ? "workers" : row.reach.slice(4)}`;
1265
+ const identityDetail = (row) => {
1266
+ if (row.evidence?.identity === undefined)
1267
+ return "";
1268
+ return row.uncoveredAlias
1269
+ ? `resolved identity ${row.evidence.identity} — no deny entry covers it`
1270
+ : `resolved identity ${row.evidence.identity}`;
1271
+ };
1119
1272
  const modelDetail = (row) => {
1120
1273
  const parts = [
1121
1274
  `${row.adapter}:${row.model}`,
1122
1275
  row.tier ?? "unclassified",
1276
+ ui.lastEdit?.id === `${row.adapter}:${row.model}` ? ui.lastEdit.text : "",
1277
+ identityDetail(row),
1278
+ row.tier ? reachDetail(row) : "",
1123
1279
  row.variants?.length ? `variants ${row.variants.join(", ")}` : "",
1124
1280
  row.classifyModel ? `classify writes ${row.classifyModel}` : "",
1125
1281
  row.foldedModels?.length ? `${row.foldedModels.length} gateway ids folded: ${row.foldedModels.join(", ")}` : "",
@@ -1138,20 +1294,22 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
1138
1294
  const tier = tierCell(row);
1139
1295
  const showProbe = bodyW >= 76;
1140
1296
  const showPrice = bodyW >= 60;
1141
- // every cell accounted for: pointer 2 + glyph 1 + gap 1 + tier 11 + ctx 6 (+ price 12 + probe 7)
1142
- const nameW = bodyW - 4 - 11 - 6 - (showPrice ? 12 : 0) - (showProbe ? 7 : 0);
1297
+ // every cell accounted for: pointer 2 + glyph 1 + gap 1 + tier 11 + reach 12 + ctx 6 (+ price 12 + probe 7)
1298
+ const nameW = bodyW - 4 - 11 - 12 - 6 - (showPrice ? 12 : 0) - (showProbe ? 7 : 0);
1143
1299
  // OBS-531: deep router ids (omp/prime-agent) clip tail-preserving — the LAST segment is the
1144
1300
  // distinguishing half; end-clipping rendered ten identical "prime-agent/anthropic/claude-…" rows.
1145
1301
  const folded = row.foldedModels?.length ?? 1;
1146
1302
  const name = clipPathTail(`${row.adapter}/${row.model}${folded > 1 ? ` ×${folded}` : ""}`, nameW);
1147
1303
  const prefixLen = Math.min(name.length, row.adapter.length + 1);
1148
- return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Pointer, { on: selected }), row.denied
1304
+ return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Pointer, { on: selected }), row.reach === "out-all" || row.reach === "out-allow"
1149
1305
  ? _jsx(Glyph, { kind: "off" })
1150
- : row.evidence?.unauthed !== undefined
1306
+ : row.evidence?.unauthed !== undefined || row.reach === "unknown"
1151
1307
  ? _jsx(Glyph, { kind: "fail" })
1152
- : row.tier
1153
- ? _jsx(Glyph, { kind: "on" })
1154
- : _jsx(Glyph, { kind: "unknown" }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: row.denied, children: [_jsx(Text, { dimColor: true, children: name.slice(0, prefixLen) }), _jsx(Text, { bold: selected, children: name.slice(prefixLen).padEnd(Math.max(nameW - prefixLen, 0)) })] }), _jsx(Text, { color: tier.color, dimColor: tier.dim, children: padCellStart(tier.text, 11) }), _jsx(Text, { dimColor: true, children: padCellStart(fmtCtx(row.evidence?.contextWindow), 6) }), showPrice && _jsx(Text, { dimColor: priceCell(row) === "sub", children: padCellStart(priceCell(row), 12) }), showProbe && _jsx(Text, { dimColor: true, children: padCellStart(fmtMs(row.evidence?.probeMs), 7) })] }, `${row.adapter}:${row.model}`));
1308
+ : row.reach === "out-workers"
1309
+ ? _jsx(Glyph, { kind: "warn" })
1310
+ : row.tier
1311
+ ? _jsx(Glyph, { kind: "on" })
1312
+ : _jsx(Glyph, { kind: "unknown" }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: row.denied, children: [_jsx(Text, { dimColor: true, children: name.slice(0, prefixLen) }), _jsx(Text, { bold: selected, children: name.slice(prefixLen).padEnd(Math.max(nameW - prefixLen, 0)) })] }), _jsx(Text, { color: tier.color, dimColor: tier.dim, children: padCellStart(tier.text, 11) }), _jsx(Text, { dimColor: row.reach === "in", children: padCellStart(row.tier ? reachCell(row) : "", 12) }), _jsx(Text, { dimColor: true, children: padCellStart(fmtCtx(row.evidence?.contextWindow), 6) }), showPrice && _jsx(Text, { dimColor: priceCell(row) === "sub", children: padCellStart(priceCell(row), 12) }), showProbe && _jsx(Text, { dimColor: true, children: padCellStart(fmtMs(row.evidence?.probeMs), 7) })] }, `${row.adapter}:${row.model}`));
1155
1313
  };
1156
1314
  const renderPlainRow = (id, label, selected, rowW = bodyW - 2) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Pointer, { on: selected }), _jsx(Text, { bold: selected, children: clip(label, rowW) })] }, id));
1157
1315
  const renderShapeRow = (row, selected, rowW = bodyW - 2) => {
@@ -1213,12 +1371,24 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
1213
1371
  }
1214
1372
  if (overlay.kind === "candidates") {
1215
1373
  const rows = overlay.rows.filter((candidate) => matches(candidate.label, ui.filter));
1216
- const { visible, start, above, below } = windowRows(rows, overlay.at, capacity);
1374
+ // the shape-wide captions, bounded to half the list; per-channel ledger rows are list items below
1375
+ const notes = overlay.excludedNote?.split("\n") ?? [];
1376
+ const noteRoom = Math.max(2, Math.floor(capacity / 2));
1377
+ const noteLines = notes.length > noteRoom
1378
+ ? [...notes.slice(0, noteRoom - 1), `… +${notes.length - noteRoom + 1} more notes`]
1379
+ : notes;
1380
+ const items = [
1381
+ ...rows.map((candidate) => ({ candidate, line: candidate.label })),
1382
+ ...overlay.ledger.filter((line) => matches(line, ui.filter)).map((line) => ({ candidate: undefined, line })),
1383
+ ];
1384
+ const { visible, start, above, below } = windowRows(items, overlay.at, Math.max(1, capacity - noteLines.length + 1));
1217
1385
  const chained = overlay.chain.length > 0;
1218
- 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 }), overlay.excludedNote !== undefined
1219
- && _jsx(Text, { dimColor: true, wrap: "truncate", children: clip(overlay.excludedNote, bodyW - 4) }), above > 0 && _jsx(ElisionMark, { count: above, side: "above" }), visible.map((candidate, index) => {
1220
- const at = overlay.chain.indexOf(candidate.id);
1386
+ 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) => {
1221
1387
  const selected = start + index === overlay.at;
1388
+ if (candidate === undefined) {
1389
+ 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}`));
1390
+ }
1391
+ const at = overlay.chain.indexOf(candidate.id);
1222
1392
  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));
1223
1393
  }), below > 0 && _jsx(ElisionMark, { count: below, side: "below" })] }));
1224
1394
  }
@@ -1263,8 +1433,9 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
1263
1433
  const scopeLabel = ui.adapterAt === -1
1264
1434
  ? "All models"
1265
1435
  : modelGroups[ui.adapterAt]?.adapter ?? "";
1266
- const scopeDenied = ui.adapterAt !== -1 && ui.deny.has(modelGroups[ui.adapterAt]?.adapter ?? "");
1267
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: scopeLabel }), _jsx(Text, { dimColor: true, children: ` ${rows.length}` })] }), _jsx(SearchRow, { filter: ui.filter, active: ui.searching, hint: "/ to search" }), modelVisibilityLine() && _jsx(Text, { dimColor: true, children: ` ${modelVisibilityLine()}` }), scopeDenied && _jsx(Text, { color: INK.warn, children: `${modelGroups[ui.adapterAt]?.adapter} is out of the fleet — Space on its rail row adds it back` }), above > 0 && _jsx(ElisionMark, { count: above, side: "above" }), visible.map((row, index) => renderModelRow(row, ui.focus === "list" && start + index === ui.listAt)), below > 0 && _jsx(ElisionMark, { count: below, side: "below", hint: "/ to search" }), rows.length === 0 && !scopeDenied && _jsx(Text, { dimColor: true, children: " no models match" })] }));
1436
+ // OBS-994/FL-1 repair: an adapter-wide deny (flat or workers) no longer hides this
1437
+ // scope's rows — every channel still needs its own reach cell, toggleable right here.
1438
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: scopeLabel }), _jsx(Text, { dimColor: true, children: ` ${rows.length}` })] }), _jsx(SearchRow, { filter: ui.filter, active: ui.searching, hint: "/ to search" }), modelVisibilityLine() && _jsx(Text, { dimColor: true, children: ` ${modelVisibilityLine()}` }), above > 0 && _jsx(ElisionMark, { count: above, side: "above" }), visible.map((row, index) => renderModelRow(row, ui.focus === "list" && start + index === ui.listAt)), below > 0 && _jsx(ElisionMark, { count: below, side: "below", hint: "/ to search" }), rows.length === 0 && _jsx(Text, { dimColor: true, children: " no models match" })] }));
1268
1439
  }
1269
1440
  if (ui.view === "shapes") {
1270
1441
  const rows = shapeList();
@@ -1281,7 +1452,9 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
1281
1452
  if (overlay) {
1282
1453
  if (overlay.kind === "candidates") {
1283
1454
  const rows = overlay.rows.filter((candidate) => matches(candidate.label, ui.filter));
1284
- return rows[overlay.at] ? [rows[overlay.at].label] : [];
1455
+ const ledger = overlay.ledger.filter((line) => matches(line, ui.filter));
1456
+ const line = rows[overlay.at]?.label ?? ledger[overlay.at - rows.length];
1457
+ return line ? [line] : [];
1285
1458
  }
1286
1459
  if (overlay.kind === "prefer") {
1287
1460
  return overlay.chain.length ? [`chain: ${overlay.chain.join(" → ")}`] : [];
@@ -1405,7 +1578,7 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
1405
1578
  ? _jsx(Text, { color: INK.warn, children: ` ! ${clip(ui.notice, width - 4)}` })
1406
1579
  : details.map((line) => _jsx(Text, { dimColor: true, children: ` ${clip(line, width - 2)}` }, line)), _jsx(KeyBar, { binds: keyBinds() })] }));
1407
1580
  }
1408
- export async function runFleetInkEditor({ ageMs, adapters, health, initialDenyAdapters, initialDenyModels, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, entry = "probe", initialJudge = "", judgeSeats = [], initialInput = [], input, output, debug = false, }) {
1581
+ export async function runFleetInkEditor({ ageMs, adapters, health, initialDenyAdapters, initialDenyModels, initialDenyWorkersAdapters = [], initialDenyWorkersModels = [], modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, stagedRouting, entry = "probe", initialJudge = "", judgeSeats = [], initialInput = [], input, output, debug = false, }) {
1409
1582
  const agents = adapters.flatMap((adapter) => {
1410
1583
  const state = health[adapter.id];
1411
1584
  if (!state?.installed)
@@ -1429,7 +1602,7 @@ export async function runFleetInkEditor({ ageMs, adapters, health, initialDenyAd
1429
1602
  // rebuilt from output.columns/rows so a pane resize re-renders at the new size. Same root
1430
1603
  // element type on every rerender ⇒ React keeps the component instance, so staged edits,
1431
1604
  // cursor, and overlays all survive the resize.
1432
- const editorElement = () => (_jsx(FleetApp, { ageMs: ageMs, agents: agents, initialDenyAdapters: initialDenyAdapters, initialDenyModels: initialDenyModels, modelGroups: declaredModelGroups, initialMode: initialMode, modeOptions: modeOptions, initialMap: initialMap, modePreview: modePreview, shapeRows: shapeRows, candidatesForShape: candidatesForShape, preferOptionsForShape: preferOptionsForShape, initialSteering: initialSteering, steeringOptionsFor: steeringOptionsFor, reviewOverlay: reviewOverlay, reloadGuard: reloadGuard, entry: entry, initialJudge: initialJudge, judgeSeats: judgeSeats,
1605
+ const editorElement = () => (_jsx(FleetApp, { ageMs: ageMs, agents: agents, initialDenyAdapters: initialDenyAdapters, initialDenyModels: initialDenyModels, initialDenyWorkersAdapters: initialDenyWorkersAdapters, initialDenyWorkersModels: initialDenyWorkersModels, 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,
1433
1606
  // v1.90.9: cap every list to the terminal minus chrome so the cursor and header can
1434
1607
  // never scroll off-screen (omp's 218-model list did exactly that pre-cap).
1435
1608
  // 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.5.4",
3
+ "version": "2.5.5",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",