tickmarkr 2.5.4 → 2.5.6

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 (78) hide show
  1. package/README.md +3 -1
  2. package/dist/adapters/registry.js +6 -1
  3. package/dist/adapters/types.d.ts +3 -0
  4. package/dist/cli/commands/approve.d.ts +1 -0
  5. package/dist/cli/commands/approve.js +118 -9
  6. package/dist/cli/commands/doctor.d.ts +10 -0
  7. package/dist/cli/commands/doctor.js +44 -29
  8. package/dist/cli/commands/fleet.js +199 -33
  9. package/dist/cli/commands/init.js +196 -6
  10. package/dist/cli/commands/plan.js +20 -4
  11. package/dist/cli/commands/resume.js +4 -2
  12. package/dist/cli/commands/run.js +11 -2
  13. package/dist/cli/commands/verify.js +102 -81
  14. package/dist/cli/help.d.ts +2 -0
  15. package/dist/cli/help.js +3 -1
  16. package/dist/config/config.d.ts +25 -8
  17. package/dist/config/config.js +41 -29
  18. package/dist/config/fleet-overlay.d.ts +3 -9
  19. package/dist/config/fleet-overlay.js +114 -19
  20. package/dist/config/fleet-why.d.ts +7 -0
  21. package/dist/config/fleet-why.js +5 -0
  22. package/dist/drivers/index.d.ts +15 -1
  23. package/dist/drivers/index.js +39 -10
  24. package/dist/drivers/orca.d.ts +119 -10
  25. package/dist/drivers/orca.js +781 -110
  26. package/dist/gates/baseline.d.ts +17 -3
  27. package/dist/gates/baseline.js +63 -15
  28. package/dist/gates/cache.d.ts +101 -0
  29. package/dist/gates/cache.js +401 -0
  30. package/dist/gates/llm.d.ts +3 -0
  31. package/dist/gates/llm.js +11 -0
  32. package/dist/gates/review.d.ts +28 -3
  33. package/dist/gates/review.js +106 -14
  34. package/dist/gates/run-gates.d.ts +9 -1
  35. package/dist/gates/run-gates.js +407 -102
  36. package/dist/gates/test-manifest.d.ts +128 -0
  37. package/dist/gates/test-manifest.js +463 -0
  38. package/dist/gates/test-reporter.d.ts +4 -0
  39. package/dist/gates/test-reporter.js +57 -0
  40. package/dist/graph/graph.d.ts +2 -0
  41. package/dist/graph/graph.js +45 -1
  42. package/dist/route/preference.d.ts +22 -1
  43. package/dist/route/preference.js +123 -25
  44. package/dist/route/router.js +31 -6
  45. package/dist/run/consult.js +5 -4
  46. package/dist/run/daemon.d.ts +15 -0
  47. package/dist/run/daemon.js +638 -167
  48. package/dist/run/execution-budget.d.ts +25 -0
  49. package/dist/run/execution-budget.js +142 -0
  50. package/dist/run/git.d.ts +50 -1
  51. package/dist/run/git.js +131 -12
  52. package/dist/run/journal.d.ts +16 -2
  53. package/dist/run/journal.js +115 -25
  54. package/dist/run/lease.d.ts +58 -0
  55. package/dist/run/lease.js +310 -0
  56. package/dist/run/merge.d.ts +2 -0
  57. package/dist/run/merge.js +91 -3
  58. package/dist/run/operator-state.d.ts +11 -0
  59. package/dist/run/operator-state.js +17 -3
  60. package/dist/run/recovery.d.ts +8 -0
  61. package/dist/run/recovery.js +25 -0
  62. package/dist/run/repair-selection.d.ts +12 -0
  63. package/dist/run/repair-selection.js +56 -0
  64. package/dist/run/stall.d.ts +6 -1
  65. package/dist/run/stall.js +60 -3
  66. package/dist/tui/cockpit/board.d.ts +96 -0
  67. package/dist/tui/cockpit/board.js +346 -0
  68. package/dist/tui/cockpit/decision-actions.js +2 -0
  69. package/dist/tui/cockpit/layout.d.ts +5 -1
  70. package/dist/tui/cockpit/layout.js +8 -3
  71. package/dist/tui/cockpit/live-runtime.js +71 -21
  72. package/dist/tui/cockpit/run-view.d.ts +7 -5
  73. package/dist/tui/cockpit/run-view.js +12 -11
  74. package/dist/tui/ink/fleet-app.d.ts +49 -27
  75. package/dist/tui/ink/fleet-app.js +229 -38
  76. package/package.json +2 -2
  77. package/skills/tickmarkr-overseer/SKILL.md +173 -113
  78. package/skills/tickmarkr-overseer/scripts/grade-ci.sh +29 -2
