tickmarkr 2.6.3 → 2.6.5

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.
Files changed (79) hide show
  1. package/README.md +10 -2
  2. package/dist/adapters/catalog-remote.d.ts +23 -0
  3. package/dist/adapters/catalog-remote.js +38 -0
  4. package/dist/adapters/claude-code.d.ts +1 -1
  5. package/dist/adapters/claude-code.js +4 -1
  6. package/dist/adapters/codex-commit-check.d.ts +25 -0
  7. package/dist/adapters/codex-commit-check.js +108 -0
  8. package/dist/adapters/codex.d.ts +2 -1
  9. package/dist/adapters/codex.js +26 -5
  10. package/dist/adapters/model-discovery-check.d.ts +33 -0
  11. package/dist/adapters/model-discovery-check.js +123 -0
  12. package/dist/adapters/model-lints.d.ts +2 -1
  13. package/dist/adapters/model-lints.js +52 -1
  14. package/dist/adapters/model-windows.d.ts +2 -1
  15. package/dist/adapters/model-windows.js +18 -7
  16. package/dist/adapters/prompt.js +3 -2
  17. package/dist/adapters/types.d.ts +13 -0
  18. package/dist/cli/commands/approve.js +37 -6
  19. package/dist/cli/commands/doctor.d.ts +4 -0
  20. package/dist/cli/commands/doctor.js +20 -3
  21. package/dist/cli/commands/fleet.d.ts +10 -1
  22. package/dist/cli/commands/fleet.js +279 -95
  23. package/dist/cli/commands/init.js +10 -4
  24. package/dist/cli/commands/plan.js +65 -4
  25. package/dist/cli/commands/report.d.ts +39 -2
  26. package/dist/cli/commands/report.js +227 -10
  27. package/dist/cli/commands/resume.js +2 -4
  28. package/dist/cli/commands/run.d.ts +11 -0
  29. package/dist/cli/commands/run.js +33 -5
  30. package/dist/cli/commands/status.js +30 -3
  31. package/dist/cli/commands/verify.d.ts +1 -1
  32. package/dist/cli/commands/verify.js +356 -123
  33. package/dist/compile/native.js +130 -0
  34. package/dist/config/config.d.ts +25 -7
  35. package/dist/config/config.js +92 -11
  36. package/dist/config/fleet-overlay.d.ts +2 -1
  37. package/dist/config/fleet-overlay.js +77 -4
  38. package/dist/drivers/orca.d.ts +19 -1
  39. package/dist/drivers/orca.js +161 -31
  40. package/dist/gates/cache.d.ts +4 -0
  41. package/dist/gates/cache.js +10 -2
  42. package/dist/gates/llm.d.ts +4 -1
  43. package/dist/gates/llm.js +6 -1
  44. package/dist/gates/review.js +8 -3
  45. package/dist/gates/run-gates.d.ts +3 -2
  46. package/dist/gates/run-gates.js +91 -50
  47. package/dist/route/router.d.ts +11 -0
  48. package/dist/route/router.js +29 -8
  49. package/dist/run/daemon.js +158 -22
  50. package/dist/run/git.js +9 -0
  51. package/dist/run/journal.d.ts +101 -1
  52. package/dist/run/journal.js +352 -2
  53. package/dist/run/launchservices-check.d.ts +56 -0
  54. package/dist/run/launchservices-check.js +117 -0
  55. package/dist/run/operator-state.d.ts +22 -2
  56. package/dist/run/operator-state.js +20 -3
  57. package/dist/run/title-environment.d.ts +45 -0
  58. package/dist/run/title-environment.js +127 -0
  59. package/dist/run/title-preload.cjs +38 -0
  60. package/dist/run/title-preload.d.cts +1 -0
  61. package/dist/tui/cockpit/board.js +12 -6
  62. package/dist/tui/cockpit/keys.d.ts +8 -0
  63. package/dist/tui/cockpit/keys.js +4 -1
  64. package/dist/tui/cockpit/layout.d.ts +2 -2
  65. package/dist/tui/cockpit/layout.js +2 -2
  66. package/dist/tui/cockpit/live-runtime.js +46 -20
  67. package/dist/tui/cockpit/live-store.d.ts +30 -1
  68. package/dist/tui/cockpit/live-store.js +134 -18
  69. package/dist/tui/cockpit/log-view.d.ts +28 -0
  70. package/dist/tui/cockpit/log-view.js +60 -0
  71. package/dist/tui/cockpit/run-view.js +4 -0
  72. package/dist/tui/cockpit/shell.d.ts +2 -1
  73. package/dist/tui/cockpit/shell.js +6 -4
  74. package/dist/tui/ink/fleet-app.d.ts +12 -6
  75. package/dist/tui/ink/fleet-app.js +17 -11
  76. package/package.json +1 -1
  77. package/skills/tickmarkr-auto/SKILL.md +33 -3
  78. package/skills/tickmarkr-loop/SKILL.md +55 -5
  79. package/skills/tickmarkr-overseer/SKILL.md +83 -8
