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
|
@@ -6,14 +6,18 @@ import { detectPackageManager, turboContinueFindings } from "../../gates/baselin
|
|
|
6
6
|
import { version } from "./version.js";
|
|
7
7
|
import { allAdapters, binaryShadowWarnings, detectCandidateClis, flagDriftWarnings, modelAliasExclusions, modelAliasLine, probeAll, probeModels, resolveShellBinary, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
|
|
8
8
|
import { CLAUDE_ALIAS_IDENTITY_STAMPS, claudeCode, resolveClaudeAliasIdentity } from "../../adapters/claude-code.js";
|
|
9
|
-
import { BANNER, dim, fail, kvRow, legend, ok, rule, statusRow, title } from "../../brand.js";
|
|
9
|
+
import { BANNER, compactTokens, 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 });
|
|
@@ -266,13 +284,27 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
266
284
|
const modelProbeAdapters = kimiTurnEnabled && health.kimi.authed === false
|
|
267
285
|
? adapters.filter((a) => a !== kimiAdapter)
|
|
268
286
|
: adapters;
|
|
287
|
+
// One rewriting stderr line instead of a scrolling line per model (operator report 2026-08-14:
|
|
288
|
+
// 20 probe lines buried the journey). Totals print once at the end; non-TTY stays silent.
|
|
289
|
+
let probeOk = 0;
|
|
290
|
+
let probeBad = 0;
|
|
269
291
|
await probeModels(cfg, cwd, modelProbeAdapters, health, probeProgressTTY
|
|
270
|
-
? (adapter, model, status, durationMs) =>
|
|
292
|
+
? (adapter, model, status, durationMs) => {
|
|
293
|
+
if (status === "ok")
|
|
294
|
+
probeOk += 1;
|
|
295
|
+
else
|
|
296
|
+
probeBad += 1;
|
|
297
|
+
process.stderr.write(`\r\x1b[2K probed ${probeOk + probeBad} channels · ${probeOk} ok${probeBad ? ` · ${probeBad} failed` : ""} · ${adapter}:${model} ${status} (${(durationMs / 1000).toFixed(1)}s)`);
|
|
298
|
+
}
|
|
271
299
|
: undefined);
|
|
300
|
+
if (probeProgressTTY && probeOk + probeBad > 0) {
|
|
301
|
+
process.stderr.write(`\r\x1b[2K probed ${probeOk + probeBad} channels · ${probeOk} ok${probeBad ? ` · ${probeBad} failed` : ""}\n`);
|
|
302
|
+
}
|
|
272
303
|
writeDoctor(cwd, health);
|
|
273
304
|
const rows = adapters.map((a) => {
|
|
274
305
|
const h = health[a.id];
|
|
275
|
-
|
|
306
|
+
// OBS-503: a crash-on-version verdict carries its reason in note — print it instead of the lie.
|
|
307
|
+
const state = !h.installed ? (h.note ?? "not installed") : `${h.version ?? "installed"}${h.note ? ` (${h.note})` : ""}`;
|
|
276
308
|
const healthy = h.installed && (a.id !== kimi.id || h.authed);
|
|
277
309
|
return alignedStatusRow(healthy ? "pass" : "fail", a.id, state);
|
|
278
310
|
});
|
|
@@ -314,11 +346,15 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
314
346
|
// v1.22 T5: workspace-trust pre-flight — per installed adapter: trusted | seeded | action-required | n/a.
|
|
315
347
|
// action-required names the exact one-time command (or dialog) the operator must run once.
|
|
316
348
|
rows.push(legend("workspace trust:"));
|
|
349
|
+
// Declutter (operator report 2026-08-14): n/a trust rows carried zero decisions — nine of them
|
|
350
|
+
// drowned the one action-required line. They compress to a single dim roster; actionable rows keep
|
|
351
|
+
// their own verdict lines.
|
|
352
|
+
const trustNa = [];
|
|
317
353
|
for (const a of adapters) {
|
|
318
354
|
if (!health[a.id]?.installed)
|
|
319
355
|
continue;
|
|
320
356
|
if (!a.trust) {
|
|
321
|
-
|
|
357
|
+
trustNa.push(a.id);
|
|
322
358
|
continue;
|
|
323
359
|
}
|
|
324
360
|
try {
|
|
@@ -335,27 +371,36 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
335
371
|
rows.push(alignedStatusRow("warn", a.id, `trust: action-required — run ONCE: (trust check failed: ${e instanceof Error ? e.message : String(e)})`));
|
|
336
372
|
}
|
|
337
373
|
}
|
|
374
|
+
if (trustNa.length)
|
|
375
|
+
rows.push(` ${dim("=")} ${dim(`n/a (${trustNa.length}): ${trustNa.join(", ")}`)}`);
|
|
338
376
|
for (const [role, sel] of [["judge", cfg.judge], ["consult", cfg.consult]]) {
|
|
339
377
|
if (!health[sel.adapter]?.installed) {
|
|
340
378
|
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
379
|
}
|
|
342
380
|
}
|
|
343
|
-
|
|
381
|
+
// OBS-505 residue: advisory lints collect apart from the capability/trust rows so the compact
|
|
382
|
+
// init journey can elide them behind the doctor pointer (one measured lint line ran 282 chars —
|
|
383
|
+
// pure wrap noise between two TUIs). Full doctor output is byte-identical: they rejoin in order.
|
|
384
|
+
const lintRows = [];
|
|
385
|
+
const liveBenchStale = liveBenchStalenessFinding(opts.catalogNow?.() ?? new Date());
|
|
386
|
+
if (liveBenchStale)
|
|
387
|
+
lintRows.push(attentionRow(liveBenchStale));
|
|
388
|
+
lintRows.push(...modelLints(cfg, health, adapters, { tty: ttyVisual(), stateDir: stateDirName(cwd), overlayPreferShapes: overlayPreferShapes(cwd) }).map(attentionRow));
|
|
344
389
|
const excluded = excludedChannels(cfg, adapters, health);
|
|
345
390
|
if (excluded.length)
|
|
346
|
-
|
|
347
|
-
|
|
391
|
+
lintRows.push(attentionRow(exclusionLine(excluded)));
|
|
392
|
+
lintRows.push(...denyPreferCollisions(cfg).map((c) => attentionRow(denyPreferCollisionLine(c))));
|
|
348
393
|
const aliasExcluded = modelAliasExclusions(cfg, adapters, health);
|
|
349
394
|
if (aliasExcluded.length)
|
|
350
|
-
|
|
351
|
-
|
|
395
|
+
lintRows.push(attentionRow(modelAliasLine(aliasExcluded)));
|
|
396
|
+
lintRows.push(...binaryShadowWarnings(adapters, health, cwd).map(attentionRow));
|
|
352
397
|
// HYG-07(a): doctor just probed fresh (probeAll above), so servability attribution is current by construction.
|
|
353
398
|
const servable = servableExclusions(cfg, adapters, health);
|
|
354
399
|
if (servable.length)
|
|
355
|
-
|
|
400
|
+
lintRows.push(attentionRow(servabilityLine(servable)));
|
|
356
401
|
// v1.65 T3: hardcoded-flag drift — advisory warn rows only. Runs AFTER writeDoctor so the verdicts
|
|
357
402
|
// can never leak into doctor.json, and discoverChannels/routing never read them.
|
|
358
|
-
|
|
403
|
+
lintRows.push(...flagDriftWarnings(adapters, health).map(attentionRow));
|
|
359
404
|
// OBS-145: resolved-identity drift is the same class of display-only doctor warning. Stamps live
|
|
360
405
|
// beside the alias-owning adapter; the comparison runs only for configured floating aliases and
|
|
361
406
|
// never enters health/doctor.json, config, channel discovery, learned profiles, or route().
|
|
@@ -374,7 +419,7 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
374
419
|
continue; // advisory source failure is unknown, never a doctor failure
|
|
375
420
|
}
|
|
376
421
|
if (resolvedIdentity && resolvedIdentity !== stampedIdentity) {
|
|
377
|
-
|
|
422
|
+
lintRows.push(attentionRow(`resolved-identity drift: claude-code:${alias} resolved to ${resolvedIdentity}, stamped identity ${stampedIdentity} — reclassify per benchmark policy (advisory — routing unchanged)`));
|
|
378
423
|
}
|
|
379
424
|
}
|
|
380
425
|
}
|
|
@@ -413,29 +458,43 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
413
458
|
const trunc = (s, n) => (s.length <= n ? s : `${s.slice(0, n - 1)}…`);
|
|
414
459
|
const dateOf = (iso) => iso.slice(0, 10);
|
|
415
460
|
const showWindows = hasWindowsConfig(cfg);
|
|
461
|
+
const unclassifiedByAdapter = new Map(adapters.flatMap((a) => {
|
|
462
|
+
const h = health[a.id];
|
|
463
|
+
if (!h?.installed)
|
|
464
|
+
return [];
|
|
465
|
+
const classified = cfg.tiers[a.id]?.models ?? {};
|
|
466
|
+
return [[a.id, (h.models ?? []).filter((m) => !(m in classified))]];
|
|
467
|
+
}));
|
|
468
|
+
// Every model under advisory in THIS report bands against one universe, built before any row
|
|
469
|
+
// renders — otherwise an adapter's band would depend on where it sits in the iteration.
|
|
470
|
+
const catalogRanking = catalogTierRanking(cfg, catalog, [...unclassifiedByAdapter].flatMap(([adapter, models]) => models.map((model) => ({ adapter, model, resolvedModel: resolvedCatalogModel(adapter, model) }))), resolvedCatalogModel);
|
|
416
471
|
const modelStatus = adapters.flatMap((a) => {
|
|
417
472
|
const h = health[a.id];
|
|
418
473
|
if (!h?.installed)
|
|
419
474
|
return [];
|
|
420
475
|
const classified = cfg.tiers[a.id]?.models ?? {};
|
|
421
476
|
const models = Object.keys(classified);
|
|
422
|
-
const unclassified = (
|
|
477
|
+
const unclassified = unclassifiedByAdapter.get(a.id) ?? [];
|
|
423
478
|
if (!models.length && !unclassified.length)
|
|
424
479
|
return [];
|
|
425
480
|
const w = Math.max(8, ...models.map((m) => m.length));
|
|
426
481
|
const rows = [` ${dim(a.id)}`];
|
|
427
482
|
for (const m of models) {
|
|
428
483
|
const v = h.modelAuth?.[m];
|
|
484
|
+
// probe wall clock (ProbedModelAuth.durationMs) — pre-v1.91 doctor.json rows lack it
|
|
485
|
+
const probedMs = v?.durationMs;
|
|
486
|
+
const probed = probedMs !== undefined ? dim(` ${(probedMs / 1000).toFixed(1)}s`) : "";
|
|
429
487
|
const auth = !v
|
|
430
488
|
? dim("unknown")
|
|
431
489
|
: v.authed
|
|
432
|
-
? ok("authed")
|
|
490
|
+
? `${ok("authed")}${probed}`
|
|
433
491
|
: `${fail("unauthed:")} ${trunc(v.reason ?? "probe failed", 40)} (${dateOf(v.probedAt)})`;
|
|
434
492
|
const d = disallowedBy({ adapter: a.id, model: m }, cfg.routing);
|
|
435
493
|
const denied = d?.by === "deny" ? d.entry : "—";
|
|
436
494
|
const pref = preferRanks({ adapter: a.id, model: m }, cfg).map((p) => `${p.shape}#${p.rank}`).join(",") || "—";
|
|
495
|
+
const window = declaredModelWindow(cfg, a.id, m);
|
|
437
496
|
const windowCol = showWindows
|
|
438
|
-
? ` ${
|
|
497
|
+
? ` ${(window !== undefined ? compactTokens(window) : "—").padEnd(5)}`
|
|
439
498
|
: "";
|
|
440
499
|
rows.push(` ${m.padEnd(w)} ${classified[m].padEnd(8)}${windowCol} ${auth} ${dim("denied=")}${denied} ${dim("prefer=")}${pref}`);
|
|
441
500
|
}
|
|
@@ -443,7 +502,7 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
443
502
|
// Keep the historical count as an index, but it is no longer the whole report: every model gets
|
|
444
503
|
// a cache-backed evidence or explicit uncovered line. Neither branch changes cfg, health, or route().
|
|
445
504
|
rows.push(` ${dim(`(${unclassified.length} more listed, unclassified)`)}`);
|
|
446
|
-
const advisories = unclassified.map((model) => catalogModelAdvisory(cfg, catalog, a.id, model, resolvedCatalogModel(a.id, model)));
|
|
505
|
+
const advisories = unclassified.map((model) => catalogModelAdvisory(cfg, catalog, a.id, model, resolvedCatalogModel(a.id, model), catalogRanking));
|
|
447
506
|
// Q128s exhibit #4 / operator directive 2026-08-12: ~600 per-model advisory rows buried the six
|
|
448
507
|
// decision-grade flags on a real machine. Default view keeps ONLY rows that ask the operator for a
|
|
449
508
|
// decision (tier suggestions); the no-decision bulk (uncovered + covered evidence-only) compresses
|
|
@@ -474,7 +533,18 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
474
533
|
if (opts.compact) {
|
|
475
534
|
// Operator field report 2026-08-13: the full matrix mid-init-journey is clutter between two
|
|
476
535
|
// TUIs. Every file side effect above already happened; the pointer line is the contract.
|
|
477
|
-
|
|
536
|
+
// Advisory lints elide with the matrix (OBS-505 residue) — capability, trust, and fail-closed
|
|
537
|
+
// judge/consult rows stay; the counted pointer names what moved behind `tickmarkr doctor`.
|
|
538
|
+
const elided = [
|
|
539
|
+
...(lintRows.length ? [`${lintRows.length} advisory lint${lintRows.length === 1 ? "" : "s"}`] : []),
|
|
540
|
+
"model matrix",
|
|
541
|
+
].join(" and ");
|
|
542
|
+
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`;
|
|
478
543
|
}
|
|
479
|
-
|
|
544
|
+
// Attention rows earn a counted section header so the matrix, the lints, and the model table
|
|
545
|
+
// read as three sections instead of one undifferentiated wall (operator report 2026-08-14).
|
|
546
|
+
const lintSection = lintRows.length
|
|
547
|
+
? `\n${legend(`attention (${lintRows.length}):`)}\n${lintRows.join("\n")}`
|
|
548
|
+
: "";
|
|
549
|
+
return `${header}\n${rows.join("\n")}${lintSection}${modelSummary}${drift}\nwrote ${stateDirName(cwd)}/doctor.json`;
|
|
480
550
|
}
|
|
@@ -19,7 +19,7 @@ export function candidateRow(c, pricing) {
|
|
|
19
19
|
// order — route() returns a map pin first and fail-louds on its exclusion, which would collapse
|
|
20
20
|
// the picker to a single row. No hidden mutation: fresh cfg objects only.
|
|
21
21
|
export function shapeCandidates(task, cfg, channels, profile) {
|
|
22
|
-
const { pin: _pin, ...entry } = cfg.routing.map[task.shape] ?? {};
|
|
22
|
+
const { pin: _pin, pool: _pool, ...entry } = cfg.routing.map[task.shape] ?? {};
|
|
23
23
|
const map = { ...cfg.routing.map, [task.shape]: entry };
|
|
24
24
|
return rankCandidates(task, { ...cfg, routing: { ...cfg.routing, map } }, channels, profile);
|
|
25
25
|
}
|
|
@@ -29,13 +29,13 @@ export declare function fleet(argv: string[], cwd?: string, adapters?: WorkerAda
|
|
|
29
29
|
code: number;
|
|
30
30
|
}>;
|
|
31
31
|
/** Everything between the doctor-reuse gate and the Ink render, packaged for reuse: `tickmarkr init`
|
|
32
|
-
* runs the same editor (
|
|
33
|
-
*
|
|
34
|
-
* and performs the one guarded
|
|
32
|
+
* runs the same editor (entry="presets": Esc is HOME to the preset overlay, never a bare quit)
|
|
33
|
+
* without re-owning the assembly or the write funnel. `commit` remains the single config actuator —
|
|
34
|
+
* it maps editor results to the command's exact result strings and performs the one guarded
|
|
35
|
+
* overlay write. */
|
|
35
36
|
export declare function assembleFleetEditor(cwd: string, adapters: WorkerAdapter[], io: FleetIO, opts: {
|
|
36
37
|
globalDir: string;
|
|
37
38
|
entry?: "presets" | "probe";
|
|
38
|
-
fresh?: boolean;
|
|
39
39
|
}): Promise<{
|
|
40
40
|
props: FleetEditorProps;
|
|
41
41
|
commit: (result: FleetEditorResult) => string;
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
|
-
import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse } from "../../adapters/registry.js";
|
|
5
|
-
import { fleetUnclassifiedModels } from "../../adapters/model-lints.js";
|
|
4
|
+
import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse, modelAuthExclusions } from "../../adapters/registry.js";
|
|
5
|
+
import { catalogModelAdvisory, catalogTierRanking, declaredModelWindow, 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";
|
|
11
|
+
import { doctor } from "./doctor.js";
|
|
9
12
|
import { candidateRow, costSignal, shapeCandidates } from "./fleet-picker.js";
|
|
10
13
|
import { route } from "../../route/router.js";
|
|
14
|
+
import { disallowedBy } from "../../route/preference.js";
|
|
11
15
|
import { resolveRunMode } from "../../run/daemon.js";
|
|
12
16
|
import { loadRoutingProfile } from "../../run/journal.js";
|
|
13
17
|
const NON_TTY_MSG = "tickmarkr fleet: interactive fleet editor requires a TTY — use `tickmarkr fleet --print` for non-interactive output";
|
|
@@ -104,10 +108,18 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
104
108
|
}
|
|
105
109
|
if (!why && !interactive)
|
|
106
110
|
return { out: NON_TTY_MSG, code: 1 };
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
// OBS-528: `--fresh` parsed since v1.92 but nothing ever RAN the probe — it forced the reuse
|
|
112
|
+
// gate false and guaranteed the "probe data missing or stale" refusal. The law stands — the
|
|
113
|
+
// EDITOR never probes (previews stay cache-only, `r` still exits to the operator) — but the
|
|
114
|
+
// command may run the sensor up front, exactly like init's act 2: a stale cache (or --fresh)
|
|
115
|
+
// probes first, visibly, then the editor assembles from what the probe just recorded.
|
|
116
|
+
if (!why && interactive) {
|
|
117
|
+
const { reuse } = initDoctorReuse(cwd, values.fresh ?? false);
|
|
118
|
+
if (!reuse) {
|
|
119
|
+
output.write(`${await doctor([], cwd, adapters, { banner: false, compact: true })}\n`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const assembled = await assembleFleetEditor(cwd, adapters, io, { globalDir });
|
|
111
123
|
if ("unavailable" in assembled)
|
|
112
124
|
return { out: assembled.unavailable, code: 1 };
|
|
113
125
|
if (why)
|
|
@@ -146,33 +158,98 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
146
158
|
return assembled.commit(result);
|
|
147
159
|
}
|
|
148
160
|
/** Everything between the doctor-reuse gate and the Ink render, packaged for reuse: `tickmarkr init`
|
|
149
|
-
* runs the same editor (
|
|
150
|
-
*
|
|
151
|
-
* and performs the one guarded
|
|
161
|
+
* runs the same editor (entry="presets": Esc is HOME to the preset overlay, never a bare quit)
|
|
162
|
+
* without re-owning the assembly or the write funnel. `commit` remains the single config actuator —
|
|
163
|
+
* it maps editor results to the command's exact result strings and performs the one guarded
|
|
164
|
+
* overlay write. */
|
|
152
165
|
export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
153
166
|
const globalDir = opts.globalDir;
|
|
154
|
-
const { reuse, health: cached } = initDoctorReuse(cwd,
|
|
167
|
+
const { reuse, health: cached } = initDoctorReuse(cwd, false);
|
|
155
168
|
if (!reuse || !cached) {
|
|
156
169
|
return {
|
|
157
|
-
unavailable: "tickmarkr fleet: probe data missing or stale — run `tickmarkr doctor` first
|
|
170
|
+
unavailable: "tickmarkr fleet: probe data missing or stale — run `tickmarkr doctor` first, or `tickmarkr fleet` interactively (it runs the probe itself when the cache is stale; --fresh forces one)",
|
|
158
171
|
};
|
|
159
172
|
}
|
|
160
173
|
const rm = resolveRunMode(cwd, { globalDir });
|
|
161
174
|
const cfg = rm.cfg;
|
|
162
|
-
const initial = fleetEditableFromConfig(cfg);
|
|
163
|
-
const editable = structuredClone(initial);
|
|
164
175
|
const health = cached;
|
|
176
|
+
// v1.92: the discovered fleet universe — every channel the adapters actually SERVE, discovered
|
|
177
|
+
// blind to both membership scopes (deny and allow). Membership is an allow-complement, which is
|
|
178
|
+
// only computable against the served universe: cfg.tiers alone would drop served-but-unclassified
|
|
179
|
+
// channels (adapter-declared extras) out of the fleet the moment any exclusion is staged.
|
|
180
|
+
// fleetEditableFromConfig folds routing.allow into the editor's exclusion sets with it, and the
|
|
181
|
+
// writer emits the minimal allow form from it. The same pool later ranks every preview.
|
|
182
|
+
const { deny: _diskDeny, allow: _diskAllow, ...routingScopeBlind } = cfg.routing;
|
|
183
|
+
const previewChannels = discoverChannels({ ...cfg, routing: routingScopeBlind }, adapters, health);
|
|
184
|
+
const universe = adapters
|
|
185
|
+
.filter((adapter) => health[adapter.id]?.installed)
|
|
186
|
+
.map((adapter) => ({
|
|
187
|
+
adapter: adapter.id,
|
|
188
|
+
models: [...new Set(previewChannels.filter((c) => c.adapter === adapter.id).map((c) => c.model))],
|
|
189
|
+
}));
|
|
190
|
+
const initial = fleetEditableFromConfig(cfg, universe);
|
|
191
|
+
const editable = structuredClone(initial);
|
|
192
|
+
// OBS-508: the same catalog evidence doctor's drift overlay prints now rides each unclassified
|
|
193
|
+
// row — it prefills the classify flow and feeds the bulk `s` stage. Suggestions stay advisory:
|
|
194
|
+
// only the review-diff confirm writes, so "tickmarkr never applies" holds with less typing.
|
|
195
|
+
const catalog = readCachedCatalog(cwd);
|
|
196
|
+
// The same alias→identity resolution doctor hands its advisory rows: models.dev has never heard of
|
|
197
|
+
// `opus`, so without it the fleet's own frontier models drop out of the universe they are supposed
|
|
198
|
+
// to anchor and fleet bands a different set than doctor for one fleet. The stored identity wins;
|
|
199
|
+
// the stamped one backs it up. No probe — fleet never re-probes, doctor is the sensor (and doctor
|
|
200
|
+
// is the seat that lints a stamp the live identity has drifted away from).
|
|
201
|
+
const resolvedCatalogModel = (adapter, model) => adapter !== "claude-code" || !(model in CLAUDE_ALIAS_IDENTITY_STAMPS)
|
|
202
|
+
? undefined
|
|
203
|
+
: readClaudeAliasIdentity(cwd, model) ?? CLAUDE_ALIAS_IDENTITY_STAMPS[model];
|
|
204
|
+
// One ranking universe for the whole screen: every unclassified row bands fleet-relatively
|
|
205
|
+
// against the same set, so a suggestion never depends on which adapter group renders first.
|
|
206
|
+
const unclassifiedRows = fleetUnclassifiedModels(cfg, health, adapters)
|
|
207
|
+
.map((row) => ({ ...row, resolvedModel: resolvedCatalogModel(row.adapter, row.model) }));
|
|
208
|
+
const catalogRanking = catalogTierRanking(cfg, catalog, unclassifiedRows, resolvedCatalogModel);
|
|
209
|
+
// OBS-508 follow-through: the browser renders the metadata the assembler always had — catalog
|
|
210
|
+
// ctx/price evidence and doctor's model-probe wall clock — as columns instead of dropping them.
|
|
211
|
+
const rowEvidence = (adapter, model, evidence) => {
|
|
212
|
+
const probed = health[adapter]?.modelAuth?.[model];
|
|
213
|
+
const contextWindow = evidence?.contextWindow ?? declaredModelWindow(cfg, adapter, model);
|
|
214
|
+
const out = {
|
|
215
|
+
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
216
|
+
...(evidence?.outputWindow !== undefined ? { outputWindow: evidence.outputWindow } : {}),
|
|
217
|
+
...(evidence?.inputCostPerMtok !== undefined ? { inputCostPerMtok: evidence.inputCostPerMtok } : {}),
|
|
218
|
+
...(evidence?.outputCostPerMtok !== undefined ? { outputCostPerMtok: evidence.outputCostPerMtok } : {}),
|
|
219
|
+
...(probed?.durationMs !== undefined ? { probeMs: probed.durationMs } : {}),
|
|
220
|
+
// OBS-519: doctor already recorded the failed verdict — a rate-limited/unauthed model must
|
|
221
|
+
// not render identically to a healthy row on the surface that edits its fleet membership.
|
|
222
|
+
...(probed?.authed === false ? { unauthed: probed.reason ?? "probe failed" } : {}),
|
|
223
|
+
};
|
|
224
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
225
|
+
};
|
|
165
226
|
const modelGroups = adapters
|
|
166
227
|
.filter((adapter) => health[adapter.id]?.installed)
|
|
167
228
|
.map((adapter) => {
|
|
168
|
-
const unclassified =
|
|
229
|
+
const unclassified = unclassifiedRows.filter((row) => row.adapter === adapter.id);
|
|
169
230
|
return {
|
|
170
231
|
adapter: adapter.id,
|
|
232
|
+
channel: cfg.tiers[adapter.id]?.channel,
|
|
171
233
|
rows: [
|
|
172
|
-
...Object.entries(editable.tiers[adapter.id] ?? {}).map(([model, value]) =>
|
|
234
|
+
...Object.entries(editable.tiers[adapter.id] ?? {}).map(([model, value]) => {
|
|
235
|
+
const advisory = catalogModelAdvisory(cfg, catalog, adapter.id, model, resolvedCatalogModel(adapter.id, model), catalogRanking);
|
|
236
|
+
const evidence = rowEvidence(adapter.id, model, advisory.coverage === "covered" ? advisory.evidence : undefined);
|
|
237
|
+
return { model, tier: value?.tier, ...(evidence ? { evidence } : {}) };
|
|
238
|
+
}),
|
|
173
239
|
...unclassified
|
|
174
240
|
.filter((row) => !editable.tiers[adapter.id]?.[row.model])
|
|
175
|
-
.map((row) =>
|
|
241
|
+
.map((row) => {
|
|
242
|
+
const advisory = catalogModelAdvisory(cfg, catalog, adapter.id, row.model, row.resolvedModel, catalogRanking);
|
|
243
|
+
const evidence = rowEvidence(adapter.id, row.model, advisory.coverage === "covered" ? advisory.evidence : undefined);
|
|
244
|
+
return {
|
|
245
|
+
model: row.model,
|
|
246
|
+
detectedAt: row.detectedAt,
|
|
247
|
+
...(evidence ? { evidence } : {}),
|
|
248
|
+
...(advisory.suggestion
|
|
249
|
+
? { suggestion: { tier: advisory.suggestion.tier, note: advisory.suggestion.provenanceNote } }
|
|
250
|
+
: {}),
|
|
251
|
+
};
|
|
252
|
+
}),
|
|
176
253
|
],
|
|
177
254
|
};
|
|
178
255
|
});
|
|
@@ -184,18 +261,38 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
184
261
|
]));
|
|
185
262
|
const channels = discoverChannels(cfg, adapters, health);
|
|
186
263
|
const profile = loadRoutingProfile(cwd, cfg, { preview: true });
|
|
187
|
-
const previewCfg = (mode, map) =>
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
264
|
+
const previewCfg = (mode, map, deny) => {
|
|
265
|
+
// mirror the writer: only the adapters/models scopes are fleet-editable; workers deny survives
|
|
266
|
+
const nextDeny = { ...cfg.routing.deny };
|
|
267
|
+
if (deny.adapters.length)
|
|
268
|
+
nextDeny.adapters = deny.adapters;
|
|
269
|
+
else
|
|
270
|
+
delete nextDeny.adapters;
|
|
271
|
+
if (deny.models.length)
|
|
272
|
+
nextDeny.models = deny.models;
|
|
273
|
+
else
|
|
274
|
+
delete nextDeny.models;
|
|
275
|
+
const routing = {
|
|
276
|
+
...routingScopeBlind,
|
|
277
|
+
map,
|
|
278
|
+
floors: modeCfgs[mode].cfg.routing.floors,
|
|
279
|
+
...(Object.keys(nextDeny).length ? { deny: nextDeny } : {}),
|
|
280
|
+
};
|
|
281
|
+
return { ...cfg, routing };
|
|
282
|
+
};
|
|
283
|
+
// route() never deny-filters its pool — that contract lives in discoverChannels — so every
|
|
284
|
+
// preview call rebuilds the pool the staged deny would discover
|
|
285
|
+
const previewPool = (cfgPreview) => previewChannels.filter((c) => disallowedBy(c, cfgPreview.routing) === null);
|
|
286
|
+
const modeSpend = (mode, map, deny) => {
|
|
192
287
|
const tierCount = {};
|
|
193
288
|
let subs = 0;
|
|
194
289
|
let apiN = 0;
|
|
195
290
|
let apiUsd = 0;
|
|
291
|
+
const cfgPreview = previewCfg(mode, map, deny);
|
|
292
|
+
const pool = previewPool(cfgPreview);
|
|
196
293
|
for (const shape of SHAPES) {
|
|
197
294
|
try {
|
|
198
|
-
const assignment = route(previewTask(shape),
|
|
295
|
+
const assignment = route(previewTask(shape), cfgPreview, pool, profile, undefined, undefined, PREVIEW_EXPLORE).assignment;
|
|
199
296
|
tierCount[assignment.tier] = (tierCount[assignment.tier] ?? 0) + 1;
|
|
200
297
|
if (assignment.channel === "sub")
|
|
201
298
|
subs += 1;
|
|
@@ -250,13 +347,20 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
250
347
|
? { declaredAt: `routing.floors.${shape}` }
|
|
251
348
|
: { declaredAt: "routing.mode" };
|
|
252
349
|
};
|
|
253
|
-
const projectedShapeRows = (mode, map) => {
|
|
350
|
+
const projectedShapeRows = (mode, map, deny) => {
|
|
351
|
+
const cfgPreview = previewCfg(mode, map, deny);
|
|
352
|
+
const pool = previewPool(cfgPreview);
|
|
254
353
|
const values = SHAPES.map((shape) => {
|
|
255
354
|
try {
|
|
256
|
-
const routed = route(previewTask(shape),
|
|
355
|
+
const routed = route(previewTask(shape), cfgPreview, pool, profile, undefined, undefined, PREVIEW_EXPLORE);
|
|
257
356
|
const assignment = routed.assignment;
|
|
258
|
-
|
|
259
|
-
|
|
357
|
+
// OBS-531: a pooled shape used to render only the routed WINNER — indistinguishable from
|
|
358
|
+
// a pin. The declaration is the operator's decision; the winner is today's dice.
|
|
359
|
+
const declaredPool = map[shape]?.pool;
|
|
360
|
+
const poolPrefix = declaredPool ? `pool(${declaredPool.mode}·${declaredPool.channels.length}) → ` : "";
|
|
361
|
+
const effective = `${poolPrefix}${assignment.adapter}:${assignment.model} (${assignment.channel}, ${assignment.tier}) ${costSignal(assignment, cfg.pricing)}`;
|
|
362
|
+
const mapProducedValue = map[shape]?.pin !== undefined || map[shape]?.pool !== undefined
|
|
363
|
+
|| routed.provenance.includes("via prefer");
|
|
260
364
|
return {
|
|
261
365
|
id: shape,
|
|
262
366
|
effective,
|
|
@@ -264,7 +368,8 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
264
368
|
};
|
|
265
369
|
}
|
|
266
370
|
catch (error) {
|
|
267
|
-
const mapProducedValue = map[shape]?.pin !== undefined ||
|
|
371
|
+
const mapProducedValue = map[shape]?.pin !== undefined || map[shape]?.pool !== undefined
|
|
372
|
+
|| (map[shape]?.prefer?.length ?? 0) > 0;
|
|
268
373
|
return {
|
|
269
374
|
id: shape,
|
|
270
375
|
effective: error.message,
|
|
@@ -275,12 +380,49 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
275
380
|
});
|
|
276
381
|
return projectFleetWhy(values, { repoRoot: cwd, globalDir });
|
|
277
382
|
};
|
|
278
|
-
const routedShapeRows = (mode, map) => projectedShapeRows(mode, map).map(({ id, label }) => ({ id, label }));
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
383
|
+
const routedShapeRows = (mode, map, deny) => projectedShapeRows(mode, map, deny).map(({ id, label }) => ({ id, label }));
|
|
384
|
+
// OBS-530: the picker silently omitted every excluded channel — the operator asked "why is X
|
|
385
|
+
// missing" three times in one session and the answer lived only in config archaeology. One dim
|
|
386
|
+
// line now names each bucket. Static buckets (unauthed, unclassified) computed once; the
|
|
387
|
+
// staged-vs-disk split recomputed per open because it ranks against the SESSION's deny state.
|
|
388
|
+
const unauthedClis = adapters
|
|
389
|
+
.filter((adapter) => health[adapter.id]?.installed && health[adapter.id]?.authed === false)
|
|
390
|
+
.map((adapter) => adapter.id);
|
|
391
|
+
const unauthedModelCount = modelAuthExclusions(cfg, adapters, health).length;
|
|
392
|
+
const excludedNoteFor = (deny) => {
|
|
393
|
+
let stagedOut = 0;
|
|
394
|
+
let denied = 0;
|
|
395
|
+
const stagedRouting = previewCfg(rm.mode.mode, {}, deny).routing;
|
|
396
|
+
for (const channel of previewChannels) {
|
|
397
|
+
if (disallowedBy(channel, stagedRouting) === null)
|
|
398
|
+
continue;
|
|
399
|
+
if (disallowedBy(channel, cfg.routing) === null)
|
|
400
|
+
stagedOut += 1;
|
|
401
|
+
else
|
|
402
|
+
denied += 1;
|
|
403
|
+
}
|
|
404
|
+
const parts = [];
|
|
405
|
+
if (stagedOut)
|
|
406
|
+
parts.push(`${stagedOut} staged out this session`);
|
|
407
|
+
if (denied)
|
|
408
|
+
parts.push(`${denied} denied in config`);
|
|
409
|
+
if (unauthedModelCount)
|
|
410
|
+
parts.push(`${unauthedModelCount} unauthed`);
|
|
411
|
+
if (unauthedClis.length)
|
|
412
|
+
parts.push(`${unauthedClis.join("/")} CLI unauthed`);
|
|
413
|
+
if (unclassifiedRows.length)
|
|
414
|
+
parts.push(`${unclassifiedRows.length} unclassified (never routed)`);
|
|
415
|
+
return parts.length ? `not offered: ${parts.join(" · ")} — the models view explains each row` : undefined;
|
|
416
|
+
};
|
|
417
|
+
const candidatesForShape = (shape, mode, map, deny) => {
|
|
418
|
+
const cfgPreview = previewCfg(mode, map, deny);
|
|
419
|
+
const rows = shapeCandidates(previewTask(shape), cfgPreview, previewPool(cfgPreview), profile).map((candidate) => ({
|
|
420
|
+
id: `${candidate.assignment.adapter}:${candidate.assignment.model}`,
|
|
421
|
+
label: candidateRow(candidate, cfg.pricing),
|
|
422
|
+
pin: { via: candidate.assignment.adapter, model: candidate.assignment.model },
|
|
423
|
+
}));
|
|
424
|
+
return { rows, excludedNote: excludedNoteFor(deny) };
|
|
425
|
+
};
|
|
284
426
|
const preferUniverse = [
|
|
285
427
|
...new Set(channels.flatMap((channel) => [
|
|
286
428
|
channel.adapter,
|
|
@@ -331,6 +473,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
331
473
|
const write = {
|
|
332
474
|
initial,
|
|
333
475
|
edited: staged,
|
|
476
|
+
universe,
|
|
334
477
|
...(modeChanged ? { mode: state.selectedMode } : {}),
|
|
335
478
|
...(judgeChanged && judgeSeat ? { judge: judgeSeat } : {}),
|
|
336
479
|
steering: { initial: initialSteering, edited: state.steering },
|
|
@@ -361,7 +504,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
361
504
|
initialMode: rm.mode.mode,
|
|
362
505
|
modeOptions: ROUTING_MODES.map((mode) => ({ id: mode, gloss: MODE_GLOSS[mode] })),
|
|
363
506
|
initialMap: editable.map,
|
|
364
|
-
modePreview: (mode, map) => [modeSpend(mode, map), ...floorPreview(mode)],
|
|
507
|
+
modePreview: (mode, map, deny) => [modeSpend(mode, map, deny), ...floorPreview(mode)],
|
|
365
508
|
shapeRows: routedShapeRows,
|
|
366
509
|
candidatesForShape,
|
|
367
510
|
preferOptionsForShape: (_shape, current) => [
|
|
@@ -384,7 +527,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
384
527
|
if (result.kind === "quit")
|
|
385
528
|
return QUIT;
|
|
386
529
|
if (result.kind === "refresh") {
|
|
387
|
-
return "fleet:
|
|
530
|
+
return "fleet: probe refresh requested — re-run `tickmarkr fleet --fresh` (doctor is the sensor; the editor itself never re-probes)";
|
|
388
531
|
}
|
|
389
532
|
if (result.kind === "no-changes")
|
|
390
533
|
return "fleet: no overlay changes (empty diff)";
|
|
@@ -396,11 +539,29 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
396
539
|
if (!write)
|
|
397
540
|
throw new Error("fleet write reached confirmation without a staged overlay mutation");
|
|
398
541
|
writeFleetOverlay(result.review.path, (prior) => renderFleetOverlayWrite(prior, write));
|
|
399
|
-
|
|
542
|
+
// OBS-529: a freshly classified model has no probe verdict (doctor probes CONFIGURED models,
|
|
543
|
+
// and it was not configured at probe time), so it stays unroutable — invisible in every
|
|
544
|
+
// picker — until the next probe. Name the step, or the classify flow reads as broken.
|
|
545
|
+
const unprobed = [];
|
|
546
|
+
for (const [adapter, models] of Object.entries(write.edited.tiers)) {
|
|
547
|
+
for (const [model, assigned] of Object.entries(models)) {
|
|
548
|
+
if (assigned === null || assigned === undefined)
|
|
549
|
+
continue;
|
|
550
|
+
if (JSON.stringify(write.initial.tiers[adapter]?.[model]) === JSON.stringify(assigned))
|
|
551
|
+
continue;
|
|
552
|
+
if (health[adapter]?.modelAuth?.[model] === undefined)
|
|
553
|
+
unprobed.push(`${adapter}:${model}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const head = `fleet: wrote ${result.review.path}`;
|
|
557
|
+
if (!unprobed.length)
|
|
558
|
+
return head;
|
|
559
|
+
const named = unprobed.slice(0, 3).join(", ") + (unprobed.length > 3 ? `, +${unprobed.length - 3} more` : "");
|
|
560
|
+
return `${head}\nfleet: ${unprobed.length} newly classified model(s) have no probe verdict yet (${named}) — run \`tickmarkr fleet --fresh\` to probe them; unverified models stay unroutable`;
|
|
400
561
|
};
|
|
401
562
|
return {
|
|
402
563
|
props,
|
|
403
564
|
commit,
|
|
404
|
-
renderWhy: () => renderFleetWhy(projectedShapeRows(rm.mode.mode, editable.map)),
|
|
565
|
+
renderWhy: () => renderFleetWhy(projectedShapeRows(rm.mode.mode, editable.map, { adapters: editable.denyAdapters, models: editable.denyModels })),
|
|
405
566
|
};
|
|
406
567
|
}
|
|
@@ -275,9 +275,10 @@ async function installAgentFiles(cwd, force, docs, notes) {
|
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
277
|
// The consolidated wizard (operator directive 2026-08-12): init walks three acts in one
|
|
278
|
-
// command — preferences (Ink form) → discovery (doctor, the sensor) → fleet (
|
|
279
|
-
//
|
|
280
|
-
//
|
|
278
|
+
// command — preferences (Ink form) → discovery (doctor, the sensor) → fleet (the browser opens
|
|
279
|
+
// on the models view; the presets overlay raises on the first Shapes entry). The readline asks
|
|
280
|
+
// this replaced live on in git history; the Ink act keeps the same four questions and the same
|
|
281
|
+
// defaults, so `--yes` and non-TTY paths are unchanged.
|
|
281
282
|
async function runInitWizard(cwd, input, output) {
|
|
282
283
|
// Dynamic import, deliberately (same seam as fleet.ts): the Ink/React runtime must stay out of
|
|
283
284
|
// the non-TTY and --yes paths — init runs in CI and agent shells where importing Ink at module
|
|
@@ -395,7 +396,8 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
|
|
|
395
396
|
: `using probe results from ${formatDoctorAgeForInit(ageMs)} ago — run tickmarkr doctor to refresh (or init --fresh)\n${formatDoctorReport(cwd, loadConfig(cwd), health, allAdapters(), { wrote: false })}`
|
|
396
397
|
: await doctor([], cwd, undefined, { banner: false, compact: interactive });
|
|
397
398
|
// Act 3 — fleet. The compact discovery surface prints BETWEEN the acts so the operator reads
|
|
398
|
-
// what the
|
|
399
|
+
// what the fleet browser (and its presets overlay, raised on the first Shapes entry) ranks
|
|
400
|
+
// with; the final summary then carries a pointer, not a repeat.
|
|
399
401
|
if (interactive) {
|
|
400
402
|
output.write(`${doc}\n`);
|
|
401
403
|
doc = "discovery shown above — full matrix any time with `tickmarkr doctor`";
|