@@ -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, initialAllowOut = [], 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,12 @@ 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
+ allowOut: new Set(initialAllowOut),
49
+ denyWorkersAdapters: new Set(initialDenyWorkersAdapters),
50
+ denyWorkersModels: new Set(initialDenyWorkersModels),
45
51
  classifications: [],
46
52
  selectedMode: initialMode,
47
53
  map: structuredClone(initialMap),
@@ -59,6 +65,9 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
59
65
  // chip, and the empty-`w` notice all read it. Counts staged EDITS, not diff hunks.
60
66
  const initialDenySet = new Set(initialDenyAdapters);
61
67
  const initialDenyModelSet = new Set(initialDenyModels);
68
+ const initialAllowOutSet = new Set(initialAllowOut);
69
+ const initialDenyWorkersAdapterSet = new Set(initialDenyWorkersAdapters);
70
+ const initialDenyWorkersModelSet = new Set(initialDenyWorkersModels);
62
71
  const stagedCount = () => {
63
72
  let n = ui.classifications.length
64
73
  + (ui.selectedMode !== initialMode ? 1 : 0)
@@ -75,6 +84,21 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
75
84
  for (const model of initialDenyModelSet)
76
85
  if (!ui.denyModels.has(model))
77
86
  n += 1;
87
+ for (const key of initialAllowOutSet)
88
+ if (!ui.allowOut.has(key))
89
+ n += 1;
90
+ for (const adapter of ui.denyWorkersAdapters)
91
+ if (!initialDenyWorkersAdapterSet.has(adapter))
92
+ n += 1;
93
+ for (const adapter of initialDenyWorkersAdapterSet)
94
+ if (!ui.denyWorkersAdapters.has(adapter))
95
+ n += 1;
96
+ for (const model of ui.denyWorkersModels)
97
+ if (!initialDenyWorkersModelSet.has(model))
98
+ n += 1;
99
+ for (const model of initialDenyWorkersModelSet)
100
+ if (!ui.denyWorkersModels.has(model))
101
+ n += 1;
78
102
  for (const shape of new Set([...Object.keys(initialMap), ...Object.keys(ui.map)])) {
79
103
  // {} ≡ absent: pin-then-auto leaves an empty entry that writes nothing — not staged work
80
104
  if (JSON.stringify(initialMap[shape] ?? {}) !== JSON.stringify(ui.map[shape] ?? {}))
@@ -87,17 +111,55 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
87
111
  return n;
88
112
  };
89
113
  // ── derived data ───────────────────────────────────────────────────────────
90
- const enabledGroups = () => modelGroups.filter((group) => !ui.deny.has(group.adapter));
114
+ // OBS-994/FL-1 repair: a flatly (or workers-) denied adapter's channels stay VISIBLE here —
115
+ // every channel needs a reach cell, and Space below can only cycle a scope it can see.
116
+ const enabledGroups = () => modelGroups;
91
117
  const scopedGroups = () => {
92
118
  if (ui.adapterAt === -1)
93
119
  return enabledGroups();
94
120
  const group = modelGroups[ui.adapterAt];
95
- return group && !ui.deny.has(group.adapter) ? [group] : [];
121
+ return group ? [group] : [];
122
+ };
123
+ // LEG2-T3 finding 1: a channel's reach and every reason come from the exclusion collector over
124
+ // the STAGED policy (fleet loads the candidate bytes; see stagedRouting) and the recorded
125
+ // identity — never literal membership of the staged sets, which missed identity, bare-model and
126
+ // family entries and could not tell an allowlist exclusion from a deny.
127
+ let routingMemo = null;
128
+ const stagedPolicy = () => {
129
+ const deny = stagedDeny();
130
+ const key = JSON.stringify(deny);
131
+ if (routingMemo?.key !== key) {
132
+ routingMemo = {
133
+ key,
134
+ policy: stagedRouting
135
+ ? stagedRouting(deny)
136
+ : { ok: true, routing: { deny: { adapters: deny.adapters, models: deny.models, workers: { adapters: deny.workersAdapters, models: deny.workersModels } } } },
137
+ };
138
+ }
139
+ return routingMemo.policy;
140
+ };
141
+ const reachFor = (adapter, model, identity) => {
142
+ const policy = stagedPolicy();
143
+ if (!policy.ok)
144
+ return { reach: "unknown", reasons: [`preview unavailable (${policy.error})`], uncoveredAlias: false };
145
+ const channel = { adapter, model, ...(identity !== undefined ? { identity } : {}) };
146
+ const worker = exclusionCollector(channel, policy.routing, "worker");
147
+ const allSeats = exclusionCollector(channel, policy.routing, "judge");
148
+ const reach = allSeats.some((scope) => scope.by === "deny") ? "out-all"
149
+ : allSeats.length ? "out-allow"
150
+ : worker.length ? "out-workers"
151
+ : "in";
152
+ return {
153
+ reach,
154
+ reasons: worker.map(exclusionReason),
155
+ uncoveredAlias: identity !== undefined && !worker.some((scope) => scope.by === "deny"),
156
+ };
96
157
  };
97
158
  const groupRows = (group) => {
98
159
  const rows = group.rows.map((row) => {
99
160
  const staged = ui.classifications.find((classification) => classification.adapter === group.adapter
100
161
  && (classification.model === row.model || classification.model === row.classifyModel));
162
+ const reach = reachFor(group.adapter, row.model, row.evidence?.identity);
101
163
  return {
102
164
  adapter: group.adapter,
103
165
  model: row.model,
@@ -110,18 +172,21 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
110
172
  variants: row.variants,
111
173
  foldedModels: row.foldedModels,
112
174
  score: row.score,
113
- denied: ui.denyModels.has(`${group.adapter}:${row.model}`),
175
+ denied: reach.reach !== "in",
176
+ ...reach,
114
177
  };
115
178
  });
116
179
  const known = new Set(rows.flatMap((row) => row.classifyModel ? [row.model, row.classifyModel] : [row.model]));
117
180
  for (const staged of ui.classifications) {
118
181
  if (staged.adapter === group.adapter && !known.has(staged.model)) {
182
+ const reach = reachFor(group.adapter, staged.model);
119
183
  rows.push({
120
184
  adapter: group.adapter,
121
185
  model: staged.model,
122
186
  tier: staged.tier,
123
187
  channel: group.channel,
124
- denied: ui.denyModels.has(`${group.adapter}:${staged.model}`),
188
+ denied: reach.reach !== "in",
189
+ ...reach,
125
190
  });
126
191
  }
127
192
  }
@@ -151,8 +216,15 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
151
216
  return groups.flatMap(groupRows)
152
217
  .filter((row) => ui.showAll || row.tier !== undefined || retiredModelReason(row.model) === null).length;
153
218
  };
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() });
219
+ // three preview callbacks share it in lockstep — the staged deny truth every preview ranks under.
220
+ // LEG2-T3 finding 2: all four scopes travel distinct; the preview loads them as written.
221
+ const stagedDeny = () => ({
222
+ adapters: [...ui.deny].sort(),
223
+ models: [...ui.denyModels].sort(),
224
+ workersAdapters: [...ui.denyWorkersAdapters].sort(),
225
+ workersModels: [...ui.denyWorkersModels].sort(),
226
+ allowOut: [...ui.allowOut].sort(),
227
+ });
156
228
  const shapeList = () => shapeRows(ui.selectedMode, ui.map, stagedDeny());
