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.
@@ -15,6 +15,29 @@ const DAY_MS = 86400000;
15
15
  // 2026-08-12 (D-OBS-11: cursor's residual lint list was still mostly effort variants of configured bases).
16
16
  const LINT_VARIANT_RE = /^auto$|-(fast|minimal|none|low|medium|high|xhigh|max|thinking)$/;
17
17
  const LINT_CAP = 5;
18
+ /**
19
+ * Operator directive 2026-08-13 ("we should exclude all retired models"): the fleet models
20
+ * screen hides these classes BY DEFAULT — omp alone reports 218 ids, most of them dated
21
+ * snapshots, previews, and SKUs that can never carry a worker. Shape-based on purpose: a
22
+ * knowledge list of retired families rots, a suffix grammar does not. Hidden is never gone —
23
+ * the screen counts what it hid and one key reveals it, and CLASSIFIED models are never hidden
24
+ * regardless of shape (an operator who tiered a dated snapshot meant it).
25
+ */
26
+ export function retiredModelReason(model) {
27
+ const bare = model.includes("/") ? model.slice(model.lastIndexOf("/") + 1) : model;
28
+ // vendor-dated pins: claude-3-5-sonnet-20241022, claude-opus-4-1-20250805, *-preview-10-2025
29
+ if (/-20\d{6}$/.test(bare))
30
+ return "dated snapshot";
31
+ if (/-preview(?:-\d{2}-20\d{2})?$/.test(bare))
32
+ return "preview";
33
+ // SKUs that cannot carry a worker seat: media, embedding, retrieval, and browsing surfaces
34
+ if (/(?:^|-)(?:embed(?:ding)?s?|tts|whisper|audio|image|vision|ocr|rerank|moderation|computer-use|deep-research)(?:-|$)/.test(bare))
35
+ return "non-worker";
36
+ // superseded generations still reported by CLIs long after retirement
37
+ if (/^(?:gemini-1\.|claude-3-|gpt-3\.|gpt-4-)/.test(bare))
38
+ return "legacy family";
39
+ return null;
40
+ }
18
41
  const TTY_LINT_CAP = 3;
19
42
  const DEFAULT_STATE_DIR = ".tickmarkr";
20
43
  const doctorJsonRef = (stateDir) => ` — see ${stateDir}/doctor.json`;
@@ -23,33 +46,100 @@ const LEGACY_DOCTOR_SEED_PREFER_DISPOSITION = "auto-prefer is routing around it"
23
46
  export const ttyVisual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
24
47
  // ponytail: chars/4 token heuristic — good enough for advisory plan lint; no tokenizer dep.
25
48
  const CHARS_PER_TOKEN = 4;
26
- const intelligenceTier = (index) => index >= 65 ? "frontier" : index >= 40 ? "mid" : "cheap";
49
+ /** Bases that band FLEET-RELATIVELY, most-trusted first (2026-08-13 ranking-sites assessment §4.3). */
50
+ const RANKED_BASES = ["agentic-coding", "intelligence"];
51
+ /**
52
+ * D-2 (2026-08-13 ranking-sites assessment): the retired `index >= 65 → frontier` cut was calibrated
53
+ * against an older, higher-ceilinged AA index. After the v4.x rescale the best model in existence
54
+ * scored 63.05, so NOTHING could ever be frontier. An absolute cut against a vendor index that gets
55
+ * rescaled is the defect CLASS — the fix is a rank among the models we actually hold same-basis
56
+ * evidence for, not a new magic number. Three is the floor: thirds over two models is a coin toss.
57
+ */
58
+ const MIN_RANKED_MODELS = 3;
59
+ /** Rank r of n, 0-based: top third → frontier, middle third → mid, bottom third → cheap. */
60
+ const bandOfRank = (rank, total) => rank * 3 < total ? "frontier" : rank * 3 < total * 2 ? "mid" : "cheap";
61
+ const basisScore = (evidence, basis) => basis === "agentic-coding" ? evidence.agenticCodingScore : evidence.intelligenceIndex;
27
62
  const priceTier = (outputPerMtok) => outputPerMtok >= 12 ? "frontier" : outputPerMtok >= 2 ? "mid" : "cheap";
