tickmarkr 1.90.7 → 1.91.0

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.
@@ -4,6 +4,7 @@ import { PassThrough } from "node:stream";
4
4
  import { render, Text, useApp, useInput } from "ink";
5
5
  import { useRef, useState } from "react";
6
6
  import { MODEL_ID_RE } from "../../adapters/types.js";
7
+ import { retiredModelReason } from "../../adapters/model-lints.js";
7
8
  import { fleetFirstTouchProvenance } from "../../config/fleet-overlay.js";
8
9
  import { TIERS } from "../../graph/schema.js";
9
10
  import { FleetListScreen, FleetReviewScreen, ToggleMark } from "./components.js";
@@ -108,7 +109,7 @@ function splitSeat(seat) {
108
109
  const at = seat.indexOf(":");
109
110
  return { adapter: seat.slice(0, at), model: seat.slice(at + 1) };
110
111
  }
111
- export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, entry = "probe", initialJudge = "", judgeSeats = [], }) {
112
+ 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, }) {
112
113
  const { exit } = useApp();
113
114
  const [screen, setScreen] = useState(entry === "presets" ? "modes" : "probe");
114
115
  const screenRef = useRef(screen);
@@ -134,6 +135,10 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
134
135
  const tierCursorRef = useRef(0);
135
136
  const [tierCursor, setTierCursor] = useState(0);
136
137
  const pendingClassificationRef = useRef(null);
138
+ // OBS-508: the suggestion riding the in-flight classification (prefills tier + note), and the
139
+ // rows a bulk `s` staged while waiting on the one first-touch channel answer.
140
+ const pendingSuggestionRef = useRef(null);
141
+ const bulkRef = useRef(null);
137
142
  const noteRef = useRef("");
138
143
  const [note, setNote] = useState("");
139
144
  const [notice, setNotice] = useState("");
@@ -178,6 +183,9 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
178
183
  // type-to-search on the long lists — reset on every screen change (per-screen-visit filter)
179
184
  const filterRef = useRef("");
180
185
  const [filter, setFilter] = useState("");
186
+ // `a` on the models screen reveals retired/preview/non-worker shapes; sticky for the session.
187
+ const showAllModelsRef = useRef(false);
188
+ const [, setShowAllModels] = useState(false);
181
189
  const enabledModelGroups = () => modelGroups.filter((group) => !denyRef.current?.has(group.adapter));
182
190
  const stagedMap = () => mapRef.current;
183
191
  const currentModelRows = () => {
@@ -199,7 +207,12 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
199
207
  // type-to-search over the long lists: plain case-insensitive substring on the row's visible
200
208
  // label. Each filterable screen derives its rows through these so input and render agree.
201
209
  const matches = (label, f = filterRef.current) => f === "" || label.toLowerCase().includes(f.toLowerCase());
202
- const filteredModelRows = (f = filterRef.current) => currentModelRows().filter((row) => matches(row.model, f));
210
+ // Operator directive 2026-08-13: retired shapes (dated snapshots, previews, non-worker SKUs,
211
+ // legacy families) hide by DEFAULT — omp reports 218 ids and most can never carry a worker.
212
+ // `a` reveals them; a CLASSIFIED row is never hidden (a tiered dated snapshot was meant).
213
+ const filteredModelRows = (f = filterRef.current) => currentModelRows().filter((row) => matches(row.model, f)
214
+ && (showAllModelsRef.current || row.tier !== undefined || retiredModelReason(row.model) === null));
215
+ const hiddenModelCount = (f = filterRef.current) => currentModelRows().filter((row) => matches(row.model, f)).length - filteredModelRows(f).length;
203
216
  const filteredCandidates = (f = filterRef.current) => candidatesRef.current.filter((candidate) => matches(candidate.label, f));
204
217
  const filteredSteeringRows = (f = filterRef.current) => steeringRowsRef.current.filter((option) => matches(option, f));
205
218
  const judgeKeepRow = initialJudge ? `(keep default) ${initialJudge}` : "(keep default)";
@@ -266,9 +279,26 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
266
279
  setNotice("");
267
280
  showScreen("review");
268
281
  };
282
+ const stageSuggested = (group, rows, firstTouch) => {
283
+ const staged = rows.map((row) => ({
284
+ adapter: group.adapter,
285
+ model: row.model,
286
+ tier: row.suggestion.tier,
287
+ note: row.suggestion.note,
288
+ ...firstTouch,
289
+ }));
290
+ classificationsRef.current = [...classificationsRef.current, ...staged];
291
+ setClassificationRevision((revision) => revision + 1);
292
+ setNotice(`fleet: staged ${staged.length} catalog-suggested classification(s) — nothing writes before the review diff`);
293
+ };
269
294
  const beginClassification = (group, model) => {
270
- tierCursorRef.current = 0;
271
- setTierCursor(0);
295
+ // OBS-508: catalog evidence prefills the flow — tier cursor lands on the suggested band and
296
+ // (when the operator keeps that band) the provenance note arrives pre-typed. Free to override.
297
+ const suggestion = group.rows.find((row) => row.model === model)?.suggestion ?? null;
298
+ pendingSuggestionRef.current = suggestion;
299
+ const tierAt = suggestion ? Math.max(TIERS.indexOf(suggestion.tier), 0) : 0;
300
+ tierCursorRef.current = tierAt;
301
+ setTierCursor(tierAt);
272
302
  const firstTouch = !group.rows.some((row) => row.tier !== undefined);
273
303
  const pending = {
274
304
  adapter: group.adapter,
@@ -309,18 +339,39 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
309
339
  showScreen("steering");
310
340
  return;
311
341
  }
312
- if (key.escape && screenRef.current === "add-model") {
342
+ // The classification sub-flow (add-model → channel → tier → provenance) cancels back to the
343
+ // models list — before this, provenance's own "esc cancel" legend lied: Esc fell through to
344
+ // the walk-home branch in init entry and to QUIT in fleet entry, discarding staged work.
345
+ if (key.escape && (screenRef.current === "add-model" || screenRef.current === "channel" || screenRef.current === "tiers" || screenRef.current === "provenance")) {
346
+ noteRef.current = "";
347
+ setNote("");
313
348
  setNotice("");
349
+ pendingSuggestionRef.current = null;
350
+ bulkRef.current = null;
314
351
  showScreen("models");
315
352
  return;
316
353
  }
354
+ // v1.90.8 (operator field report): inside the init journey, one Esc must never discard the
355
+ // whole walk — from any walk screen it returns HOME to the presets screen; Esc there (or
356
+ // `q`/ctrl+c anywhere) remains the real quit. Sub-screen Escs above keep their tighter backs.
357
+ if (key.escape && entry === "presets" && screenRef.current !== "modes") {
358
+ setReview(null);
359
+ presetPickRef.current = true;
360
+ showScreen("modes");
361
+ return;
362
+ }
363
+ // While a type-to-search filter is LIVE, every printable char belongs to the search box:
364
+ // the letter aliases (j/k navigation, q quit, models' t/n/a) go dormant so "fake-n" or
365
+ // "qwen" can actually be typed — the k moved the cursor and the q quit the editor before
366
+ // this (operator field session, 2026-08-13). Arrows, Enter, Space, and Esc keep their roles.
367
+ const letterAlias = filterRef.current === "" ? input : "";
317
368
  const typing = screenRef.current === "provenance" || screenRef.current === "add-model";
318
- if (key.escape || (!typing && input === "q") || (key.ctrl && input === "c")) {
369
+ if (key.escape || (!typing && letterAlias === "q") || (key.ctrl && input === "c")) {
319
370
  finish({ kind: "quit" });
320
371
  return;
321
372
  }
322
373
  if (screenRef.current === "probe") {
323
- if (input === "r") {
374
+ if (letterAlias === "r") {
324
375
  finish({ kind: "refresh" });
325
376
  }
326
377
  else if (key.return) {
@@ -329,13 +380,13 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
329
380
  return;
330
381
  }
331
382
  if (screenRef.current === "agents") {
332
- if (key.downArrow || input === "j") {
383
+ if (key.downArrow || letterAlias === "j") {
333
384
  const next = Math.min(cursorRef.current + 1, Math.max(agents.length - 1, 0));
334
385
  cursorRef.current = next;
335
386
  setCursor(next);
336
387
  return;
337
388
  }
338
- if (key.upArrow || input === "k") {
389
+ if (key.upArrow || letterAlias === "k") {
339
390
  const next = Math.max(cursorRef.current - 1, 0);
340
391
  cursorRef.current = next;
341
392
  setCursor(next);
@@ -362,14 +413,14 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
362
413
  }
363
414
  if (screenRef.current === "models") {
364
415
  const rows = filteredModelRows();
365
- if (key.downArrow || input === "j") {
416
+ if (key.downArrow || letterAlias === "j") {
366
417
  const next = Math.min(modelCursorRef.current + 1, Math.max(rows.length - 1, 0));
367
418
  modelCursorRef.current = next;
368
419
  setModelCursor(next);
369
420
  setNotice("");
370
421
  return;
371
422
  }
372
- if (key.upArrow || input === "k") {
423
+ if (key.upArrow || letterAlias === "k") {
373
424
  const next = Math.max(modelCursorRef.current - 1, 0);
374
425
  modelCursorRef.current = next;
375
426
  setModelCursor(next);
@@ -378,7 +429,14 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
378
429
  }
379
430
  const group = enabledModelGroups()[modelGroupRef.current];
380
431
  const row = rows[modelCursorRef.current];
381
- if (input === " " && group && row?.tier) {
432
+ if (input === " " && group && row) {
433
+ if (!row.tier) {
434
+ // v1.90.9 (operator field report: "not selectable"): an unclassified model has no tier
435
+ // to route on — selecting IS classifying, so Space opens the same channel → tier →
436
+ // provenance flow as `t` instead of dying silently on a row drawn like a checkbox.
437
+ beginClassification(group, row.model);
438
+ return;
439
+ }
382
440
  const id = `${group.adapter}:${row.model}`;
383
441
  const next = new Set(denyModelsRef.current);
384
442
  if (next.has(id))
@@ -389,7 +447,10 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
389
447
  setDenyModels(next);
390
448
  return;
391
449
  }
392
- if (input === "t" && group && row) {
450
+ // Letter hotkeys yield to an ACTIVE search (letterAlias above): with a filter live, every
451
+ // printable char belongs to the search box — otherwise typing "anthropic" toggles show-all
452
+ // at the `a` and opens classify at the `t`.
453
+ if (letterAlias === "t" && group && row) {
393
454
  if (row.tier) {
394
455
  setNotice("fleet: tier reassignment on classified models is not supported in v1 — edit config directly");
395
456
  return;
@@ -397,13 +458,53 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
397
458
  beginClassification(group, row.model);
398
459
  return;
399
460
  }
400
- if (input === "n" && group) {
461
+ if (letterAlias === "n" && group) {
401
462
  modelIdRef.current = "";
402
463
  setModelId("");
403
464
  setNotice("");
404
465
  showScreen("add-model");
405
466
  return;
406
467
  }
468
+ if (letterAlias === "s" && group) {
469
+ // OBS-508: bulk-stage every VISIBLE unclassified model carrying a catalog suggestion —
470
+ // visible means the type-to-search filter and the retired-hide both scope the batch. The
471
+ // staging is consent-preserving: it lands in the same classifications list the single
472
+ // flow feeds, and only the review diff (y) ever writes.
473
+ const suggested = filteredModelRows()
474
+ .filter((candidate) => !candidate.tier && candidate.suggestion !== undefined);
475
+ if (!suggested.length) {
476
+ setNotice("fleet: no catalog tier suggestions among the visible unclassified models — evidence comes from the cached catalogs (AA index / API pricing)");
477
+ return;
478
+ }
479
+ const firstTouch = !group.rows.some((candidate) => candidate.tier !== undefined);
480
+ if (!firstTouch) {
481
+ stageSuggested(group, suggested);
482
+ return;
483
+ }
484
+ if (!group.vendor) {
485
+ setNotice(`fleet: ${group.adapter} has no vendor declaration — classification cannot be saved`);
486
+ return;
487
+ }
488
+ const answered = channelByAdapterRef.current[group.adapter];
489
+ if (answered) {
490
+ stageSuggested(group, suggested, { vendor: group.vendor, channel: answered });
491
+ return;
492
+ }
493
+ bulkRef.current = { group, rows: suggested };
494
+ channelCursorRef.current = 0;
495
+ setChannelCursor(0);
496
+ setNotice("");
497
+ showScreen("channel");
498
+ return;
499
+ }
500
+ if (letterAlias === "a") {
501
+ showAllModelsRef.current = !showAllModelsRef.current;
502
+ setShowAllModels(showAllModelsRef.current);
503
+ const clamped = Math.min(modelCursorRef.current, Math.max(filteredModelRows().length - 1, 0));
504
+ modelCursorRef.current = clamped;
505
+ setModelCursor(clamped);
506
+ return;
507
+ }
407
508
  if (key.return) {
408
509
  if (modelGroupRef.current + 1 < enabledModelGroups().length) {
409
510
  const next = modelGroupRef.current + 1;
@@ -455,43 +556,60 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
455
556
  return;
456
557
  }
457
558
  if (screenRef.current === "channel") {
458
- if (key.downArrow || input === "j") {
559
+ if (key.downArrow || letterAlias === "j") {
459
560
  const next = Math.min(channelCursorRef.current + 1, CHANNELS.length - 1);
460
561
  channelCursorRef.current = next;
461
562
  setChannelCursor(next);
462
563
  return;
463
564
  }
464
- if (key.upArrow || input === "k") {
565
+ if (key.upArrow || letterAlias === "k") {
465
566
  const next = Math.max(channelCursorRef.current - 1, 0);
466
567
  channelCursorRef.current = next;
467
568
  setChannelCursor(next);
468
569
  return;
469
570
  }
470
- if (key.return && pendingClassificationRef.current) {
571
+ if (key.return) {
471
572
  const channel = CHANNELS[channelCursorRef.current];
472
- channelByAdapterRef.current[pendingClassificationRef.current.adapter] = channel;
473
- pendingClassificationRef.current.channel = channel;
474
- showScreen("tiers");
573
+ // OBS-508: a bulk stage waiting on the one first-touch channel answer resumes here.
574
+ const bulk = bulkRef.current;
575
+ if (bulk?.group.vendor) {
576
+ channelByAdapterRef.current[bulk.group.adapter] = channel;
577
+ stageSuggested(bulk.group, bulk.rows, { vendor: bulk.group.vendor, channel });
578
+ bulkRef.current = null;
579
+ showScreen("models");
580
+ return;
581
+ }
582
+ if (pendingClassificationRef.current) {
583
+ channelByAdapterRef.current[pendingClassificationRef.current.adapter] = channel;
584
+ pendingClassificationRef.current.channel = channel;
585
+ showScreen("tiers");
586
+ }
475
587
  }
476
588
  return;
477
589
  }
478
590
  if (screenRef.current === "tiers") {
479
- if (key.downArrow || input === "j") {
591
+ if (key.downArrow || letterAlias === "j") {
480
592
  const next = Math.min(tierCursorRef.current + 1, TIERS.length - 1);
481
593
  tierCursorRef.current = next;
482
594
  setTierCursor(next);
483
595
  return;
484
596
  }
485
- if (key.upArrow || input === "k") {
597
+ if (key.upArrow || letterAlias === "k") {
486
598
  const next = Math.max(tierCursorRef.current - 1, 0);
487
599
  tierCursorRef.current = next;
488
600
  setTierCursor(next);
489
601
  return;
490
602
  }
491
603
  if (key.return && pendingClassificationRef.current) {
492
- pendingClassificationRef.current.tier = TIERS[tierCursorRef.current];
493
- noteRef.current = "";
494
- setNote("");
604
+ const chosen = TIERS[tierCursorRef.current];
605
+ pendingClassificationRef.current.tier = chosen;
606
+ // OBS-508: keep the suggested band → the evidence note arrives pre-typed (Enter applies
607
+ // it as-is); override the band → the note starts empty, because the suggestion's text
608
+ // argues for a DIFFERENT tier than the one being recorded.
609
+ const suggestion = pendingSuggestionRef.current;
610
+ const prefill = suggestion && suggestion.tier === chosen ? suggestion.note : "";
611
+ noteRef.current = prefill;
612
+ setNote(prefill);
495
613
  setNotice("");
496
614
  showScreen("provenance");
497
615
  }
@@ -510,6 +628,7 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
510
628
  setClassificationRevision((revision) => revision + 1);
511
629
  }
512
630
  pendingClassificationRef.current = null;
631
+ pendingSuggestionRef.current = null;
513
632
  setNotice("");
514
633
  showScreen("models");
515
634
  return;
@@ -530,13 +649,13 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
530
649
  // preset entry adds one final `custom` row after the modes — the full-walk escape hatch
531
650
  const rowCount = modeOptions.length + (presetPickRef.current ? 1 : 0);
532
651
  const currentModeCursor = modeCursorRef.current ?? 0;
533
- if (key.downArrow || input === "j") {
652
+ if (key.downArrow || letterAlias === "j") {
534
653
  const next = Math.min(currentModeCursor + 1, Math.max(rowCount - 1, 0));
535
654
  modeCursorRef.current = next;
536
655
  setModeCursor(next);
537
656
  return;
538
657
  }
539
- if (key.upArrow || input === "k") {
658
+ if (key.upArrow || letterAlias === "k") {
540
659
  const next = Math.max(currentModeCursor - 1, 0);
541
660
  modeCursorRef.current = next;
542
661
  setModeCursor(next);
@@ -562,20 +681,20 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
562
681
  }
563
682
  if (screenRef.current === "shapes") {
564
683
  const rows = shapeRows(selectedModeRef.current, stagedMap());
565
- if (key.downArrow || input === "j") {
684
+ if (key.downArrow || letterAlias === "j") {
566
685
  const next = Math.min(shapeCursorRef.current + 1, Math.max(rows.length - 1, 0));
567
686
  shapeCursorRef.current = next;
568
687
  setShapeCursor(next);
569
688
  return;
570
689
  }
571
- if (key.upArrow || input === "k") {
690
+ if (key.upArrow || letterAlias === "k") {
572
691
  const next = Math.max(shapeCursorRef.current - 1, 0);
573
692
  shapeCursorRef.current = next;
574
693
  setShapeCursor(next);
575
694
  return;
576
695
  }
577
696
  const shape = rows[shapeCursorRef.current]?.id;
578
- if (input === "a" && shape) {
697
+ if (letterAlias === "a" && shape) {
579
698
  const nextEntry = { ...stagedMap()[shape] };
580
699
  delete nextEntry.pin;
581
700
  const nextMap = { ...stagedMap(), [shape]: nextEntry };
@@ -583,14 +702,14 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
583
702
  setMap(nextMap);
584
703
  return;
585
704
  }
586
- if (input === "p" && shape) {
705
+ if (letterAlias === "p" && shape) {
587
706
  candidatesRef.current = candidatesForShape(shape, selectedModeRef.current, stagedMap());
588
707
  candidateCursorRef.current = 0;
589
708
  setCandidateCursor(0);
590
709
  showScreen("candidates");
591
710
  return;
592
711
  }
593
- if (input === "f" && shape) {
712
+ if (letterAlias === "f" && shape) {
594
713
  const current = stagedMap()[shape]?.prefer ?? [];
595
714
  preferRowsRef.current = preferOptionsForShape(shape, current);
596
715
  preferChainRef.current = current.slice();
@@ -605,13 +724,13 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
605
724
  }
606
725
  if (screenRef.current === "candidates") {
607
726
  const rows = filteredCandidates();
608
- if (key.downArrow || input === "j") {
727
+ if (key.downArrow || letterAlias === "j") {
609
728
  const next = Math.min(candidateCursorRef.current + 1, Math.max(rows.length - 1, 0));
610
729
  candidateCursorRef.current = next;
611
730
  setCandidateCursor(next);
612
731
  return;
613
732
  }
614
- if (key.upArrow || input === "k") {
733
+ if (key.upArrow || letterAlias === "k") {
615
734
  const next = Math.max(candidateCursorRef.current - 1, 0);
616
735
  candidateCursorRef.current = next;
617
736
  setCandidateCursor(next);
@@ -632,13 +751,13 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
632
751
  return;
633
752
  }
634
753
  if (screenRef.current === "shape-prefer") {
635
- if (key.downArrow || input === "j") {
754
+ if (key.downArrow || letterAlias === "j") {
636
755
  const next = Math.min(preferCursorRef.current + 1, Math.max(preferRowsRef.current.length - 1, 0));
637
756
  preferCursorRef.current = next;
638
757
  setPreferCursor(next);
639
758
  return;
640
759
  }
641
- if (key.upArrow || input === "k") {
760
+ if (key.upArrow || letterAlias === "k") {
642
761
  const next = Math.max(preferCursorRef.current - 1, 0);
643
762
  preferCursorRef.current = next;
644
763
  setPreferCursor(next);
@@ -672,19 +791,19 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
672
791
  }
673
792
  if (screenRef.current === "steering") {
674
793
  // review.prefer + consult.prefer rows plus the single judge seat as the final row
675
- if (key.downArrow || input === "j") {
794
+ if (key.downArrow || letterAlias === "j") {
676
795
  const next = Math.min(steeringCursorRef.current + 1, STEERING_KEYS.length);
677
796
  steeringCursorRef.current = next;
678
797
  setSteeringCursor(next);
679
798
  return;
680
799
  }
681
- if (key.upArrow || input === "k") {
800
+ if (key.upArrow || letterAlias === "k") {
682
801
  const next = Math.max(steeringCursorRef.current - 1, 0);
683
802
  steeringCursorRef.current = next;
684
803
  setSteeringCursor(next);
685
804
  return;
686
805
  }
687
- if (input === "f") {
806
+ if (letterAlias === "f") {
688
807
  if (steeringCursorRef.current === STEERING_KEYS.length) {
689
808
  judgeCursorRef.current = 0;
690
809
  setJudgeCursor(0);
@@ -709,13 +828,13 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
709
828
  }
710
829
  if (screenRef.current === "judge-pick") {
711
830
  const rows = filteredJudgeRows();
712
- if (key.downArrow || input === "j") {
831
+ if (key.downArrow || letterAlias === "j") {
713
832
  const next = Math.min(judgeCursorRef.current + 1, Math.max(rows.length - 1, 0));
714
833
  judgeCursorRef.current = next;
715
834
  setJudgeCursor(next);
716
835
  return;
717
836
  }
718
- if (key.upArrow || input === "k") {
837
+ if (key.upArrow || letterAlias === "k") {
719
838
  const next = Math.max(judgeCursorRef.current - 1, 0);
720
839
  judgeCursorRef.current = next;
721
840
  setJudgeCursor(next);
@@ -735,13 +854,13 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
735
854
  }
736
855
  if (screenRef.current === "steering-prefer") {
737
856
  const rows = filteredSteeringRows();
738
- if (key.downArrow || input === "j") {
857
+ if (key.downArrow || letterAlias === "j") {
739
858
  const next = Math.min(steeringPickerCursorRef.current + 1, Math.max(rows.length - 1, 0));
740
859
  steeringPickerCursorRef.current = next;
741
860
  setSteeringPickerCursor(next);
742
861
  return;
743
862
  }
744
- if (key.upArrow || input === "k") {
863
+ if (key.upArrow || letterAlias === "k") {
745
864
  const next = Math.max(steeringPickerCursorRef.current - 1, 0);
746
865
  steeringPickerCursorRef.current = next;
747
866
  setSteeringPickerCursor(next);
@@ -804,25 +923,42 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
804
923
  content: (_jsxs(_Fragment, { children: [_jsx(ToggleMark, { active: active }), _jsx(Text, { children: ` ${agent.id} ${agent.version} ${agent.authed ? "authed" : "unauthed"}` })] })),
805
924
  };
806
925
  });
807
- return (_jsx(FleetListScreen, { title: "step 2/6 \u00B7 agent CLIs", legend: "\u2191\u2193/jk move \u00B7 space toggle \u00B7 enter next \u00B7 esc/q quit", rows: rows, cursor: cursor }));
926
+ return (_jsx(FleetListScreen, { title: "step 2/6 \u00B7 agent CLIs", viewRows: viewRows, legend: "\u2191\u2193/jk move \u00B7 space toggle \u00B7 enter next \u00B7 esc/q quit", rows: rows, cursor: cursor }));
808
927
  }
809
928
  const group = enabledModelGroups()[modelGroup];
810
929
  if (screen === "models") {
811
- const rows = filteredModelRows().map((row) => {
930
+ const filtered = filteredModelRows();
931
+ const rows = filtered.map((row) => {
812
932
  const denied = group ? denyModels.has(`${group.adapter}:${row.model}`) : false;
813
933
  return {
814
934
  id: row.model,
815
- content: row.tier ? (_jsxs(_Fragment, { children: [_jsx(ToggleMark, { active: !denied }), _jsx(Text, { children: ` ${row.model} ${row.tier} ${denied ? "denied" : "allowed"}` })] })) : _jsx(Text, { children: `( ) ${row.model} ??? unclassified${row.detectedAt ? ` (${row.detectedAt})` : ""}` }),
935
+ content: row.tier ? (_jsxs(_Fragment, { children: [_jsx(ToggleMark, { active: !denied }), _jsx(Text, { children: ` ${row.model} ${row.tier} ${denied ? "denied" : "allowed"}` })] })) : _jsx(Text, { children: `? ${row.model}${row.suggestion ? ` → ${row.suggestion.tier} suggested` : ""}` }),
816
936
  };
817
937
  });
818
- return (_jsx(FleetListScreen, { title: `step 3/6 · models · ${group?.adapter ?? ""}`, legend: "\u2191\u2193 to move \u00B7 Space to toggle \u00B7 t to classify tier \u00B7 n to add model \u00B7 Enter for next \u00B7 Type to search \u00B7 Esc to quit", rows: rows, cursor: modelCursor, details: notice ? [notice] : [], filter: filter }));
938
+ // The remedy renders ONCE, for the row under the cursor — 218 identical "Space or t to
939
+ // classify" suffixes drowned the model names and wrapped long ids (operator screenshot,
940
+ // 2026-08-13). A notice always wins the line.
941
+ const focused = filtered[modelCursor];
942
+ const hidden = hiddenModelCount();
943
+ const detail = [
944
+ ...(notice
945
+ ? [notice]
946
+ : focused && !focused.tier
947
+ ? [focused.suggestion
948
+ ? `? ${focused.suggestion.tier} suggested from catalog evidence — Space accepts prefilled, s stages every visible suggestion${focused.detectedAt ? ` (detected ${focused.detectedAt})` : ""}`
949
+ : `? unclassified — Space or t to classify${focused.detectedAt ? ` (detected ${focused.detectedAt})` : ""}; unclassified models are never routed`]
950
+ : []),
951
+ ...(hidden > 0 ? [`… ${hidden} retired/preview/non-worker hidden — a shows all`] : []),
952
+ ...(showAllModelsRef.current ? ["showing retired models — a hides them again"] : []),
953
+ ];
954
+ return (_jsx(FleetListScreen, { title: `step 3/6 · models · ${group?.adapter ?? ""}`, legend: "\u2191\u2193 move \u00B7 Space toggle/classify \u00B7 s stage suggested \u00B7 t tier \u00B7 n add \u00B7 a all \u00B7 Enter next \u00B7 Type to search \u00B7 Esc quit", viewRows: viewRows, rows: rows, cursor: modelCursor, details: detail, filter: filter }));
819
955
  }
820
956
  if (screen === "add-model")
821
957
  return (_jsx(FleetListScreen, { title: `add model · ${group?.adapter ?? ""}`, legend: "type model id \u00B7 enter classify \u00B7 esc cancel", rows: [{ id: "model", content: _jsx(Text, { children: modelId || "model id (required):" }) }], cursor: 0, details: notice ? [notice] : [] }));
822
958
  if (screen === "channel")
823
- return (_jsx(FleetListScreen, { title: `channel · ${pendingClassificationRef.current?.adapter ?? ""}`, legend: "\u2191\u2193/jk move \u00B7 enter select \u00B7 esc/q quit", rows: CHANNELS.map((channel) => ({ id: channel, content: _jsx(Text, { children: channel }) })), cursor: channelCursor }));
959
+ return (_jsx(FleetListScreen, { title: `channel · ${pendingClassificationRef.current?.adapter ?? ""}`, legend: "\u2191\u2193/jk move \u00B7 enter select \u00B7 esc cancel \u00B7 q quit", rows: CHANNELS.map((channel) => ({ id: channel, content: _jsx(Text, { children: channel }) })), cursor: channelCursor }));
824
960
  if (screen === "tiers") {
825
- return (_jsx(FleetListScreen, { title: `pick · tier · ${pendingClassificationRef.current?.adapter}:${pendingClassificationRef.current?.model}`, legend: "\u2191\u2193/jk move \u00B7 enter select \u00B7 esc/q quit", rows: TIERS.map((tier) => ({ id: tier, content: _jsx(Text, { children: tier }) })), cursor: tierCursor }));
961
+ return (_jsx(FleetListScreen, { title: `pick · tier · ${pendingClassificationRef.current?.adapter}:${pendingClassificationRef.current?.model}`, legend: "\u2191\u2193/jk move \u00B7 enter select \u00B7 esc cancel \u00B7 q quit", rows: TIERS.map((tier) => ({ id: tier, content: _jsx(Text, { children: tier }) })), cursor: tierCursor }));
826
962
  }
827
963
  if (screen === "provenance")
828
964
  return (_jsx(FleetListScreen, { title: `benchmark provenance · ${pendingClassificationRef.current?.adapter}:${pendingClassificationRef.current?.model}`, legend: "type note \u00B7 enter apply \u00B7 esc cancel", rows: [{ id: "note", content: _jsx(Text, { children: note || "benchmark provenance note (required):" }) }], cursor: 0, details: notice ? [notice] : [] }));
@@ -838,7 +974,7 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
838
974
  content: (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { children: ` ${"custom".padEnd(11)} walk the full editor (agents → models → shapes → seats)` })] })),
839
975
  });
840
976
  }
841
- return (_jsx(FleetListScreen, { title: presetPick ? "fleet · routing presets" : "step 4/6 · routing mode", legend: presetPick ? "↑↓ to move · Enter to apply preset · Esc to close" : "↑↓/jk move · enter select · esc/q quit", rows: rows, cursor: modeCursor, details: modeOptions[modeCursor] ? modePreview(modeOptions[modeCursor].id, map) : [] }));
977
+ return (_jsx(FleetListScreen, { title: presetPick ? "tickmarkr init · act 3 of 3 — fleet · routing presets" : "step 4/6 · routing mode", legend: presetPick ? "↑↓ to move · Enter to apply preset · Enter on custom to walk the editor · Esc inside the walk returns here · Esc here closes" : "↑↓/jk move · enter select · esc/q quit", rows: rows, cursor: modeCursor, details: modeOptions[modeCursor] ? modePreview(modeOptions[modeCursor].id, map) : [] }));
842
978
  }
843
979
  const renderedShapes = shapeRows(selectedModeRef.current, map);
844
980
  if (screen === "shapes") {
@@ -846,13 +982,13 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
846
982
  }
847
983
  const shape = renderedShapes[shapeCursor]?.id;
848
984
  if (screen === "candidates") {
849
- return (_jsx(FleetListScreen, { title: `pick · ${shape}`, legend: "\u2191\u2193 to move \u00B7 Enter to pin \u00B7 Type to search \u00B7 Esc to cancel \u00B7 q to quit", rows: filteredCandidates().map((candidate) => ({
985
+ return (_jsx(FleetListScreen, { title: `pick · ${shape}`, viewRows: viewRows, legend: "\u2191\u2193 to move \u00B7 Enter to pin \u00B7 Type to search \u00B7 Esc to cancel \u00B7 q to quit", rows: filteredCandidates().map((candidate) => ({
850
986
  id: candidate.id,
851
987
  content: _jsx(Text, { children: candidate.label }),
852
988
  })), cursor: candidateCursor, filter: filter }));
853
989
  }
854
990
  if (screen === "shape-prefer")
855
- return (_jsx(FleetListScreen, { title: `pick · ${shape}.prefer`, legend: "\u2191\u2193/jk move \u00B7 space add/drop \u00B7 enter apply (empty clears) \u00B7 esc cancel \u00B7 q quit", rows: preferRowsRef.current.map((option) => {
991
+ return (_jsx(FleetListScreen, { title: `pick · ${shape}.prefer`, viewRows: viewRows, legend: "\u2191\u2193/jk move \u00B7 space add/drop \u00B7 enter apply (empty clears) \u00B7 esc cancel \u00B7 q quit", rows: preferRowsRef.current.map((option) => {
856
992
  const at = preferChainRef.current.indexOf(option);
857
993
  return { id: option, content: _jsx(Text, { children: `${at === -1 ? "·" : String(at + 1)} ${option}` }) };
858
994
  }), cursor: preferCursor }));
@@ -870,14 +1006,14 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
870
1006
  }
871
1007
  if (screen === "judge-pick") {
872
1008
  const selected = judgeSeat ?? judgeKeepRow;
873
- return (_jsx(FleetListScreen, { title: "pick \u00B7 judge", legend: "\u2191\u2193 to move \u00B7 Enter to select \u00B7 Type to search \u00B7 Esc to cancel \u00B7 q to quit", rows: filteredJudgeRows().map((label) => ({
1009
+ return (_jsx(FleetListScreen, { title: "pick \u00B7 judge", viewRows: viewRows, legend: "\u2191\u2193 to move \u00B7 Enter to select \u00B7 Type to search \u00B7 Esc to cancel \u00B7 q to quit", rows: filteredJudgeRows().map((label) => ({
874
1010
  id: label,
875
1011
  content: (_jsxs(_Fragment, { children: [label === selected ? _jsx(ToggleMark, { active: true }) : _jsx(Text, { children: " " }), _jsx(Text, { children: ` ${label}` })] })),
876
1012
  })), cursor: judgeCursor, filter: filter }));
877
1013
  }
878
1014
  if (screen === "steering-prefer") {
879
1015
  const which = steeringPickerRef.current;
880
- return (_jsx(FleetListScreen, { title: `pick · ${which}.prefer`, legend: "\u2191\u2193 to move \u00B7 Space to add/drop \u00B7 Enter to apply (empty clears) \u00B7 Type to search \u00B7 Esc to cancel \u00B7 q to quit", rows: filteredSteeringRows().map((option) => {
1016
+ return (_jsx(FleetListScreen, { title: `pick · ${which}.prefer`, viewRows: viewRows, legend: "\u2191\u2193 to move \u00B7 Space to add/drop \u00B7 Enter to apply (empty clears) \u00B7 Type to search \u00B7 Esc to cancel \u00B7 q to quit", rows: filteredSteeringRows().map((option) => {
881
1017
  const at = steeringChainRef.current.indexOf(option);
882
1018
  return { id: option, content: _jsx(Text, { children: `${at === -1 ? "·" : String(at + 1)} ${option}` }) };
883
1019
  }), cursor: steeringPickerCursor, filter: filter }));
@@ -902,7 +1038,10 @@ export async function runFleetInkEditor({ ageMs, adapters, health, initialDenyAd
902
1038
  }));
903
1039
  const bridgedInput = inkInput(input, initialInput);
904
1040
  const legacyOutput = typeof output.on !== "function" || typeof output.off !== "function";
905
- const app = render(_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 }), {
1041
+ const app = render(_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,
1042
+ // v1.90.9: omp's 218-model list rendered taller than the terminal and scrolled the cursor
1043
+ // and chrome off-screen — cap every list to the terminal, minus title/legend/markers/details.
1044
+ viewRows: Math.max(8, (output.rows ?? 40) - 10) }), {
906
1045
  // FleetIO's injected stream predates Ink and did not require ref/unref.
907
1046
  // Real terminal streams pass through unchanged; the compatibility facade
908
1047
  // adds only those lifecycle methods and leaves input decoding to Ink.
@@ -101,8 +101,8 @@ function inkInput(input, initialInput) {
101
101
  // Three fields, one toggle, one action — a descriptor array, deliberately not a forms framework.
102
102
  function buildRows(offerSkills) {
103
103
  const rows = [
104
- { id: "driver", section: "Run", label: "Driver", desc: "auto: pick herdr when HERDR_ENV=1, else subprocess" },
105
- { id: "concurrency", section: "Run", label: "Concurrency", desc: "parallel task batteries per run" },
104
+ { id: "driver", section: "Run", label: "Driver", desc: "auto: herdr when HERDR_ENV=1, else subprocess · herdr: visible panes · subprocess: headless child processes" },
105
+ { id: "concurrency", section: "Run", label: "Concurrency", desc: "parallel task batteries per run — min 1; an empty or zero entry reverts on leave" },
106
106
  {
107
107
  id: "visibility",
108
108
  section: "Run",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "1.90.7",
3
+ "version": "1.91.0",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",