@@ -9,7 +9,8 @@ const ModelWindowClaimSchema = z
9
9
  .strict();
10
10
  /**
11
11
  * Validate cited model-window claims before production code can consume them.
12
- * These values are vendor-published claims, not measurements made by tickmarkr.
12
+ * These values are vendor-published claims (a vendor page, or a vendor CLI's own model cache), not
13
+ * measurements made by tickmarkr.
13
14
  */
14
15
  export function loadModelWindowClaims(input) {
15
16
  const claims = z.array(ModelWindowClaimSchema).parse(input);
@@ -22,8 +23,15 @@ export function loadModelWindowClaims(input) {
22
23
  return Object.freeze(claims.map((claim) => Object.freeze(claim)));
23
24
  }
24
25
  const ANTHROPIC_SOURCE = "https://platform.claude.com/docs/en/about-claude/models/overview";
25
- const OPENAI_SOURCE = "https://developers.openai.com/api/docs/models";
26
- const OPENAI_GPT_55_SOURCE = "https://developers.openai.com/api/docs/models/gpt-5.5";
26
+ // v2.6.4 T3 (D-717): the Codex rows cite the installed CLI's own models_cache.json snapshot, not the API
27
+ // docs. That file has no public URL, so the source names it in its own scheme, snapshot instant included.
28
+ // Every listed entry there carried context_window=272000 — the budget the codex CLI launches with. The
29
+ // API docs' 1,050,000 (formerly cited here) describes the API surface; it is superseded for these CLI
30
+ // seeds, not called false. These windows are the CLI's self-report, not a tickmarkr measurement. Only the
31
+ // window comes from this snapshot: the codex tiers seeded beside it (config.ts) rest on the provider's
32
+ // descriptions and the retained workhorse tier policy, not on any measured benchmark.
33
+ const CODEX_CLI_CACHE_SOURCE = "codex-cli:models_cache.json#fetched_at=2026-09-29T16:09Z";
34
+ const CODEX_CLI_CACHE_READ_DATE = "2026-09-29";
27
35
  const CURSOR_SOURCE = "https://cursor.com/help/ai-features/max-mode";
28
36
  const ZAI_SOURCE = "https://z.ai/blog/glm-5.2";
29
37
  const XAI_SOURCE = "https://docs.x.ai/developers/models/grok-4.5";
@@ -37,10 +45,13 @@ const VENDORED_MODEL_WINDOW_CLAIMS = [
37
45
  { modelId: "opus", window: 1_000_000, source: ANTHROPIC_SOURCE, readDate: READ_DATE },
38
46
  { modelId: "sonnet", window: 1_000_000, source: ANTHROPIC_SOURCE, readDate: READ_DATE },
39
47
  { modelId: "haiku", window: 200_000, source: ANTHROPIC_SOURCE, readDate: READ_DATE },
40
- { modelId: "gpt-5.6-sol", window: 1_050_000, source: OPENAI_SOURCE, readDate: READ_DATE },
41
- { modelId: "gpt-5.5", window: 1_050_000, source: OPENAI_GPT_55_SOURCE, readDate: READ_DATE },
42
- { modelId: "gpt-5.6-terra", window: 1_050_000, source: OPENAI_SOURCE, readDate: READ_DATE },
43
- { modelId: "gpt-5.6-luna", window: 1_050_000, source: OPENAI_SOURCE, readDate: READ_DATE },
48
+ { modelId: "gpt-6-astra", window: 272_000, source: CODEX_CLI_CACHE_SOURCE, readDate: CODEX_CLI_CACHE_READ_DATE },
49
+ { modelId: "gpt-6-sol", window: 272_000, source: CODEX_CLI_CACHE_SOURCE, readDate: CODEX_CLI_CACHE_READ_DATE },
50
+ { modelId: "gpt-6-luna", window: 272_000, source: CODEX_CLI_CACHE_SOURCE, readDate: CODEX_CLI_CACHE_READ_DATE },
51
+ { modelId: "gpt-5.6-sol", window: 272_000, source: CODEX_CLI_CACHE_SOURCE, readDate: CODEX_CLI_CACHE_READ_DATE },
52
+ { modelId: "gpt-5.5", window: 272_000, source: CODEX_CLI_CACHE_SOURCE, readDate: CODEX_CLI_CACHE_READ_DATE },
53
+ { modelId: "gpt-5.6-terra", window: 272_000, source: CODEX_CLI_CACHE_SOURCE, readDate: CODEX_CLI_CACHE_READ_DATE },
54
+ { modelId: "gpt-5.6-luna", window: 272_000, source: CODEX_CLI_CACHE_SOURCE, readDate: CODEX_CLI_CACHE_READ_DATE },
44
55
  { modelId: "composer-2.5", window: 200_000, source: CURSOR_SOURCE, readDate: READ_DATE },
45
56
  { modelId: "composer-2.5-fast", window: 200_000, source: CURSOR_SOURCE, readDate: READ_DATE },
46
57
  { modelId: "claude-fable-5-1", window: 1_000_000, source: ANTHROPIC_SOURCE, readDate: OBS_871_READ_DATE },
@@ -130,8 +130,9 @@ export function parseWorkerResult(raw, nonce) {
130
130
  }
131
131
  // Signatures anchor distinctive CLI-error phrasing — never bare fragments or bare status-code
132
132
  // numbers (the QUOTA_RE Pitfall-3 lesson): these run only over no-trailer output, but a stalled
133
- // worker's harvested pane can still contain ordinary work text.
134
- const AUTH_RE = /not logged in|please (?:log ?in|sign in)|please run [^\n]{0,30}log ?in|authentication[ _](?:required|failed|error)|invalid (?:api key|credentials)|api key (?:is )?(?:not set|missing|invalid|required)|credentials? (?:have )?expired|401 unauthorized/i;
133
+ // worker's harvested pane can still contain ordinary work text. E1: the recorded refresh failure
134
+ // ("access token could not be refreshed") is auth-required — classified, never re-probed or relaunched.
135
+ const AUTH_RE = /not logged in|please (?:log ?in|sign in)|please run [^\n]{0,30}log ?in|authentication[ _](?:required|failed|error)|invalid (?:api key|credentials)|api key (?:is )?(?:not set|missing|invalid|required)|credentials? (?:have )?expired|401 unauthorized|access token could not be refreshed/i;
135
136
  const SETUP_RE = /command not found|not recognized as an internal or external command|spawn \S+ ENOENT|missing required config|workspace trust (?:required|not granted)/i;
136
137
  const OUTAGE_RE = /unable to reach the model provider|cannot reach the model provider|model provider.{0,40}(?:unavailable|unreachable)|service (?:is )?temporarily unavailable|upstream connect error|overloaded_error/i;
137
138
  const TIMEOUT_RE = /\bETIMEDOUT\b|request timed out|connection timed out|deadline exceeded|timed out waiting for/i;
@@ -36,6 +36,10 @@ export interface ModelAuth {
36
36
  probedAt: string;
37
37
  identity?: string;
38
38
  }
39
+ export interface ModelRetirement {
40
+ retiresAt: string;
41
+ successor?: string;
42
+ }
39
43
  export interface AuthHealth {
40
44
  installed: boolean;
41
45
  authed: boolean;
@@ -46,6 +50,14 @@ export interface AuthHealth {
46
50
  servable?: string[];
47
51
  modelAuth?: Record<string, ModelAuth>;
48
52
  modelIdentities?: Record<string, string>;
53
+ modelRetirements?: Record<string, ModelRetirement | null>;
54
+ codexCommit?: "protected" | "allowed" | "unknown";
55
+ launchServices?: LaunchServicesRecord;
56
+ }
57
+ export interface LaunchServicesRecord {
58
+ pid: number;
59
+ start: string;
60
+ advisory?: string;
49
61
  }
50
62
  export declare function modelAuthed(health: AuthHealth | undefined, model: string, allowUnverifiedModels?: boolean): boolean;
51
63
  export interface Invocation {
@@ -190,6 +202,7 @@ export interface WorkerAdapter {
190
202
  parse(output: string, nonce: string): WorkerResult;
191
203
  listModels?(): Promise<string[]>;
192
204
  listModelsFetchedAt?(): string | undefined;
205
+ listModelsRetirements?(): Record<string, ModelRetirement | null> | undefined;
193
206
  collectUsage?(cwd: string, sinceMs: number): TokenUsage | undefined;
194
207
  contextUsage?(session: SessionRef): ContextUsage | null;
195
208
  readSessionTranscript?(session: SessionRef): {
@@ -1,11 +1,13 @@
1
1
  import { graphDefinitionHash, loadGraph, saveGraph, taskDefinitionFingerprint } from "../../graph/graph.js";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { userInfo } from "node:os";
4
+ import { allAdapters } from "../../adapters/registry.js";
5
+ import { channelKey } from "../../adapters/types.js";
4
6
  import { loadConfig } from "../../config/config.js";
5
7
  import { integrationBranch } from "../../run/merge.js";
6
8
  import { separabilityErrors, surfaceErrors, taskBudgetErrors } from "../../compile/collateral.js";
7
9
  import { GATE_NAMES } from "../../graph/schema.js";
8
- import { applyScopeAmendments, bindingToken, engagementComparable, engagementReleased, failedGateBeforePark, parseBindingToken, physicalLine, ATTEMPT_CAP_RELEASE, GATE_SATISFIED_RELEASE, Journal, RECHECK_RELEASE, REVIEW_UPHELD_RELEASE } from "../../run/journal.js";
10
+ import { applyScopeAmendments, bindingToken, captureOwedCheck, engagementComparable, engagementReleased, failedGateBeforePark, parseBindingToken, physicalLine, ATTEMPT_CAP_RELEASE, GATE_SATISFIED_RELEASE, Journal, RECHECK_RELEASE, REVIEW_UPHELD_RELEASE } from "../../run/journal.js";
9
11
  export const APPROVAL_DISPOSITIONS = ["dispatch", "waive-gate", "re-dispatch", "fund-fixed-attempt", "fresh-budget"];
10
12
  /**
11
13
  * What each disposition's release actually buys — the clause every operator-facing sentence about
@@ -208,11 +210,17 @@ export async function approve(argv, cwd = process.cwd()) {
208
210
  if (namedGate !== undefined && namedGate !== park?.failedGate) {
209
211
  throw new Error(`refusing mismatched gate for ${taskId}: --gate ${namedGate} but park ${bindingToken(parkBinding)} failed ${park?.failedGate ?? "no gate"} — nothing appended`);
210
212
  }
211
- if ((park?.kind === "scope-request" && !decisions) || files !== undefined) {
213
+ // A scope-request park is released one of two ways: --files widens files[], or a nonempty --reason
214
+ // refuses the expansion and funds a plain worker within the current files[] — the reason rides into
215
+ // the next brief as a standing ruling (journaledFailureBrief). No reason, no release: the refusal is
216
+ // a ruling only when it says why. The cockpit menu still offers neither (it has no reason entry).
217
+ const scopeRefusal = park?.kind === "scope-request" && !decisions && files === undefined && Boolean(reason?.trim());
218
+ if ((park?.kind === "scope-request" && !decisions && !scopeRefusal) || files !== undefined) {
212
219
  if (park?.kind !== "scope-request")
213
220
  throw new Error("--files requires a scope-request park");
214
- if (!files?.length)
215
- throw new Error(`scope-request for ${taskId} requires --files <glob,…>`);
221
+ if (!files?.length) {
222
+ throw new Error(`scope-request for ${taskId} requires --files <glob,…> to widen files[], or \`tickmarkr approve ${runId} ${taskId} --reason <text>\` to refuse the expansion and continue within the current files[]`);
223
+ }
216
224
  if (decisions)
217
225
  throw new Error("--files cannot be combined with --waive, --uphold or --recheck");
218
226
  const graph = applyScopeAmendments(loadGraph(cwd), journal, false, engagementReleased(events));
@@ -296,6 +304,28 @@ export async function approve(argv, cwd = process.cwd()) {
296
304
  if (!gateFailPark || !failedGate) {
297
305
  throw new Error(`--waive applies to a gate-fail park; ${taskId}'s newest park is ${String(lastHuman?.data.kind ?? "none")} with failed gate ${failedGate ?? "none"} — refusing`);
298
306
  }
307
+ // C1: the waiver's owed check rides the SAME row — one append, captured synchronously so a live
308
+ // daemon's boundary never observes the decision without its obligation (or a second write).
309
+ // The seats a review discharge may resolve to are this repository's declared channels, recorded on
310
+ // the row so every later fold reads the same table whatever process or checkout it runs in.
311
+ let integrationRef = "unknown";
312
+ let declared;
313
+ let owedTask;
314
+ try {
315
+ const cfg = loadConfig(cwd);
316
+ integrationRef = integrationBranch(cfg, runId);
317
+ declared = allAdapters().flatMap((a) => a.channels(cfg)).map((c) => ({ key: channelKey(c), vendor: c.vendor }));
318
+ }
319
+ catch { /* an unreadable config binds no range and declares no seat: unknown */ }
320
+ try {
321
+ owedTask = loadGraph(cwd).tasks.find((t) => t.id === taskId);
322
+ }
323
+ catch { /* recorded as unknown criteria and scope */ }
324
+ const obligation = captureOwedCheck({
325
+ cwd, runId, taskId, gate: failedGate, events, acceptance: owedTask?.acceptance, files: owedTask?.files, declared,
326
+ cause: reason ?? `operator waived failed ${failedGate} gate`, // the row's own park field binds the park
327
+ taskRef: `${integrationRef}--${taskId}`, integrationRef,
328
+ });
299
329
  journal.append("task-approved", taskId, {
300
330
  by,
301
331
  ...(reason ? { reason } : {}),
@@ -304,8 +334,9 @@ export async function approve(argv, cwd = process.cwd()) {
304
334
  gate: failedGate,
305
335
  park: parkBinding,
306
336
  ...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
337
+ obligation,
307
338
  });
308
- return disposition(cwd, runId, "waive-gate", `waived failed gate ${failedGate} for ${taskId} in ${runId} — by ${by}`, serialization.contended);
339
+ return disposition(cwd, runId, "waive-gate", `waived failed gate ${failedGate} for ${taskId} in ${runId} — by ${by}; owes ${failedGate} check ${obligation.id}${obligation.known ? "" : " (debt unknown)"}`, serialization.contended);
309
340
  }
310
341
  if (gateFailPark) {
311
342
  const choices = [`--waive (disposition waive-gate)`, `--recheck (disposition re-dispatch)`];
@@ -333,7 +364,7 @@ export async function approve(argv, cwd = process.cwd()) {
333
364
  park: parkBinding,
334
365
  });
335
366
  const token = capPark ? "fresh-budget" : "dispatch";
336
- return disposition(cwd, runId, token, `approved ${taskId} in ${runId} — by ${by}`, serialization.contended);
367
+ return disposition(cwd, runId, token, `approved ${taskId} in ${runId} — by ${by}${scopeRefusal ? "; scope expansion refused, files[] unchanged" : ""}`, serialization.contended);
337
368
  }
338
369
  finally {
339
370
  serialization.release();
@@ -1,6 +1,7 @@
1
1
  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
+ import { type CodexSandbox } from "../../adapters/codex-commit-check.js";
4
5
  import { type CatalogFetcher, type CatalogReadResult } from "../../adapters/catalog-remote.js";
5
6
  import { type ShResult } from "../../run/git.js";
6
7
  import { type VitestListResult } from "../../gates/acceptance.js";
@@ -17,6 +18,7 @@ export declare function recentReviewDemotions(cwd: string, lastRuns?: number): R
17
18
  export type DoctorOpts = {
18
19
  banner?: boolean;
19
20
  kimiTurnProbe?: (cwd: string) => Promise<KimiDoctorTurnResult>;
21
+ codexSandbox?: CodexSandbox;
20
22
  resolveClaudeAliasIdentity?: (cwd: string, alias: ClaudeAlias) => string | undefined;
21
23
  catalog?: CatalogReadResult;
22
24
  catalogNow?: () => Date;
@@ -34,6 +36,8 @@ export type DoctorOpts = {
34
36
  resolveOrcaBinary?: (cwd: string) => string | undefined;
35
37
  /** Test seam for the runner-owned JSON listing used by the acceptance-oracle report row. */
36
38
  listTests?: (cwd: string) => Promise<VitestListResult>;
39
+ /** Clock for model-knowledge lints (retirement notices, knowledge age); absent = the wall clock. */
40
+ now?: () => Date;
37
41
  };
38
42
  type OrcaCapability = {
39
43
  verdict: "pass" | "fail" | "warn";
@@ -14,11 +14,14 @@ import { loadConfig, overlayPreferShapes } from "../../config/config.js";
14
14
  import { HerdrDriver } from "../../drivers/herdr.js";
15
15
  import { ORCA_FIXTURE_VERSION, parseEnvelope, resolveOrcaCliBinary } from "../../drivers/orca.js";
16
16
  import { kimi, probeKimiDoctorTurn } from "../../adapters/kimi.js";
17
+ import { recordCodexCommit } from "../../adapters/codex-commit-check.js";
17
18
  import { denyPreferCollisionLine, denyPreferCollisions, disallowedBy, excludedChannels, exclusionLine, observedSeat, preferRanks } from "../../route/preference.js";
18
19
  import { CATALOG_REFRESH_TIMEOUT_MS, LIVEBENCH_TABLE_DATE, formatCatalogRefreshLegs, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
19
20
  import { sh } from "../../run/git.js";
20
21
  import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
21
22
  import { readReviewNoVerdictHistory, reviewNoVerdictRows } from "../../run/journal.js";
23
+ import { checkModelDiscovery } from "../../adapters/model-discovery-check.js";
24
+ import { recordLaunchServices } from "../../run/launchservices-check.js";
22
25
  /** Where a newer `table_<date>.csv` is discovered — the deployed site builds filenames by
23
26
  * concatenation and publishes no index, so the release listing is the only enumerable surface. */
24
27
  export const LIVEBENCH_RELEASES_URL = "https://api.github.com/repos/LiveBench/livebench.github.io/contents/public";
@@ -578,6 +581,7 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
578
581
  };
579
582
  }
580
583
  }
584
+ await recordCodexCommit(adapters, health, cwd, opts.codexSandbox);
581
585
  // MODEL-02: detect models where the adapter exposes a list surface, BEFORE writing doctor.json (write once, below).
582
586
  // Fail OPEN — the inverse of gates' fail-closed: detection is advisory, so a broken list surface NEVER fails doctor.
583
587
  for (const a of adapters) {
@@ -590,6 +594,11 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
590
594
  // no fetch timestamp, so it still stamps now — its staleness lint is best-effort until it surfaces one.
591
595
  if (health[a.id].models.length)
592
596
  health[a.id].modelsDetectedAt = a.listModelsFetchedAt?.() ?? new Date().toISOString();
597
+ // v2.6.4 T2 (B1a): the CLI's retirement notices ride into doctor.json beside the list they describe;
598
+ // an unreadable source leaves the field absent, which every reader treats as unknown.
599
+ const retirements = a.listModelsRetirements?.();
600
+ if (retirements)
601
+ health[a.id].modelRetirements = retirements;
593
602
  }
594
603
  catch { /* fail open: leave models as-is, doctor stays healthy */ }
595
604
  }
@@ -654,7 +663,11 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
654
663
  }
655
664
  }
656
665
  }
657
- writeDoctor(cwd, health);
666
+ const discovery = await checkModelDiscovery({ cwd, cfg, adapters, health, catalog, resolved: resolvedIdentitySnapshot, mayRefresh: !opts.catalog && !catalogRefreshLine && catalogRefreshAllowed, fetcher: opts.catalogFetcher, now: opts.catalogNow });
667
+ catalog = discovery.catalog;
668
+ aliasDriftFindings.push(...discovery.findings);
669
+ const launchServices = await recordLaunchServices(readDoctor(cwd), health);
670
+ writeDoctor(cwd, launchServices.health);
658
671
  const rows = adapters.map((a) => {
659
672
  const h = health[a.id];
660
673
  // OBS-503: a crash-on-version verdict carries its reason in note — print it instead of the lie.
@@ -784,7 +797,7 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
784
797
  const liveBenchStale = liveBenchStalenessFinding(opts.catalogNow?.() ?? new Date());
785
798
  if (liveBenchStale)
786
799
  lintRows.push(attentionRow(liveBenchStale));
787
- lintRows.push(...modelLints(cfg, health, adapters, { tty: ttyVisual(), stateDir: stateDirName(cwd), overlayPreferShapes: overlayPreferShapes(cwd) }).map(attentionRow));
800
+ lintRows.push(...modelLints(cfg, health, adapters, { tty: ttyVisual(), stateDir: stateDirName(cwd), overlayPreferShapes: overlayPreferShapes(cwd), now: opts.now?.() }).map(attentionRow));
788
801
  const excluded = excludedChannels(cfg, adapters, health);
789
802
  if (excluded.length)
790
803
  lintRows.push(attentionRow(exclusionLine(excluded)));
@@ -802,6 +815,8 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
802
815
  lintRows.push(...flagDriftWarnings(adapters, health).map(attentionRow));
803
816
  // OBS-145: resolved-identity drift is advisory display warning.
804
817
  lintRows.push(...aliasDriftFindings.map(attentionRow));
818
+ if (launchServices.line)
819
+ lintRows.push(attentionRow(launchServices.line));
805
820
  const resolvedCatalogModel = (adapter, model) => {
806
821
  if (adapter !== "claude-code" || !(model in CLAUDE_ALIAS_IDENTITY_STAMPS))
807
822
  return undefined;
@@ -890,9 +905,11 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
890
905
  rows.push(` ${dim(`catalog · ${advisory.display}`)}`);
891
906
  if (!listAllModels && bulk.length) {
892
907
  const uncovered = bulk.filter((adv) => adv.coverage === "uncovered").length;
893
- const evidenceOnly = bulk.length - uncovered;
908
+ const unknown = bulk.filter((adv) => adv.coverage === "unknown").length;
909
+ const evidenceOnly = bulk.length - uncovered - unknown;
894
910
  const parts = [
895
911
  ...(uncovered ? [`${uncovered} uncovered by ${catalog.source === "cache" ? "cached catalogs" : "vendored catalog"}`] : []),
912
+ ...(unknown ? [`${unknown} coverage unknown after a failed refresh`] : []),
896
913
  ...(evidenceOnly ? [`${evidenceOnly} covered without a tier suggestion`] : []),
897
914
  ];
898
915
  rows.push(` ${dim(`catalog · ${parts.join(" · ")} — tickmarkr doctor --models lists each`)}`);
@@ -1,6 +1,7 @@
1
1
  import { type CatalogFetcher, type CatalogReadResult } from "../../adapters/catalog-remote.js";
2
2
  import type { WorkerAdapter } from "../../adapters/types.js";
3
- import type { FleetEditorResult } from "../../tui/ink/fleet-app.js";
3
+ import { type MapEntry, type ModeResolution, type RoutingMode, type TickmarkrConfig } from "../../config/config.js";
4
+ import type { FleetEditorResult, FleetStagedDeny, FleetStagedMetadata } from "../../tui/ink/fleet-app.js";
4
5
  type FleetEditorProps = Parameters<typeof import("../../tui/ink/fleet-app.js").runFleetInkEditor>[0];
5
6
  export type FleetInput = NodeJS.ReadableStream & {
6
7
  isTTY?: boolean;
@@ -44,6 +45,14 @@ export declare function assembleFleetEditor(cwd: string, adapters: WorkerAdapter
44
45
  props: FleetEditorProps;
45
46
  commit: (result: FleetEditorResult) => string;
46
47
  renderWhy: () => string;
48
+ previewConfig: (mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny, stage?: FleetStagedMetadata) => {
49
+ ok: true;
50
+ cfg: TickmarkrConfig;
51
+ mode: ModeResolution;
52
+ } | {
53
+ ok: false;
54
+ error: string;
55
+ };
47
56
  } | {
48
57
  unavailable: string;
49
58
  }>;