tickmarkr 1.90.8 → 1.92.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.
@@ -3,7 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { DEFAULT_CONFIG, TIER_RANK } from "../config/config.js";
5
5
  import { buildTaskPrompt } from "./prompt.js";
6
- import { MODEL_ID_RE } from "./types.js";
6
+ import { channelKey, MODEL_ID_RE } from "./types.js";
7
7
  import { resolveCatalogModel } from "./catalog-remote.js";
8
8
  export const SEED_STAMPED = "2026-07-09";
9
9
  // knowledge past this age gets a "rerun tickmarkr doctor" nudge (BLOCKED_POLL_MS-style named constant).
@@ -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,
@@ -219,6 +319,33 @@ function collectSeedPreferLints(cfg, health, adapters, overlayPreferShapes, disp
219
319
  export function seedPreferLints(cfg, health, adapters, overlayPreferShapes = new Set()) {
220
320
  return collectSeedPreferLints(cfg, health, adapters, overlayPreferShapes, DECLARED_SEED_PREFER_DISPOSITION);
221
321
  }
322
+ // v1.92: dead-pool lint — mirrors the dead-adapter seed lint above. A pool naming zero
323
+ // doctor-found channels renders advisory here; plan/run stays the fail-loud authority (the
324
+ // router's exhaustion RoutingError). Liveness mirrors adapterHasAuthedChannel's compat rule:
325
+ // an installed adapter without per-model probe data counts live, not dead.
326
+ export function deadPoolLints(cfg, health, adapters) {
327
+ const lints = [];
328
+ for (const [shape, entry] of Object.entries(cfg.routing.map)) {
329
+ const pool = entry.pool;
330
+ if (!pool)
331
+ continue;
332
+ const live = pool.channels.some((id) => {
333
+ const a = adapters.find((x) => x.id === id.slice(0, id.indexOf(":")));
334
+ const h = a && health[a.id];
335
+ if (!a || !h?.installed || typeof a.channels !== "function")
336
+ return false;
337
+ const c = a.channels(cfg).find((ch) => channelKey(ch) === id);
338
+ if (!c)
339
+ return false;
340
+ if (!h.modelAuth || !Object.keys(h.modelAuth).length)
341
+ return true; // no per-model probe data — compat, not dead
342
+ return h.modelAuth[c.model]?.authed === true;
343
+ });
344
+ if (!live)
345
+ lints.push(`routing.map.${shape}.pool names no live channel — declared: ${pool.channels.join(", ")}`);
346
+ }
347
+ return lints;
348
+ }
222
349
  // v1.54 T3: dead-steering sweep — operator prefer entries (routing.map overlay shapes, review.prefer,
223
350
  // consult.prefer) that can never match an installed channel are named at plan time; v1.53 T2 pins the
224
351
  // no-match case as a silent no-op, which makes a typo invisible. Advisory only: reads config + doctor
@@ -256,7 +383,12 @@ export function modelLints(cfg, health, adapters, opts) {
256
383
  for (const adapter of adapters) {
257
384
  const id = adapter.id;
258
385
  if (!adapter.listModels) {
259
- lints.push(`${id}: no model-list surface — seeds stamped ${SEED_STAMPED}; verify manually`);
386
+ // v1.90 / OBS-504: the seeds-stamped wording presumes a seeded tier table. agy ships routable
387
+ // but UNCLASSIFIED (no listModels, no seed models) — for that shape the honest sentence names
388
+ // the classification debt, not a seed stamp with nothing behind it.
389
+ lints.push(Object.keys(cfg.tiers[id]?.models ?? {}).length > 0
390
+ ? `${id}: no model-list surface — seeds stamped ${SEED_STAMPED}; verify manually`
391
+ : `${id}: no model-list surface and no configured models — classify per benchmark policy to open channels`);
260
392
  continue;
261
393
  }
262
394
  const h = health[id];
@@ -292,6 +424,7 @@ export function modelLints(cfg, health, adapters, opts) {
292
424
  ? LEGACY_DOCTOR_SEED_PREFER_DISPOSITION
293
425
  : DECLARED_SEED_PREFER_DISPOSITION;
294
426
  lints.push(...collectSeedPreferLints(cfg, health, adapters, opts?.overlayPreferShapes ?? new Set(), seedDisposition));
427
+ lints.push(...deadPoolLints(cfg, health, adapters));
295
428
  return lints;
296
429
  }
297
430
  // T2/T6: one lint per exclusion, naming the probe reason and date. TTY truncates reasons to 60 chars and
@@ -312,25 +445,20 @@ export function formatModelAuthLine(excluded, tty, stateDir = DEFAULT_STATE_DIR)
312
445
  // `<id>: null` tombstones (deepMerge deletes the key). Pure function: no fs, no routing contact.
313
446
  // Returns "" when no adapter has a delta. Mirrors modelLints' per-adapter guards exactly.
314
447
  export function suggestOverlay(cfg, health, adapters, stateDir = DEFAULT_STATE_DIR, opts = {}) {
315
- const blocks = [];
316
- for (const adapter of adapters) {
448
+ // Pass 1 — decide WHAT each adapter drifts by. Pass 2 renders, after every addition this call
449
+ // will print has joined one ranking universe: a fleet-relative band must not depend on which
450
+ // adapter happened to be iterated first.
451
+ const drifts = adapters.flatMap((adapter) => {
317
452
  const id = adapter.id;
318
453
  if (!adapter.listModels)
319
- continue; // no list surface → nothing to diff (mirror modelLints)
454
+ return []; // no list surface → nothing to diff (mirror modelLints)
320
455
  const h = health[id];
321
456
  const detected = h?.models ?? [];
322
457
  if (detected.length === 0)
323
- continue; // no detection data → don't guess a delta
458
+ return []; // no detection data → don't guess a delta
324
459
  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
460
  // 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
- }
461
+ const tombstones = configured.filter((model) => !detected.includes(model) && MODEL_ID_RE.test(model));
334
462
  // Additions: detected ids not in cfg. WHOLE line commented, no tier (MODEL-06). Ids come from an external
335
463
  // CLI → MODEL_ID_RE (defense-in-depth, T-21-01) + the variant filter (cursor's ~193 parameterized ids).
336
464
  // RELATIONAL gate (no capability judgment — "looks like an embedding model" is auto-tiering's cousin, the
@@ -344,6 +472,7 @@ export function suggestOverlay(cfg, health, adapters, stateDir = DEFAULT_STATE_D
344
472
  // not silently. ponytail: not worth a per-adapter heuristic to quiet cursor at the price of codex signal.
345
473
  const cfgPrefixes = new Set(configured.map(providerPrefix));
346
474
  const cfgCanon = new Set(configured.map(canonical));
475
+ const additions = [];
347
476
  let omitted = 0;
348
477
  for (const model of detected) {
349
478
  if (configured.includes(model) || !MODEL_ID_RE.test(model) || LINT_VARIANT_RE.test(model))
@@ -352,8 +481,23 @@ export function suggestOverlay(cfg, health, adapters, stateDir = DEFAULT_STATE_D
352
481
  omitted++;
353
482
  continue;
354
483
  }
484
+ additions.push(model);
485
+ }
486
+ return [{ id, date: h?.modelsDetectedAt?.split("T")[0], tombstones, additions, omitted }]; // date: best-effort day stamp
487
+ });
488
+ const ranking = opts.catalog
489
+ ? catalogTierRanking(cfg, opts.catalog, drifts.flatMap(({ id, additions }) => additions.map((model) => ({ adapter: id, model, resolvedModel: opts.resolvedModel?.(id, model) }))), opts.resolvedModel)
490
+ : [];
491
+ const blocks = [];
492
+ for (const { id, date, tombstones, additions, omitted } of drifts) {
493
+ const detNote = date ? ` (detected ${date})` : "";
494
+ const lines = [];
495
+ for (const model of tombstones) {
496
+ lines.push(` ${model}: null # tombstone: ${id} no longer reports this id${detNote}${referenceWarning(cfg, id, model)}`);
497
+ }
498
+ for (const model of additions) {
355
499
  const advisory = opts.catalog
356
- ? catalogModelAdvisory(cfg, opts.catalog, id, model, opts.resolvedModel?.(id, model))
500
+ ? catalogModelAdvisory(cfg, opts.catalog, id, model, opts.resolvedModel?.(id, model), ranking)
357
501
  : undefined;
358
502
  const guidance = advisory?.suggestion
359
503
  ? `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"}`);
package/dist/brand.d.ts CHANGED
@@ -80,3 +80,5 @@ export declare const kvRow: (key: string, value: string, keyWidth?: number) => s
80
80
  export type Verdict = "pass" | "fail" | "warn" | "neutral";
81
81
  /** Status row: verdict glyph FIRST, then the label — glyph-first, distinct shape per verdict. */
82
82
  export declare const statusRow: (verdict: Verdict, label: string) => string;
83
+ /** Compact token counts for tables: 1048576 → "1m", 262144 → "262k" (UI-free — safe for doctor). */
84
+ export declare function compactTokens(tokens: number): string;
package/dist/brand.js CHANGED
@@ -160,3 +160,16 @@ const VERDICT = {
160
160
  };
161
161
  /** Status row: verdict glyph FIRST, then the label — glyph-first, distinct shape per verdict. */
162
162
  export const statusRow = (verdict, label) => `${VERDICT[verdict]()} ${label}`;
163
+ /** Compact token counts for tables: 1048576 → "1m", 262144 → "262k" (UI-free — safe for doctor). */
164
+ export function compactTokens(tokens) {
165
+ if (!Number.isFinite(tokens) || tokens <= 0)
166
+ return "0";
167
+ if (tokens >= 1_000_000) {
168
+ const m = tokens / 1_000_000;
169
+ const rounded = m >= 10 ? Math.round(m) : Math.round(m * 10) / 10;
170
+ return `${String(rounded).replace(/\.0$/, "")}m`;
171
+ }
172
+ if (tokens >= 1000)
173
+ return `${Math.round(tokens / 1000)}k`;
174
+ return String(tokens);
175
+ }
@@ -2,6 +2,10 @@ 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>;
@@ -58,4 +62,12 @@ export declare function selfShadowFinding(ownVersion: string, cwd?: string, reso
58
62
  verdict: "pass" | "fail";
59
63
  detail: string;
60
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;
61
73
  export declare function doctor(_argv: string[], cwd?: string, adapters?: WorkerAdapter[], opts?: DoctorOpts): Promise<string>;