63
+ const round1 = (n) => Math.round(n * 10) / 10;
64
+ function catalogEvidenceFor(cfg, catalog, row) {
65
+ return resolveCatalogModel(catalog.catalog, {
66
+ // A tier vendor is NULLABLE (config.ts:64) — null means "declared as having none at tier level, so
67
+ // each model must declare its own". `resolveCatalogModel` takes an optional provider HINT
68
+ // (catalog-remote.ts:218), and "no hint" is `undefined` there, so null normalizes to undefined rather
69
+ // than travelling as a third state. Neither T17 nor T19 could see this seam: their files[] are
70
+ // disjoint, so no ordering edge was derived, they dispatched 7ms apart, and each worktree was cut
71
+ // before the other's change existed. Both gates passed truthfully; only the merged pair fails to
72
+ // compile (OBS-371).
73
+ provider: cfg.tiers[row.adapter]?.vendor ?? undefined,
74
+ model: row.model,
75
+ ...(row.resolvedModel ? { resolvedModel: row.resolvedModel } : {}),
76
+ });
77
+ }
78
+ /**
79
+ * The ranking universe: configured tier models ∪ the rows under advisory at THIS call site,
80
+ * restricted per basis to those that resolve it. Build it once per call — a band that depended on
81
+ * which adapter happened to be iterated first would not be a band. `resolvedModel` is the SAME
82
+ * resolver the call site hands its advisory rows: a configured claude alias (`opus`) is absent from
83
+ * models.dev under that spelling, so without it the fleet's own frontier models silently drop out
84
+ * of the universe they are supposed to anchor.
85
+ */
86
+ export function catalogTierRanking(cfg, catalog, rows = [], resolvedModel) {
87
+ // Orca import #1: the vendored catalog is a shipped default (`catalogOrigin: spec` in orca's terms),
88
+ // never a fetched observation — it may not seed a ranking any more than it may seed a suggestion.
89
+ if (catalog.source === "vendored")
90
+ return [];
91
+ const configured = Object.entries(cfg.tiers).flatMap(([adapter, entry]) => Object.keys(entry.models).map((model) => ({ adapter, model, resolvedModel: resolvedModel?.(adapter, model) })));
92
+ // The resolver supplies the matched `<providerKey>/<recordId>`: caller spellings never define
93
+ // identity, aliases of one record collapse, and equal bare ids under distinct providers do not.
94
+ const universe = new Map();
95
+ for (const row of [...configured, ...rows]) {
96
+ const evidence = catalogEvidenceFor(cfg, catalog, row);
97
+ if (evidence)
98
+ universe.set(evidence.catalogId, evidence);
99
+ }
100
+ const members = [...universe.values()];
101
+ return RANKED_BASES.flatMap((basis) => {
102
+ const scores = members
103
+ .map((member) => basisScore(member, basis))
104
+ .filter((score) => score !== undefined)
105
+ .sort((a, b) => b - a);
106
+ return scores.length >= MIN_RANKED_MODELS ? [{ basis, scores }] : [];
107
+ });
108
+ }
109
+ /** First basis that both clears the floor AND can place this model; ties share the better band. */
110
+ function rankedSuggestion(ranking, evidence) {
111
+ for (const { basis, scores } of ranking) {
112
+ const score = basisScore(evidence, basis);
113
+ if (score === undefined)
114
+ continue;
115
+ const rank = scores.filter((other) => other > score).length;
116
+ // Every suggestion names its evidence source AND its date: an AA band that omits the index
117
+ // version is exactly the silent-rescale defect D-2 shipped to everyone.
118
+ const source = basis === "agentic-coding"
119
+ ? `LiveBench Agentic Coding ${round1(score)} (LiveBench table ${evidence.evidenceDate ?? "date not reported"})`
120
+ : `Artificial Analysis Intelligence Index ${round1(score)} (intelligence index version ${evidence.intelligenceIndexVersion ?? "not reported"})`;
121
+ return { basis, tier: bandOfRank(rank, scores.length), basisNote: `fleet-relative rank ${rank + 1}/${scores.length} by ${source}` };
122
+ }
123
+ return undefined;
124
+ }
28
125
  function catalogEvidenceNote(evidence, catalog) {
29
126
  const cost = evidence.inputCostPerMtok !== undefined && evidence.outputCostPerMtok !== undefined
30
127
  ? `$${evidence.inputCostPerMtok}/$${evidence.outputCostPerMtok} per Mtok`
31
128
  : "not reported";
32
129
  const features = evidence.features.length ? evidence.features.join(",") : "none reported";
130
+ const agentic = evidence.agenticCodingScore !== undefined
131
+ ? `; LiveBench Agentic Coding=${round1(evidence.agenticCodingScore)}${evidence.evidenceDate ? ` (table ${evidence.evidenceDate})` : ""}`
132
+ : "";
33
133
  const intelligence = evidence.intelligenceIndex !== undefined
34
134
  ? `; Artificial Analysis Intelligence Index=${evidence.intelligenceIndex}${evidence.intelligenceIndexVersion ? ` (version ${evidence.intelligenceIndexVersion})` : ""}`
35
135
  : "";
36
136
  const freshness = catalog.stale ? `${catalog.source} stale cache` : catalog.source;
37
- return `models.dev id=${evidence.modelId}; cost=${cost}; context=${evidence.contextWindow ?? "not reported"}; features=${features}${intelligence}; catalog=${freshness}; fetchedAt=${catalog.catalog.fetchedAt}`;
137
+ return `models.dev id=${evidence.modelId}; cost=${cost}; context=${evidence.contextWindow ?? "not reported"}; features=${features}${agentic}${intelligence}; catalog=${freshness}; fetchedAt=${catalog.catalog.fetchedAt}`;
38
138
  }