157
229
  const steeringList = () => [
158
230
  ...STEERING_KEYS.map((key) => ({
@@ -179,6 +251,9 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
179
251
  const editorState = () => ({
180
252
  denyAdapters: [...ui.deny].sort(),
181
253
  denyModels: [...ui.denyModels].sort(),
254
+ allowOut: [...ui.allowOut].sort(),
255
+ denyWorkersAdapters: [...ui.denyWorkersAdapters].sort(),
256
+ denyWorkersModels: [...ui.denyWorkersModels].sort(),
182
257
  classifications: ui.classifications.map((classification) => classification.vendor && classification.channel
183
258
  ? {
184
259
  ...classification,
@@ -605,12 +680,13 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
605
680
  }
606
681
  if (overlay.kind === "candidates") {
607
682
  const rows = overlay.rows.filter((candidate) => matches(candidate.label, ui.filter));
683
+ const ledger = overlay.ledger.filter((line) => matches(line, ui.filter));
608
684
  if (key.escape) {
609
685
  setOverlay(null);
610
686
  return;
611
687
  }
612
688
  if (key.downArrow) {
613
- overlay.at = Math.min(overlay.at + 1, Math.max(rows.length - 1, 0));
689
+ overlay.at = Math.min(overlay.at + 1, Math.max(rows.length + ledger.length - 1, 0));
614
690
  bump();
615
691
  return;
616
692
  }
@@ -644,7 +720,8 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
644
720
  }
645
721
  return;
646
722
  }
647
- editFilter((f) => overlay.rows.filter((candidate) => matches(candidate.label, f)).length, (max) => {
723
+ editFilter((f) => overlay.rows.filter((candidate) => matches(candidate.label, f)).length
724
+ + overlay.ledger.filter((line) => matches(line, f)).length, (max) => {
648
725
  overlay.at = Math.min(overlay.at, max);
649
726
  });
650
727
  return;
@@ -870,12 +947,18 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
870
947
  if (agents.find((agent) => agent.id === id)?.authed === false) {
871
948
  ui.notice = `${id} is not authed — Space only toggles fleet membership; re-auth the ${id} CLI, then run tickmarkr doctor`;
872
949
  }
873
- const next = new Set(ui.deny);
874
- if (next.has(id))
875
- next.delete(id);
876
- else
877
- next.add(id);
878
- ui.deny = next;
950
+ // OBS-1046: an adapter routing.allow leaves out comes back in by clearing THAT reason
951
+ if (ui.allowOut.has(id)) {
952
+ ui.allowOut = new Set([...ui.allowOut].filter((key) => key !== id));
953
+ }
954
+ else {
955
+ const next = new Set(ui.deny);
956
+ if (next.has(id))
957
+ next.delete(id);
958
+ else
959
+ next.add(id);
960
+ ui.deny = next;
961
+ }
879
962
  clampList();
880
963
  bump();
881
964
  return;
@@ -931,13 +1014,76 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
931
1014
  beginClassification(row.adapter, row.model);
932
1015
  return;
933
1016
  }
1017
+ // OBS-994/FL-1: membership can't fix an auth failure either — the row's reach stays
1018
+ // whatever it is and the operator is pointed at doctor instead of a toggle that lies.
1019
+ if (row.evidence?.unauthed !== undefined) {
1020
+ ui.notice = `${row.adapter}:${row.model} is unauthed — re-probe with tickmarkr doctor before its fleet reach can change`;
1021
+ bump();
1022
+ return;
1023
+ }
934
1024
  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;
1025
+ // OBS-994/FL-1, LEG2-T3 finding 3: Space cycles in → out(workers) → out(all) → in by editing
1026
+ // only THIS channel's own entries in the per-model scopes — its adapter:model key, or an
1027
+ // identity/bare-model entry no other browser row matches. An adapter-wide or shared entry
1028
+ // is never promoted or cleared from a channel row: that would move sibling channels and
1029
+ // wipe reasons that are not this row's to clear. The row's detail line names the entry each
1030
+ // press edited; a refused press names the shared entry in the notice instead.
1031
+ const self = { adapter: row.adapter, model: row.model, identity: row.evidence?.identity };
1032
+ const others = modelGroups.flatMap((group) => group.rows
1033
+ .filter((other) => group.adapter !== row.adapter || other.model !== row.model)
1034
+ .map((other) => ({ adapter: group.adapter, model: other.model, identity: other.evidence?.identity })));
1035
+ // Judge c3 (R107): exactly ONE selected entry per press — the channel's adapter:model key when
1036
+ // it is a candidate, else the first candidate in sorted order. The cycle is three states over
1037
+ // two scopes, so the out-workers → out-all press re-scopes that one entry from
1038
+ // routing.deny.workers.models to routing.deny.models; every other press adds or clears one
1039
+ // entry in one scope. The channel's other entries keep their reasons for later presses.
1040
+ // LEG2-T3 round 2 finding 1: promotion widens the selected ban as spelled — a bare model id
1041
+ // then excludes every seat serving that model — while a CLEAR only takes an entry no other
1042
+ // row matches, so no sibling is ever admitted by a channel toggle.
1043
+ const select = (entries, ownOnly) => {
1044
+ const candidates = [...entries].filter((entry) => entryMatchesChannel(entry, self, true)
1045
+ && (!ownOnly || !others.some((other) => entryMatchesChannel(entry, other, true))));
1046
+ return candidates.includes(id) ? id : candidates.sort()[0];
1047
+ };
1048
+ if (row.reach === "unknown") {
1049
+ ui.notice = `${id} reach is unknown — Space does not toggle it until the staged overlay loads: ${row.reasons.join("; ")}`;
1050
+ bump();
1051
+ return;
1052
+ }
1053
+ if (row.reach === "in") {
1054
+ ui.denyWorkersModels = new Set([...ui.denyWorkersModels, id]);
1055
+ ui.lastEdit = { id, text: `space: added ${id} to routing.deny.workers.models` };
1056
+ }
1057
+ else if (row.reach === "out-workers") {
1058
+ const entry = select(ui.denyWorkersModels, false) ?? id;
1059
+ ui.denyWorkersModels = new Set([...ui.denyWorkersModels].filter((staged) => staged !== entry));
1060
+ ui.denyModels = new Set([...ui.denyModels, entry]);
1061
+ ui.lastEdit = { id, text: `space: moved ${entry} to routing.deny.models` };
1062
+ }
1063
+ else {
1064
+ // round 2 finding 2: either flat list may carry this channel's own reason; OBS-1046: an
1065
+ // allow exclusion is the LAST reason cleared, and never together with an authored deny
1066
+ const fromModels = select(ui.denyModels, true);
1067
+ const fromAdapters = fromModels === undefined ? select(ui.deny, true) : undefined;
1068
+ const fromAllow = fromModels === undefined && fromAdapters === undefined ? select(ui.allowOut, true) : undefined;
1069
+ const entry = fromModels ?? fromAdapters ?? fromAllow;
1070
+ if (entry === undefined) {
1071
+ const shared = ui.deny.has(row.adapter) || ui.allowOut.has(row.adapter)
1072
+ ? `every ${row.adapter} channel is out together — take the adapter back in on the rail`
1073
+ : "a shared entry covers other channels too";
1074
+ ui.notice = `${id} stays out — ${row.reasons.join("; ")} — Space edits only this channel's own entries; ${shared}`;
1075
+ bump();
1076
+ return;
1077
+ }
1078
+ if (fromModels !== undefined)
1079
+ ui.denyModels = new Set([...ui.denyModels].filter((staged) => staged !== entry));
1080
+ else if (fromAdapters !== undefined)
1081
+ ui.deny = new Set([...ui.deny].filter((staged) => staged !== entry));
1082
+ else
1083
+ ui.allowOut = new Set([...ui.allowOut].filter((staged) => staged !== entry));
1084
+ const scope = fromModels !== undefined ? "routing.deny.models" : fromAdapters !== undefined ? "routing.deny.adapters" : "routing.allow";
1085
+ ui.lastEdit = { id, text: `space: cleared ${entry} from ${scope}` };
1086
+ }
941
1087
  bump();
942
1088
  return;
943
1089
  }
@@ -1051,6 +1197,7 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
1051
1197
  shape,
1052
1198
  rows: picked.rows,
1053
1199
  excludedNote: picked.excludedNote,
1200
+ ledger: picked.ledger ?? [],
1054
1201
  // OBS-525: an existing pool round-trips — reopening the picker starts from the staged
1055
1202
  // channels instead of an empty selection that would silently replace the pool with a pin.
1056
1203
  chain: ui.map[shape]?.pool?.channels.slice() ?? [],
@@ -1116,10 +1263,37 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
1116
1263
  return "sub";
1117
1264
  return usd;
1118
1265
  };
1266
+ // OBS-994/FL-1: the reach line — every collector scope that excludes this row, each with its
1267
+ // config path and matching entry (an unauthed row instead points at the doctor re-probe).
1268
+ const reachDetail = (row) => {
1269
+ if (row.evidence?.unauthed !== undefined)
1270
+ return "unauthed — re-probe with tickmarkr doctor (Space does not toggle reach)";
1271
+ const reasons = row.reasons.join("; ");
1272
+ if (row.reach === "out-all")
1273
+ return `reach: out · all seats — ${reasons}`;
1274
+ if (row.reach === "out-allow")
1275
+ return `reach: out · all seats · allow — ${reasons}`;
1276
+ if (row.reach === "out-workers")
1277
+ return `reach: out · workers only (judge/review/consult unaffected) — ${reasons}`;
1278
+ if (row.reach === "unknown")
1279
+ return `reach: unknown — ${reasons}`;
1280
+ return "reach: in — Space cycles to out · workers";
1281
+ };
1282
+ const reachCell = (row) => row.reach === "in" || row.reach === "unknown" ? row.reach : `out ${row.reach === "out-workers" ? "workers" : row.reach.slice(4)}`;
1283
+ const identityDetail = (row) => {
1284
+ if (row.evidence?.identity === undefined)
1285
+ return "";
1286
+ return row.uncoveredAlias
1287
+ ? `resolved identity ${row.evidence.identity} — no deny entry covers it`
1288
+ : `resolved identity ${row.evidence.identity}`;
1289
+ };
1119
1290
  const modelDetail = (row) => {
1120
1291
  const parts = [
1121
1292
  `${row.adapter}:${row.model}`,
1122
1293
  row.tier ?? "unclassified",
1294
+ ui.lastEdit?.id === `${row.adapter}:${row.model}` ? ui.lastEdit.text : "",
1295
+ identityDetail(row),
1296
+ row.tier ? reachDetail(row) : "",
1123
1297
  row.variants?.length ? `variants ${row.variants.join(", ")}` : "",
1124
1298
  row.classifyModel ? `classify writes ${row.classifyModel}` : "",
1125
1299
  row.foldedModels?.length ? `${row.foldedModels.length} gateway ids folded: ${row.foldedModels.join(", ")}` : "",
@@ -1138,20 +1312,22 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
1138
1312
  const tier = tierCell(row);
1139
1313
  const showProbe = bodyW >= 76;
1140
1314
  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);
1315
+ // every cell accounted for: pointer 2 + glyph 1 + gap 1 + tier 11 + reach 12 + ctx 6 (+ price 12 + probe 7)
1316
+ const nameW = bodyW - 4 - 11 - 12 - 6 - (showPrice ? 12 : 0) - (showProbe ? 7 : 0);
1143
1317
  // OBS-531: deep router ids (omp/prime-agent) clip tail-preserving — the LAST segment is the
1144
1318
  // distinguishing half; end-clipping rendered ten identical "prime-agent/anthropic/claude-…" rows.
1145
1319
  const folded = row.foldedModels?.length ?? 1;
1146
1320
  const name = clipPathTail(`${row.adapter}/${row.model}${folded > 1 ? ` ×${folded}` : ""}`, nameW);
1147
1321
  const prefixLen = Math.min(name.length, row.adapter.length + 1);
1148
- return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Pointer, { on: selected }), row.denied
1322
+ return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Pointer, { on: selected }), row.reach === "out-all" || row.reach === "out-allow"
1149
1323
  ? _jsx(Glyph, { kind: "off" })
1150
- : row.evidence?.unauthed !== undefined
1324
+ : row.evidence?.unauthed !== undefined || row.reach === "unknown"
1151
1325
  ? _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}`));
1326
+ : row.reach === "out-workers"
1327
+ ? _jsx(Glyph, { kind: "warn" })
1328
+ : row.tier
1329
+ ? _jsx(Glyph, { kind: "on" })
1330
+ : _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
1331
  };
1156
1332
  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
1333
  const renderShapeRow = (row, selected, rowW = bodyW - 2) => {
@@ -1175,7 +1351,7 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
1175
1351
  const railAdapter = (index, railIndex) => {
1176
1352
  const group = modelGroups[index];
1177
1353
  const agent = agents.find((candidate) => candidate.id === group.adapter);
1178
- const denied = ui.deny.has(group.adapter);
1354
+ const denied = ui.deny.has(group.adapter) || ui.allowOut.has(group.adapter);
1179
1355
  const active = ui.view === "models" && ui.adapterAt === index;
1180
1356
  const selected = ui.focus === "rail" && ui.railAt === railIndex;
1181
1357
  return (_jsxs(Text, { children: [selected ? _jsx(Text, { color: INK.brand, children: "❯ " }) : _jsx(Text, { children: " " }), denied ? _jsx(Glyph, { kind: "off" }) : agent?.authed === false ? _jsx(Glyph, { kind: "fail" }) : _jsx(Glyph, { kind: "on" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: active, color: active ? INK.brand : undefined, dimColor: denied, children: padCell(group.adapter, 12) }), _jsx(Text, { dimColor: true, children: padCellStart(String(visibleCount(index)), 4) })] }, group.adapter));
@@ -1213,12 +1389,24 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
1213
1389
  }
1214
1390
  if (overlay.kind === "candidates") {
1215
1391
  const rows = overlay.rows.filter((candidate) => matches(candidate.label, ui.filter));
1216
- const { visible, start, above, below } = windowRows(rows, overlay.at, capacity);
1392
+ // the shape-wide captions, bounded to half the list; per-channel ledger rows are list items below
1393
+ const notes = overlay.excludedNote?.split("\n") ?? [];
1394
+ const noteRoom = Math.max(2, Math.floor(capacity / 2));
1395
+ const noteLines = notes.length > noteRoom
1396
+ ? [...notes.slice(0, noteRoom - 1), `… +${notes.length - noteRoom + 1} more notes`]
1397
+ : notes;
1398
+ const items = [
1399
+ ...rows.map((candidate) => ({ candidate, line: candidate.label })),
1400
+ ...overlay.ledger.filter((line) => matches(line, ui.filter)).map((line) => ({ candidate: undefined, line })),
1401
+ ];
1402
+ const { visible, start, above, below } = windowRows(items, overlay.at, Math.max(1, capacity - noteLines.length + 1));
1217
1403
  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);
1404
+ 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
1405
  const selected = start + index === overlay.at;
1406
+ if (candidate === undefined) {
1407
+ 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}`));
1408
+ }
1409
+ const at = overlay.chain.indexOf(candidate.id);
1222
1410
  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
