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.
- package/README.md +12 -9
- package/dist/adapters/catalog-remote.d.ts +11 -1
- package/dist/adapters/catalog-remote.js +169 -19
- package/dist/adapters/catalog.js +59 -0
- package/dist/adapters/model-lints.d.ts +37 -2
- package/dist/adapters/model-lints.js +180 -36
- package/dist/adapters/registry.js +32 -6
- package/dist/brand.d.ts +2 -0
- package/dist/brand.js +13 -0
- package/dist/cli/commands/doctor.d.ts +12 -0
- package/dist/cli/commands/doctor.js +90 -20
- package/dist/cli/commands/fleet-picker.js +1 -1
- package/dist/cli/commands/fleet.d.ts +4 -4
- package/dist/cli/commands/fleet.js +198 -37
- package/dist/cli/commands/init.js +6 -4
- package/dist/compile/native.js +5 -4
- package/dist/config/config.d.ts +34 -1
- package/dist/config/config.js +101 -5
- package/dist/config/fleet-overlay.d.ts +9 -2
- package/dist/config/fleet-overlay.js +159 -14
- package/dist/gates/acceptance.js +13 -4
- package/dist/route/preference.d.ts +1 -1
- package/dist/route/preference.js +12 -2
- package/dist/route/router.js +31 -2
- package/dist/run/daemon.js +1 -1
- package/dist/tui/ink/components.d.ts +16 -1
- package/dist/tui/ink/components.js +16 -2
- package/dist/tui/ink/fleet-app.d.ts +57 -8
- package/dist/tui/ink/fleet-app.js +1228 -727
- package/dist/tui/ink/frame.d.ts +62 -0
- package/dist/tui/ink/frame.js +204 -0
- package/dist/tui/ink/init-app.d.ts +2 -1
- package/dist/tui/ink/init-app.js +28 -100
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
|
42
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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>;
|