39
- export function catalogModelAdvisory(cfg, catalog, adapter, model, resolvedModel) {
139
+ export function catalogModelAdvisory(cfg, catalog, adapter, model, resolvedModel, ranking) {
40
140
  const entry = cfg.tiers[adapter];
41
- const evidence = resolveCatalogModel(catalog.catalog, {
42
- // A tier vendor is NULLABLE (config.ts:64) — null means "declared as having none at tier level, so
43
- // each model must declare its own". `resolveCatalogModel` takes an optional provider HINT
44
- // (catalog-remote.ts:218), and "no hint" is `undefined` there, so null normalizes to undefined rather
45
- // than travelling as a third state. Neither T17 nor T19 could see this seam: their files[] are
46
- // disjoint, so no ordering edge was derived, they dispatched 7ms apart, and each worktree was cut
47
- // before the other's change existed. Both gates passed truthfully; only the merged pair fails to
48
- // compile (OBS-371).
49
- provider: entry?.vendor ?? undefined,
50
- model,
51
- ...(resolvedModel ? { resolvedModel } : {}),
52
- });
141
+ const row = { adapter, model, resolvedModel };
142
+ const evidence = catalogEvidenceFor(cfg, catalog, row);
53
143
  if (!evidence) {
54
144
  return {
55
145
  coverage: "uncovered",
@@ -57,18 +147,28 @@ export function catalogModelAdvisory(cfg, catalog, adapter, model, resolvedModel
57
147
  };
58
148
  }
59
149
  const note = catalogEvidenceNote(evidence, catalog);
60
- const basis = evidence.intelligenceIndex !== undefined
61
- ? { basis: "intelligence", tier: intelligenceTier(evidence.intelligenceIndex) }
62
- : entry?.channel === "api" && evidence.outputCostPerMtok !== undefined
63
- ? { basis: "price", tier: priceTier(evidence.outputCostPerMtok) }
64
- : undefined;
150
+ // Orca import #1, read site: a shipped default must never launder into a fleet-reported suggestion.
151
+ if (catalog.source === "vendored") {
152
+ return {
153
+ coverage: "covered",
154
+ evidence,
155
+ display: `${model} → ${note}; the vendored catalog is a shipped default, not fetched evidence — no tier suggestion (run tickmarkr doctor --refresh-catalog)`,
156
+ };
157
+ }
158
+ const ranked = rankedSuggestion(ranking ?? catalogTierRanking(cfg, catalog, [row]), evidence);
159
+ const basis = ranked
160
+ ?? (entry?.channel === "api" && evidence.outputCostPerMtok !== undefined
161
+ // composer-2.5's permanent home: it has zero public benchmark evidence anywhere, so price is
162
+ // the only basis it will ever have. Unchanged absolute thresholds — price is not rescaled.
163
+ ? { basis: "price", tier: priceTier(evidence.outputCostPerMtok), basisNote: "price-derived fallback — no fleet-relative ranking basis covers this model" }
164
+ : undefined);
65
165
  if (!basis) {
66
166
  const reason = entry?.channel === "sub" && evidence.outputCostPerMtok !== undefined
67
167
  ? "subscription billing; no price-derived suggestion"
68
168
  : "no tier suggestion from available evidence";
69
169
  return { coverage: "covered", evidence, display: `${model} → ${note}; ${reason}` };
70
170
  }
71
- const provenanceNote = `SUGGESTED ${basis.tier} (${basis.basis} inference, not a measurement) — ${note}; operator confirmation required`;
171
+ const provenanceNote = `SUGGESTED ${basis.tier} (${basis.basis} inference, not a measurement) — ${basis.basisNote}; ${note}; operator confirmation required`;
72
172
  return {
73
173
  coverage: "covered",
74
174
  evidence,
@@ -256,7 +356,12 @@ export function modelLints(cfg, health, adapters, opts) {
256
356
  for (const adapter of adapters) {
257
357
  const id = adapter.id;
258
358
  if (!adapter.listModels) {
259
- lints.push(`${id}: no model-list surface — seeds stamped ${SEED_STAMPED}; verify manually`);
359
+ // v1.90 / OBS-504: the seeds-stamped wording presumes a seeded tier table. agy ships routable
360
+ // but UNCLASSIFIED (no listModels, no seed models) — for that shape the honest sentence names
361
+ // the classification debt, not a seed stamp with nothing behind it.
362
+ lints.push(Object.keys(cfg.tiers[id]?.models ?? {}).length > 0
363
+ ? `${id}: no model-list surface — seeds stamped ${SEED_STAMPED}; verify manually`
364
+ : `${id}: no model-list surface and no configured models — classify per benchmark policy to open channels`);
260
365
  continue;
261
366
  }
262
367
  const h = health[id];
@@ -312,25 +417,20 @@ export function formatModelAuthLine(excluded, tty, stateDir = DEFAULT_STATE_DIR)
312
417
  // `<id>: null` tombstones (deepMerge deletes the key). Pure function: no fs, no routing contact.
313
418
  // Returns "" when no adapter has a delta. Mirrors modelLints' per-adapter guards exactly.
314
419
  export function suggestOverlay(cfg, health, adapters, stateDir = DEFAULT_STATE_DIR, opts = {}) {
315
- const blocks = [];
316
- for (const adapter of adapters) {
420
+ // Pass 1 — decide WHAT each adapter drifts by. Pass 2 renders, after every addition this call
421
+ // will print has joined one ranking universe: a fleet-relative band must not depend on which
422
+ // adapter happened to be iterated first.
423
+ const drifts = adapters.flatMap((adapter) => {
317
424
  const id = adapter.id;
318
425
  if (!adapter.listModels)
319
- continue; // no list surface → nothing to diff (mirror modelLints)
426
+ return []; // no list surface → nothing to diff (mirror modelLints)
320
427
  const h = health[id];
321
428
  const detected = h?.models ?? [];
322
429
  if (detected.length === 0)
323
- continue; // no detection data → don't guess a delta
430
+ return []; // no detection data → don't guess a delta
324
431
  const configured = Object.keys(cfg.tiers[id]?.models ?? {});
325
- const date = h?.modelsDetectedAt?.split("T")[0]; // best-effort day stamp
326
- const detNote = date ? ` (detected ${date})` : "";
327
- const lines = [];
328
432
  // Tombstones: configured ids the CLI no longer reports. Ids are operator-authored (from cfg) → MODEL_ID_RE only.
329
- for (const model of configured) {
330
- if (detected.includes(model) || !MODEL_ID_RE.test(model))
331
- continue;
332
- lines.push(` ${model}: null # tombstone: ${id} no longer reports this id${detNote}${referenceWarning(cfg, id, model)}`);
333
- }
433
+ const tombstones = configured.filter((model) => !detected.includes(model) && MODEL_ID_RE.test(model));
334
434
  // Additions: detected ids not in cfg. WHOLE line commented, no tier (MODEL-06). Ids come from an external
335
435
  // CLI → MODEL_ID_RE (defense-in-depth, T-21-01) + the variant filter (cursor's ~193 parameterized ids).
336
436
  // RELATIONAL gate (no capability judgment — "looks like an embedding model" is auto-tiering's cousin, the
@@ -344,6 +444,7 @@ export function suggestOverlay(cfg, health, adapters, stateDir = DEFAULT_STATE_D
344
444
  // not silently. ponytail: not worth a per-adapter heuristic to quiet cursor at the price of codex signal.
345
445
  const cfgPrefixes = new Set(configured.map(providerPrefix));
346
446
  const cfgCanon = new Set(configured.map(canonical));
447
+ const additions = [];
347
448
  let omitted = 0;
348
449
  for (const model of detected) {
349
450
  if (configured.includes(model) || !MODEL_ID_RE.test(model) || LINT_VARIANT_RE.test(model))
@@ -352,8 +453,23 @@ export function suggestOverlay(cfg, health, adapters, stateDir = DEFAULT_STATE_D
352
453
  omitted++;
353
454
  continue;
354
455
  }
456
+ additions.push(model);
457
+ }
458
+ return [{ id, date: h?.modelsDetectedAt?.split("T")[0], tombstones, additions, omitted }]; // date: best-effort day stamp
459
+ });
460
+ const ranking = opts.catalog
461
+ ? catalogTierRanking(cfg, opts.catalog, drifts.flatMap(({ id, additions }) => additions.map((model) => ({ adapter: id, model, resolvedModel: opts.resolvedModel?.(id, model) }))), opts.resolvedModel)
462
+ : [];
463
+ const blocks = [];
464
+ for (const { id, date, tombstones, additions, omitted } of drifts) {
465
+ const detNote = date ? ` (detected ${date})` : "";
466
+ const lines = [];
467
+ for (const model of tombstones) {
468
+ lines.push(` ${model}: null # tombstone: ${id} no longer reports this id${detNote}${referenceWarning(cfg, id, model)}`);
469
+ }
470
+ for (const model of additions) {
355
471
  const advisory = opts.catalog
356
- ? catalogModelAdvisory(cfg, opts.catalog, id, model, opts.resolvedModel?.(id, model))
472
+ ? catalogModelAdvisory(cfg, opts.catalog, id, model, opts.resolvedModel?.(id, model), ranking)
357
473
  : undefined;
358
474
  const guidance = advisory?.suggestion
359
475
  ? `provenance note (operator confirmation required): ${advisory.suggestion.provenanceNote}; choose a tier, then uncomment`
@@ -65,8 +65,21 @@ export function probeVersionShell(bin, cwd = process.cwd()) {
65
65
  if (!resolved)
66
66
  return { installed: false, authed: false, models: [] };
67
67
  const r = shellSpawnSync(`${shq(resolved)} --version`, cwd);
68
- if (r.code !== 0)
69
- return { installed: false, authed: false, models: [], binaryPaths: all };
68
+ if (r.code !== 0) {
69
+ // OBS-503: a resolved binary whose --version CRASHES under the worker shell is not "not
70
+ // installed" — a stale node on the login-shell PATH (bash -lc re-derives PATH) crashes every
71
+ // `#!/usr/bin/env node` CLI at dispatch while the operator's interactive shell runs it fine.
72
+ // installed stays false (dispatch runs through this same shell and would crash identically —
73
+ // fail closed), but the note names the real failure so doctor never prints a lie.
74
+ const reason = `${r.stderr}\n${r.stdout}`.replace(/\s+/g, " ").trim();
75
+ return {
76
+ installed: false,
77
+ authed: false,
78
+ models: [],
79
+ binaryPaths: all,
80
+ note: `found at ${resolved} but \`--version\` exited ${r.code} under the worker shell (bash -lc)${reason ? ` — ${reasonTail(reason)}` : ""}`,
81
+ };
82
+ }
70
83
  return {
71
84
  installed: true,
72
85
  authed: true,
@@ -241,7 +254,13 @@ export function parseDeclaredModels(raw, parser, path, field) {
241
254
  else if (parser === "pi-table") {
242
255
  const lines = raw.trim().split("\n");
243
256
  const header = lines.findIndex((line) => /^provider\s+model\b/i.test(line.trim()));
244
- values = header === -1 ? [] : lines.slice(header + 1).map((line) => {
257
+ // OBS-506: a missing header previously fell through to a REASONLESS empty result, so the
258
+ // "never silently" warning below the parser could not fire — prime-agent's stderr-routed
259
+ // table produced a silent zero for a whole doctor run. An absent table names itself.
260
+ if (header === -1) {
261
+ return { models: [], reason: `no \`provider model …\` table header in ${raw.trim() ? `${lines.length} line(s) of output` : "empty output"}` };
262
+ }
263
+ values = lines.slice(header + 1).map((line) => {
245
264
  const [provider, model] = line.trim().split(/\s+/);
246
265
  return provider && model ? `${provider}/${model}` : "";
247
266
  });
@@ -301,7 +320,11 @@ function declarativeAdapter(entry) {
301
320
  const result = shellSpawnSync(cmd, process.cwd(), 15000);
302
321
  if (result.code !== 0)
303
322
  return [];
304
- const { models, reason } = parseDeclaredModels(result.stdout, drive.listModels.parser, drive.listModels.path, drive.listModels.field);
323
+ const { models, reason } = parseDeclaredModels(
324
+ // OBS-506: prime-agent prints its model table to STDERR (stdout 0 bytes, live-verified
325
+ // 2026-08-13) — the same stream convention probeVersionShell already tolerates for
326
+ // version banners. Fall back only when stdout is EMPTY, never merge the streams.
327
+ result.stdout.trim() ? result.stdout : result.stderr, drive.listModels.parser, drive.listModels.path, drive.listModels.field);
305
328
  // Listing stays advisory (fail-open to []), but never silently: an empty projection names why.
306
329
  if (models.length === 0 && reason)
307
330
  console.warn(`tickmarkr: ${entry.id} listed no models — ${reason}`);
@@ -344,8 +367,10 @@ export async function probeAll(adapters, opts = {}) {
344
367
  if (bin) {
345
368
  const shell = probeVersionShell(bin, cwd);
346
369
  h = { ...h, installed: shell.installed, version: shell.version ?? h.version };
370
+ // OBS-503: the shell probe's note replaces the adapter probe's own — keeping a bare-spawn
371
+ // success note ("auth verified via …") next to installed:false stores a self-contradiction.
347
372
  if (!shell.installed)
348
- h = { ...h, installed: false, authed: false };
373
+ h = { ...h, installed: false, authed: false, note: shell.note };
349
374
  }
350
375
  out[a.id] = h;
351
376
  }));
@@ -618,7 +643,8 @@ export function initDoctorReuse(repoRoot, fresh) {
618
643
  export function formatDoctorReport(cwd, cfg, health, adapters, opts = {}) {
619
644
  const rows = adapters.map((a) => {
620
645
  const h = health[a.id];
621
- const state = !h?.installed ? "not installed" : `${h.version ?? "installed"}${h.note ? ` (${h.note})` : ""}`;
646
+ // OBS-503: a crash-on-version verdict carries its reason in note — print it instead of the lie.
647
+ const state = !h?.installed ? (h?.note ?? "not installed") : `${h.version ?? "installed"}${h.note ? ` (${h.note})` : ""}`;
622
648
  return ` ${h?.installed ? "✓" : "✗"} ${a.id.padEnd(14)} ${state}`;
623
649
  });
624
650
  rows.push(` ${HerdrDriver.available() ? "✓" : "✗"} herdr ${HerdrDriver.available() ? "driver available (HERDR_ENV=1)" : "not detected — subprocess driver will be used"}`);
@@ -2,12 +2,19 @@ import { type ClaudeAlias } from "../../adapters/claude-code.js";
2
2
  import type { WorkerAdapter } from "../../adapters/types.js";
3
3
  import { type KimiDoctorTurnResult } from "../../adapters/kimi.js";
4
4
  import { type CatalogReadResult } from "../../adapters/catalog-remote.js";
5
+ /** Where a newer `table_<date>.csv` is discovered — the deployed site builds filenames by
6
+ * concatenation and publishes no index, so the release listing is the only enumerable surface. */
7
+ export declare const LIVEBENCH_RELEASES_URL = "https://api.github.com/repos/LiveBench/livebench.github.io/contents/public";
8
+ export declare const LIVEBENCH_TABLE_MAX_AGE_DAYS = 90;
5
9
  export type DoctorOpts = {
6
10
  banner?: boolean;
7
11
  kimiTurnProbe?: (cwd: string) => Promise<KimiDoctorTurnResult>;
8
12
  resolveClaudeAliasIdentity?: (cwd: string, alias: ClaudeAlias) => string | undefined;
9
13
  catalog?: CatalogReadResult;
10
14
  catalogNow?: () => Date;
15
+ /** init's between-acts surface: status rows only — the model matrix and inline drift stay
16
+ * behind `tickmarkr doctor` (files are still written; only the RETURNED string shrinks). */
17
+ compact?: boolean;
11
18
  };
12
19
  export declare function runnerIgnoreFinding(cwd: string): {
13
20
  verdict: "pass" | "warn";
@@ -55,4 +62,12 @@ export declare function selfShadowFinding(ownVersion: string, cwd?: string, reso
55
62
  verdict: "pass" | "fail";
56
63
  detail: string;
57
64
  } | undefined;
65
+ /**
66
+ * §4.4: `LIVEBENCH_TABLE_DATE` is a hand-bumped constant, and a constant nobody lints is a stale
67
+ * constant — the pinned table ages silently while every tier suggestion keeps citing it. Warns past
68
+ * 90 days, naming the pinned date and the listing where a newer table is discovered.
69
+ * ponytail: a LINT, never a fetch — doctor stays cache-only (T17); the operator (or RELEASING.md's
70
+ * release-time check) reads the listing and bumps the constant.
71
+ */
72
+ export declare function liveBenchStalenessFinding(now: Date): string | undefined;
58
73
  export declare function doctor(_argv: string[], cwd?: string, adapters?: WorkerAdapter[], opts?: DoctorOpts): Promise<string>;
@@ -8,12 +8,16 @@ import { allAdapters, binaryShadowWarnings, detectCandidateClis, flagDriftWarnin
8
8
  import { CLAUDE_ALIAS_IDENTITY_STAMPS, claudeCode, resolveClaudeAliasIdentity } from "../../adapters/claude-code.js";
9
9
  import { BANNER, dim, fail, kvRow, legend, ok, rule, statusRow, title } from "../../brand.js";
10
10
  import { tickmarkrDir, stateDirName } from "../../graph/graph.js";
11
- import { catalogModelAdvisory, declaredModelWindow, hasWindowsConfig, modelLints, suggestOverlay, ttyVisual } from "../../adapters/model-lints.js";
11
+ import { catalogModelAdvisory, catalogTierRanking, declaredModelWindow, hasWindowsConfig, modelLints, suggestOverlay, ttyVisual } from "../../adapters/model-lints.js";
12
12
  import { loadConfig, overlayPreferShapes } from "../../config/config.js";
13
13
  import { HerdrDriver } from "../../drivers/herdr.js";
14
14
  import { kimi, probeKimiDoctorTurn } from "../../adapters/kimi.js";
15
15
  import { denyPreferCollisionLine, denyPreferCollisions, disallowedBy, excludedChannels, exclusionLine, preferRanks } from "../../route/preference.js";
16
- import { readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
16
+ import { LIVEBENCH_TABLE_DATE, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
17
+ /** Where a newer `table_<date>.csv` is discovered — the deployed site builds filenames by
18
+ * concatenation and publishes no index, so the release listing is the only enumerable surface. */
19
+ export const LIVEBENCH_RELEASES_URL = "https://api.github.com/repos/LiveBench/livebench.github.io/contents/public";
20
+ export const LIVEBENCH_TABLE_MAX_AGE_DAYS = 90;
17
21
  const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
18
22
  const alignedStatusRow = (verdict, key, value) => ` ${statusRow(verdict, kvRow(key, value).slice(2))}`;
19
23
  const attentionRow = (text) => ` ${statusRow("warn", text)}`;
@@ -205,6 +209,20 @@ export function selfShadowFinding(ownVersion, cwd = process.cwd(), resolve = res
205
209
  detail: `stale tickmarkr/tkr binaries on PATH: ${listing} (running ${ownVersion}) — a seat with different PATH order silently runs the old binary; remove the stale install root`,
206
210
  };
207
211
  }
212
+ /**
213
+ * §4.4: `LIVEBENCH_TABLE_DATE` is a hand-bumped constant, and a constant nobody lints is a stale
214
+ * constant — the pinned table ages silently while every tier suggestion keeps citing it. Warns past
215
+ * 90 days, naming the pinned date and the listing where a newer table is discovered.
216
+ * ponytail: a LINT, never a fetch — doctor stays cache-only (T17); the operator (or RELEASING.md's
217
+ * release-time check) reads the listing and bumps the constant.
218
+ */
219
+ export function liveBenchStalenessFinding(now) {
220
+ const pinnedMs = Date.parse(`${LIVEBENCH_TABLE_DATE.replace(/_/g, "-")}T00:00:00Z`);
221
+ const days = Math.floor((now.getTime() - pinnedMs) / 86_400_000);
222
+ if (!Number.isFinite(days) || days <= LIVEBENCH_TABLE_MAX_AGE_DAYS)
223
+ return undefined;
224
+ return `LiveBench table pinned at ${LIVEBENCH_TABLE_DATE} is ${days} days old (>${LIVEBENCH_TABLE_MAX_AGE_DAYS}d) — list ${LIVEBENCH_RELEASES_URL} for a newer table_<date>.csv and bump LIVEBENCH_TABLE_DATE (advisory — tickmarkr never fetches that listing)`;
225
+ }
208
226
  export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(), opts = {}) {
209
227
  if (_argv.length === 1 && _argv[0] === "--refresh-catalog") {
210
228
  const refreshed = await refreshCatalogCommand({ repoRoot: cwd, now: opts.catalogNow });
@@ -272,7 +290,8 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
272
290
  writeDoctor(cwd, health);
273
291
  const rows = adapters.map((a) => {
274
292
  const h = health[a.id];
275
- const state = !h.installed ? "not installed" : `${h.version ?? "installed"}${h.note ? ` (${h.note})` : ""}`;
293
+ // OBS-503: a crash-on-version verdict carries its reason in note — print it instead of the lie.
294
+ const state = !h.installed ? (h.note ?? "not installed") : `${h.version ?? "installed"}${h.note ? ` (${h.note})` : ""}`;
276
295
  const healthy = h.installed && (a.id !== kimi.id || h.authed);
277
296
  return alignedStatusRow(healthy ? "pass" : "fail", a.id, state);
278
297
  });
@@ -340,22 +359,29 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
340
359
  rows.push(attentionRow(`${role} runs on ${sel.adapter}:${sel.model} — NOT installed; that gate will fail closed until you install it or remap cfg.${role}`));
341
360
  }
342
361
  }
343
- rows.push(...modelLints(cfg, health, adapters, { tty: ttyVisual(), stateDir: stateDirName(cwd), overlayPreferShapes: overlayPreferShapes(cwd) }).map(attentionRow));
362
+ // OBS-505 residue: advisory lints collect apart from the capability/trust rows so the compact
363
+ // init journey can elide them behind the doctor pointer (one measured lint line ran 282 chars —
364
+ // pure wrap noise between two TUIs). Full doctor output is byte-identical: they rejoin in order.
365
+ const lintRows = [];
366
+ const liveBenchStale = liveBenchStalenessFinding(opts.catalogNow?.() ?? new Date());
367
+ if (liveBenchStale)
368
+ lintRows.push(attentionRow(liveBenchStale));
369
+ lintRows.push(...modelLints(cfg, health, adapters, { tty: ttyVisual(), stateDir: stateDirName(cwd), overlayPreferShapes: overlayPreferShapes(cwd) }).map(attentionRow));
344
370
  const excluded = excludedChannels(cfg, adapters, health);
345
371
  if (excluded.length)
346
- rows.push(attentionRow(exclusionLine(excluded)));
347
- rows.push(...denyPreferCollisions(cfg).map((c) => attentionRow(denyPreferCollisionLine(c))));
372
+ lintRows.push(attentionRow(exclusionLine(excluded)));
373
+ lintRows.push(...denyPreferCollisions(cfg).map((c) => attentionRow(denyPreferCollisionLine(c))));
348
374
  const aliasExcluded = modelAliasExclusions(cfg, adapters, health);
349
375
  if (aliasExcluded.length)
350
- rows.push(attentionRow(modelAliasLine(aliasExcluded)));
351
- rows.push(...binaryShadowWarnings(adapters, health, cwd).map(attentionRow));
376
+ lintRows.push(attentionRow(modelAliasLine(aliasExcluded)));
377
+ lintRows.push(...binaryShadowWarnings(adapters, health, cwd).map(attentionRow));
352
378
  // HYG-07(a): doctor just probed fresh (probeAll above), so servability attribution is current by construction.
353
379
  const servable = servableExclusions(cfg, adapters, health);
354
380
  if (servable.length)
355
- rows.push(attentionRow(servabilityLine(servable)));
381
+ lintRows.push(attentionRow(servabilityLine(servable)));
356
382
  // v1.65 T3: hardcoded-flag drift — advisory warn rows only. Runs AFTER writeDoctor so the verdicts
357
383
  // can never leak into doctor.json, and discoverChannels/routing never read them.
358
- rows.push(...flagDriftWarnings(adapters, health).map(attentionRow));
384
+ lintRows.push(...flagDriftWarnings(adapters, health).map(attentionRow));
359
385
  // OBS-145: resolved-identity drift is the same class of display-only doctor warning. Stamps live
360
386
  // beside the alias-owning adapter; the comparison runs only for configured floating aliases and
361
387
  // never enters health/doctor.json, config, channel discovery, learned profiles, or route().
@@ -374,7 +400,7 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
374
400
  continue; // advisory source failure is unknown, never a doctor failure
375
401
  }
376
402
  if (resolvedIdentity && resolvedIdentity !== stampedIdentity) {
377
- rows.push(attentionRow(`resolved-identity drift: claude-code:${alias} resolved to ${resolvedIdentity}, stamped identity ${stampedIdentity} — reclassify per benchmark policy (advisory — routing unchanged)`));
403
+ lintRows.push(attentionRow(`resolved-identity drift: claude-code:${alias} resolved to ${resolvedIdentity}, stamped identity ${stampedIdentity} — reclassify per benchmark policy (advisory — routing unchanged)`));
378
404
  }
379
405
  }
380
406
  }
@@ -413,13 +439,23 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
413
439
  const trunc = (s, n) => (s.length <= n ? s : `${s.slice(0, n - 1)}…`);
414
440
  const dateOf = (iso) => iso.slice(0, 10);
415
441
  const showWindows = hasWindowsConfig(cfg);
442
+ const unclassifiedByAdapter = new Map(adapters.flatMap((a) => {
443
+ const h = health[a.id];
444
+ if (!h?.installed)
445
+ return [];
446
+ const classified = cfg.tiers[a.id]?.models ?? {};
447
+ return [[a.id, (h.models ?? []).filter((m) => !(m in classified))]];
448
+ }));
449
+ // Every model under advisory in THIS report bands against one universe, built before any row
450
+ // renders — otherwise an adapter's band would depend on where it sits in the iteration.
451
+ const catalogRanking = catalogTierRanking(cfg, catalog, [...unclassifiedByAdapter].flatMap(([adapter, models]) => models.map((model) => ({ adapter, model, resolvedModel: resolvedCatalogModel(adapter, model) }))), resolvedCatalogModel);
416
452
  const modelStatus = adapters.flatMap((a) => {
417
453
  const h = health[a.id];
418
454
  if (!h?.installed)
419
455
  return [];
420
456
  const classified = cfg.tiers[a.id]?.models ?? {};
421
457
  const models = Object.keys(classified);
422
- const unclassified = (h.models ?? []).filter((m) => !(m in classified));
458
+ const unclassified = unclassifiedByAdapter.get(a.id) ?? [];
423
459
  if (!models.length && !unclassified.length)
424
460
  return [];
425
461
  const w = Math.max(8, ...models.map((m) => m.length));
@@ -443,7 +479,7 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
443
479
  // Keep the historical count as an index, but it is no longer the whole report: every model gets
444
480
  // a cache-backed evidence or explicit uncovered line. Neither branch changes cfg, health, or route().
445
481
  rows.push(` ${dim(`(${unclassified.length} more listed, unclassified)`)}`);
446
- const advisories = unclassified.map((model) => catalogModelAdvisory(cfg, catalog, a.id, model, resolvedCatalogModel(a.id, model)));
482
+ const advisories = unclassified.map((model) => catalogModelAdvisory(cfg, catalog, a.id, model, resolvedCatalogModel(a.id, model), catalogRanking));
447
483
  // Q128s exhibit #4 / operator directive 2026-08-12: ~600 per-model advisory rows buried the six
448
484
  // decision-grade flags on a real machine. Default view keeps ONLY rows that ask the operator for a
449
485
  // decision (tier suggestions); the no-decision bulk (uncovered + covered evidence-only) compresses
@@ -471,5 +507,16 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
471
507
  const header = visual()
472
508
  ? `${statusRow("pass", `${title("tickmarkr doctor")} ${legend("· capability matrix")}`)}\n${rule()}`
473
509
  : "tickmarkr doctor — capability matrix:";
474
- return `${header}\n${rows.join("\n")}${modelSummary}${drift}\nwrote ${stateDirName(cwd)}/doctor.json`;
510
+ if (opts.compact) {
511
+ // Operator field report 2026-08-13: the full matrix mid-init-journey is clutter between two
512
+ // TUIs. Every file side effect above already happened; the pointer line is the contract.
513
+ // Advisory lints elide with the matrix (OBS-505 residue) — capability, trust, and fail-closed
514
+ // judge/consult rows stay; the counted pointer names what moved behind `tickmarkr doctor`.
515
+ const elided = [
516
+ ...(lintRows.length ? [`${lintRows.length} advisory lint${lintRows.length === 1 ? "" : "s"}`] : []),
517
+ "model matrix",
518
+ ].join(" and ");
519
+ return `${header}\n${rows.join("\n")}\n ${dim(`${elided} elided — \`tickmarkr doctor\` prints them in full${drift ? "; drift overlay written" : ""}`)}\nwrote ${stateDirName(cwd)}/doctor.json`;
520
+ }
521
+ return `${header}\n${[...rows, ...lintRows].join("\n")}${modelSummary}${drift}\nwrote ${stateDirName(cwd)}/doctor.json`;
475
522
  }
@@ -2,7 +2,9 @@ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileS
2
2
  import { dirname } from "node:path";
3
3
  import { parseArgs } from "node:util";
4
4
  import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse } from "../../adapters/registry.js";
5
- import { fleetUnclassifiedModels } from "../../adapters/model-lints.js";
5
+ import { catalogModelAdvisory, catalogTierRanking, fleetUnclassifiedModels } from "../../adapters/model-lints.js";
6
+ import { readCachedCatalog } from "../../adapters/catalog-remote.js";
7
+ import { CLAUDE_ALIAS_IDENTITY_STAMPS, readClaudeAliasIdentity } from "../../adapters/claude-code.js";
6
8
  import { fleetEditableFromConfig, fleetEditableEquals, formatFleetPrint, globalConfigDir, overlayBytesLoadError, renderFleetOverlayWrite, repoOverlayPath, ROUTING_MODES, unifiedYamlDiff, } from "../../config/config.js";
7
9
  import { projectFleetWhy, renderFleetWhy } from "../../config/fleet-why.js";
8
10
  import { SHAPES, TIERS } from "../../graph/schema.js";
@@ -162,17 +164,43 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
162
164
  const initial = fleetEditableFromConfig(cfg);
163
165
  const editable = structuredClone(initial);
164
166
  const health = cached;
167
+ // OBS-508: the same catalog evidence doctor's drift overlay prints now rides each unclassified
168
+ // row — it prefills the classify flow and feeds the bulk `s` stage. Suggestions stay advisory:
169
+ // only the review-diff confirm writes, so "tickmarkr never applies" holds with less typing.
170
+ const catalog = readCachedCatalog(cwd);
171
+ // The same alias→identity resolution doctor hands its advisory rows: models.dev has never heard of
172
+ // `opus`, so without it the fleet's own frontier models drop out of the universe they are supposed
173
+ // to anchor and fleet bands a different set than doctor for one fleet. The stored identity wins;
174
+ // the stamped one backs it up. No probe — fleet never re-probes, doctor is the sensor (and doctor
175
+ // is the seat that lints a stamp the live identity has drifted away from).
176
+ const resolvedCatalogModel = (adapter, model) => adapter !== "claude-code" || !(model in CLAUDE_ALIAS_IDENTITY_STAMPS)
177
+ ? undefined
178
+ : readClaudeAliasIdentity(cwd, model) ?? CLAUDE_ALIAS_IDENTITY_STAMPS[model];
179
+ // One ranking universe for the whole screen: every unclassified row bands fleet-relatively
180
+ // against the same set, so a suggestion never depends on which adapter group renders first.
181
+ const unclassifiedRows = fleetUnclassifiedModels(cfg, health, adapters)
182
+ .map((row) => ({ ...row, resolvedModel: resolvedCatalogModel(row.adapter, row.model) }));
183
+ const catalogRanking = catalogTierRanking(cfg, catalog, unclassifiedRows, resolvedCatalogModel);
165
184
  const modelGroups = adapters
166
185
  .filter((adapter) => health[adapter.id]?.installed)
167
186
  .map((adapter) => {
168
- const unclassified = fleetUnclassifiedModels(cfg, health, adapters).filter((row) => row.adapter === adapter.id);
187
+ const unclassified = unclassifiedRows.filter((row) => row.adapter === adapter.id);
169
188
  return {
170
189
  adapter: adapter.id,
171
190
  rows: [
172
191
  ...Object.entries(editable.tiers[adapter.id] ?? {}).map(([model, value]) => ({ model, tier: value?.tier })),
173
192
  ...unclassified
174
193
  .filter((row) => !editable.tiers[adapter.id]?.[row.model])
175
- .map((row) => ({ model: row.model, detectedAt: row.detectedAt })),
194
+ .map((row) => {
195
+ const advisory = catalogModelAdvisory(cfg, catalog, adapter.id, row.model, row.resolvedModel, catalogRanking);
196
+ return {
197
+ model: row.model,
198
+ detectedAt: row.detectedAt,
199
+ ...(advisory.suggestion
200
+ ? { suggestion: { tier: advisory.suggestion.tier, note: advisory.suggestion.provenanceNote } }
201
+ : {}),
202
+ };
203
+ }),
176
204
  ],
177
205
  };
178
206
  });