1411
  }), below > 0 && _jsx(ElisionMark, { count: below, side: "below" })] }));
1224
1412
  }
@@ -1263,8 +1451,9 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
1263
1451
  const scopeLabel = ui.adapterAt === -1
1264
1452
  ? "All models"
1265
1453
  : 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" })] }));
1454
+ // OBS-994/FL-1 repair: an adapter-wide deny (flat or workers) no longer hides this
1455
+ // scope's rows — every channel still needs its own reach cell, toggleable right here.
1456
+ 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
1457
  }
1269
1458
  if (ui.view === "shapes") {
1270
1459
  const rows = shapeList();
@@ -1281,7 +1470,9 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
1281
1470
  if (overlay) {
1282
1471
  if (overlay.kind === "candidates") {
1283
1472
  const rows = overlay.rows.filter((candidate) => matches(candidate.label, ui.filter));
1284
- return rows[overlay.at] ? [rows[overlay.at].label] : [];
1473
+ const ledger = overlay.ledger.filter((line) => matches(line, ui.filter));
1474
+ const line = rows[overlay.at]?.label ?? ledger[overlay.at - rows.length];
1475
+ return line ? [line] : [];
1285
1476
  }
1286
1477
  if (overlay.kind === "prefer") {
1287
1478
  return overlay.chain.length ? [`chain: ${overlay.chain.join(" → ")}`] : [];
@@ -1405,7 +1596,7 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
1405
1596
  ? _jsx(Text, { color: INK.warn, children: ` ! ${clip(ui.notice, width - 4)}` })
1406
1597
  : details.map((line) => _jsx(Text, { dimColor: true, children: ` ${clip(line, width - 2)}` }, line)), _jsx(KeyBar, { binds: keyBinds() })] }));
1407
1598
  }
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, }) {
1599
+ export async function runFleetInkEditor({ ageMs, adapters, health, initialDenyAdapters, initialDenyModels, initialAllowOut = [], 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
1600
  const agents = adapters.flatMap((adapter) => {
1410
1601
  const state = health[adapter.id];
1411
1602
  if (!state?.installed)
@@ -1429,7 +1620,7 @@ export async function runFleetInkEditor({ ageMs, adapters, health, initialDenyAd
1429
1620
  // rebuilt from output.columns/rows so a pane resize re-renders at the new size. Same root
1430
1621
  // element type on every rerender ⇒ React keeps the component instance, so staged edits,
1431
1622
  // 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,
1623
+ const editorElement = () => (_jsx(FleetApp, { ageMs: ageMs, agents: agents, initialDenyAdapters: initialDenyAdapters, initialDenyModels: initialDenyModels, initialAllowOut: initialAllowOut, 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
1624
  // v1.90.9: cap every list to the terminal minus chrome so the cursor and header can
1434
1625
  // never scroll off-screen (omp's 218-model list did exactly that pre-cap).
1435
1626
  // 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.6",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "homepage": "https://github.com/alzahrani-khalid/tickmarkr",
19
19
  "engines": {
20
- "node": ">=20"
20
+ "node": ">=20.3"
21
21
  },
22
22
  "bin": {
23
23
  "tickmarkr": "dist/cli/index.js",