tickmarkr 1.85.0 → 1.86.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.
Files changed (71) hide show
  1. package/README.md +4 -2
  2. package/dist/adapters/catalog-remote.d.ts +64 -0
  3. package/dist/adapters/catalog-remote.js +287 -0
  4. package/dist/adapters/catalog.d.ts +96 -0
  5. package/dist/adapters/catalog.js +176 -0
  6. package/dist/adapters/claude-code.js +5 -3
  7. package/dist/adapters/fake.js +33 -4
  8. package/dist/adapters/model-lints.d.ts +25 -5
  9. package/dist/adapters/model-lints.js +184 -50
  10. package/dist/adapters/model-windows.d.ts +31 -0
  11. package/dist/adapters/model-windows.js +69 -0
  12. package/dist/adapters/prompt.d.ts +5 -1
  13. package/dist/adapters/prompt.js +13 -4
  14. package/dist/adapters/registry.d.ts +25 -26
  15. package/dist/adapters/registry.js +173 -110
  16. package/dist/adapters/types.js +15 -3
  17. package/dist/brand.d.ts +5 -1
  18. package/dist/brand.js +18 -2
  19. package/dist/cli/commands/doctor.d.ts +3 -0
  20. package/dist/cli/commands/doctor.js +43 -21
  21. package/dist/cli/commands/fleet.d.ts +7 -0
  22. package/dist/cli/commands/fleet.js +94 -74
  23. package/dist/cli/commands/init.js +118 -5
  24. package/dist/cli/commands/status.js +44 -18
  25. package/dist/compile/gsd.d.ts +2 -1
  26. package/dist/compile/gsd.js +68 -2
  27. package/dist/compile/native.d.ts +14 -0
  28. package/dist/compile/native.js +161 -12
  29. package/dist/config/config.d.ts +20 -5
  30. package/dist/config/config.js +96 -64
  31. package/dist/config/fleet-overlay.d.ts +25 -20
  32. package/dist/config/fleet-overlay.js +195 -77
  33. package/dist/config/fleet-why.d.ts +23 -0
  34. package/dist/config/fleet-why.js +42 -0
  35. package/dist/drivers/herdr.d.ts +1 -0
  36. package/dist/drivers/herdr.js +70 -19
  37. package/dist/gates/acceptance.js +7 -2
  38. package/dist/gates/llm.d.ts +0 -1
  39. package/dist/gates/llm.js +5 -30
  40. package/dist/gates/review.d.ts +2 -1
  41. package/dist/gates/review.js +9 -7
  42. package/dist/gates/verdict-cause.d.ts +4 -0
  43. package/dist/gates/verdict-cause.js +63 -0
  44. package/dist/graph/schema.d.ts +6 -0
  45. package/dist/graph/schema.js +8 -5
  46. package/dist/route/router.d.ts +0 -5
  47. package/dist/route/router.js +16 -20
  48. package/dist/run/consult.d.ts +6 -0
  49. package/dist/run/consult.js +35 -25
  50. package/dist/run/daemon.js +71 -15
  51. package/dist/run/journal.js +87 -7
  52. package/dist/tui/cockpit/capture.d.ts +12 -0
  53. package/dist/tui/cockpit/capture.js +37 -1
  54. package/dist/tui/cockpit/components.js +8 -8
  55. package/dist/tui/cockpit/theme.d.ts +32 -26
  56. package/dist/tui/cockpit/theme.js +11 -5
  57. package/dist/tui/ink/components.d.ts +0 -15
  58. package/dist/tui/ink/components.js +0 -17
  59. package/dist/tui/ink/fleet-app.d.ts +4 -1
  60. package/dist/tui/ink/fleet-app.js +134 -13
  61. package/fixtures/sample.native.md +1 -1
  62. package/package.json +1 -1
  63. package/skills/tickmarkr-overseer/SKILL.md +354 -34
  64. package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +70 -0
  65. package/skills/tickmarkr-overseer/scripts/watch-panes.sh +1 -1
  66. package/dist/tui/ink/studio-app.d.ts +0 -59
  67. package/dist/tui/ink/studio-app.js +0 -320
  68. package/dist/tui/save.d.ts +0 -38
  69. package/dist/tui/save.js +0 -96
  70. package/dist/tui/staging.d.ts +0 -29
  71. package/dist/tui/staging.js +0 -78
@@ -1,9 +1,9 @@
1
1
  import { type TickmarkrConfig } from "../config/config.js";
2
- import { type RoutingProfile } from "../route/profile.js";
2
+ import { type CatalogEntriesOptions, type CliEntry } from "./catalog.js";
3
3
  import { type AuthHealth, type BillingChannel, type WorkerAdapter } from "./types.js";
4
- export declare const CANDIDATE_CLI_CATALOG: readonly ["kimi", "gemini", "qwen", "aider", "goose", "amp", "droid", "auggie", "crush"];
5
- export declare const REGISTERED_ADAPTER_BINARIES: readonly ["claude", "codex", "cursor-agent", "opencode", "pi", "grok"];
6
- export declare function resolveShellBinary(bin: string, cwd?: string): {
4
+ export declare const CANDIDATE_CLI_CATALOG: readonly string[];
5
+ export declare function registeredAdapterBinaries(adapters?: WorkerAdapter[]): string[];
6
+ export declare function resolveShellBinary(bin: string, cwd?: string, pathEnv?: string): {
7
7
  resolved?: string;
8
8
  all: string[];
9
9
  };
@@ -27,40 +27,39 @@ export declare function modelAliasLine(excluded: {
27
27
  }[]): string;
28
28
  export type CandidateCliDetection = {
29
29
  binary: string;
30
- version: string | undefined;
30
+ path: string;
31
31
  };
32
- export declare function probeCandidateVersion(bin: string): string | undefined;
33
- export declare function detectCandidateClis(opts?: {
32
+ export declare function parseHerdrCliNames(help: string): string[];
33
+ export declare function herdrNominatedCliNames(cwd?: string, pathEnv?: string): string[];
34
+ export interface DetectCandidateCliOptions extends CatalogEntriesOptions {
35
+ cwd?: string;
34
36
  pathEnv?: string;
35
- }): CandidateCliDetection[];
36
- export declare function formatCandidateCliRow({ binary, version }: CandidateCliDetection): string;
37
+ entries?: readonly CliEntry[];
38
+ adapters?: WorkerAdapter[];
39
+ resolveBinary?: (binary: string) => {
40
+ resolved?: string;
41
+ all: string[];
42
+ };
43
+ }
44
+ export declare function detectCandidateClis(opts?: DetectCandidateCliOptions): CandidateCliDetection[];
45
+ export declare function formatCandidateCliRow({ binary, path }: CandidateCliDetection): string;
37
46
  export declare function probeHelpText(bin: string): string | undefined;
38
47
  export declare function missingDeclaredFlags(helpText: string, flags: string[]): string[];
39
48
  export declare function flagDriftWarnings(adapters: WorkerAdapter[], health: Record<string, AuthHealth>, helpOf?: (bin: string) => string | undefined): string[];
40
- export declare function allAdapters(opts?: {
49
+ export declare function adaptersFromCliEntries(entries: readonly CliEntry[]): WorkerAdapter[];
50
+ export interface AllAdaptersOptions {
41
51
  fakeScriptPath?: string;
42
- }): WorkerAdapter[];
52
+ cliEntries?: readonly CliEntry[];
53
+ operatorYaml?: string | null;
54
+ operatorPath?: string | false;
55
+ }
56
+ export declare function allAdapters(opts?: AllAdaptersOptions): WorkerAdapter[];
43
57
  export declare function getAdapter(id: string, adapters: WorkerAdapter[]): WorkerAdapter;
44
58
  export declare function probeAll(adapters: WorkerAdapter[], opts?: {
45
59
  cwd?: string;
46
60
  }): Promise<Record<string, AuthHealth>>;
47
61
  export type ProbeModelStatus = "ok" | "timeout" | "failed";
48
62
  export type ProbeModelProgress = (adapter: string, model: string, status: ProbeModelStatus, durationMs: number) => void;
49
- export interface AutoPreferDoc {
50
- derivedAt: string;
51
- [shape: string]: string[] | string;
52
- }
53
- export declare const pendingAutoPreferKey: unique symbol;
54
- export declare const DOCTOR_ROUTING_STALE_MS: number;
55
- export declare function deriveAutoPrefer(cfg: TickmarkrConfig, adapters: WorkerAdapter[], health: Record<string, AuthHealth>, profile?: RoutingProfile): AutoPreferDoc;
56
- export declare function readAutoPrefer(repoRoot: string): AutoPreferDoc | null;
57
- export declare function routingPreferContext(repoRoot: string, cfg: TickmarkrConfig, opts?: {
58
- globalDir?: string;
59
- }): {
60
- autoPrefer?: AutoPreferDoc;
61
- doctorFresh: boolean;
62
- overlayPreferShapes: ReadonlySet<string>;
63
- };
64
63
  export declare function probeModels(cfg: TickmarkrConfig, repoRoot: string, adapters: WorkerAdapter[], health: Record<string, AuthHealth>, onProgress?: ProbeModelProgress): Promise<void>;
65
64
  export declare function writeDoctor(repoRoot: string, health: Record<string, AuthHealth>): void;
66
65
  export declare function readDoctor(repoRoot: string): Record<string, AuthHealth> | null;
@@ -1,29 +1,33 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { existsSync, mkdtempSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdtempSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { overlayPreferShapes, TIER_RANK } from "../config/config.js";
6
5
  import { modelLints, suggestOverlay } from "./model-lints.js";
7
6
  import { HerdrDriver } from "../drivers/herdr.js";
8
7
  import { tickmarkrDir, stateDirName } from "../graph/graph.js";
9
8
  import { disallowedBy, excludedChannels, exclusionLine, preferRanks } from "../route/preference.js";
10
- import { learnedScore } from "../route/profile.js";
11
- import { marginalCostRank } from "../route/router.js";
12
9
  import { sh } from "../run/git.js";
13
- import { claudeCode } from "./claude-code.js";
14
- import { codex } from "./codex.js";
15
- import { cursorAgent } from "./cursor-agent.js";
16
10
  import { FakeAdapter } from "./fake.js";
17
- import { grok } from "./grok.js";
18
- import { kimi } from "./kimi.js";
19
- import { opencode } from "./opencode.js";
20
- import { pi } from "./pi.js";
11
+ import { parseWorkerResult } from "./prompt.js";
21
12
  import { sealHerdrEnv } from "../drivers/subprocess.js";
22
- import { channelKey, modelAuthed, QUOTA_RE, shq } from "./types.js";
23
- // Agent-CLI binary names with no tickmarkr adapter — doctor sweeps these advisory-only (v1.48 T1).
24
- export const CANDIDATE_CLI_CATALOG = ["kimi", "gemini", "qwen", "aider", "goose", "amp", "droid", "auggie", "crush"];
25
- // Binaries probed by registered adapters — kept beside the catalog so a test can forbid overlap.
26
- export const REGISTERED_ADAPTER_BINARIES = ["claude", "codex", "cursor-agent", "opencode", "pi", "grok"];
13
+ import { catalogEntries, isNativeCliDrive, projectCliEntries, SHIPPED_CLI_CATALOG, } from "./catalog.js";
14
+ import { channelKey, channelsFromConfig, modelAuthed, MODEL_ID_RE, QUOTA_RE, shq, } from "./types.js";
15
+ // Compatibility projection for callers/tests that only need shipped advisory names. The literal
16
+ // list is gone: both advisory and routable catalog views derive from SHIPPED_CLI_CATALOG. Native
17
+ // definitions (claudeCode, codex, cursorAgent, opencode, pi, grok, kimi) are owned by catalog.ts;
18
+ // this registry consumes their drive contracts instead of restating their registration order.
19
+ export const CANDIDATE_CLI_CATALOG = SHIPPED_CLI_CATALOG
20
+ .filter((entry) => entry.drive === undefined)
21
+ .map((entry) => entry.binary);
22
+ const SHIPPED_NATIVE_ADAPTERS = new Set(SHIPPED_CLI_CATALOG.flatMap((entry) => isNativeCliDrive(entry.drive) ? [entry.drive.adapter] : []));
23
+ const declarativeBinaries = new WeakMap();
24
+ // OBS-304: this was a hand-maintained mirror of the registered binaries, and the guard test that
25
+ // forbids catalog overlap checked against IT rather than against the adapters — so when kimi gained
26
+ // an adapter and nobody updated the copy, the guard kept passing on a collision it existed to catch.
27
+ // Derive it. A list that restates another list is the defect, not the entry that fell out of sync.
28
+ export function registeredAdapterBinaries(adapters = allAdapters()) {
29
+ return adapters.map((a) => a.hardcodedFlags?.binary ?? declarativeBinaries.get(a)).filter((b) => !!b);
30
+ }
27
31
  // OBS-117: worker panes run commands through `bash -lc` (SubprocessDriver + sh()), not bare spawnSync(bin).
28
32
  // Doctor must resolve adapter binaries the same way so a shadow install on PATH cannot disagree with dispatch.
29
33
  function shellSpawnSync(cmd, cwd, timeoutMs = 10000) {
@@ -36,9 +40,24 @@ function shellSpawnSync(cmd, cwd, timeoutMs = 10000) {
36
40
  });
37
41
  return { code: r.status ?? 1, stdout: (r.stdout || "").trim(), stderr: (r.stderr || "").trim() };
38
42
  }
39
- export function resolveShellBinary(bin, cwd = process.cwd()) {
40
- const r = shellSpawnSync(`type -a ${shq(bin)} 2>/dev/null | sed -n 's/^.* is \\(.*\\)$/\\1/p'`, cwd);
41
- const all = r.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
43
+ export function resolveShellBinary(bin, cwd = process.cwd(), pathEnv) {
44
+ // An explicit PATH is a deterministic fixture/operator boundary. The default remains the same
45
+ // login-shell environment used by dispatched workers (OBS-117).
46
+ const pathPrefix = pathEnv === undefined ? "" : `export PATH=${shq(pathEnv)}; `;
47
+ const r = shellSpawnSync(`${pathPrefix}type -a ${shq(bin)} 2>/dev/null | sed -n 's/^.* is \\(.*\\)$/\\1/p'`, cwd);
48
+ const hits = r.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
49
+ // OBS-304: a duplicated PATH entry makes `type -a` emit the SAME path twice, which doctor then
50
+ // reported as "2 installs on PATH — …/claude, …/claude". Count distinct real files, so the real
51
+ // shadows (codex: homebrew + nvm) stop being buried in noise. realpath resolves symlinked twins.
52
+ const seen = new Set();
53
+ const all = hits.filter((p) => {
54
+ let key = p;
55
+ try {
56
+ key = realpathSync(p);
57
+ }
58
+ catch { /* unreadable or dangling — fall back to the literal path */ }
59
+ return seen.has(key) ? false : (seen.add(key), true);
60
+ });
42
61
  return { resolved: all[0], all };
43
62
  }
44
63
  export function probeVersionShell(bin, cwd = process.cwd()) {
@@ -101,39 +120,47 @@ export function modelAliasLine(excluded) {
101
120
  const parts = excluded.map(({ key, adapter }) => `${key} (not in ${adapter}'s reported model list)`);
102
121
  return `model alias: ${excluded.length} channel(s) invalid — ${parts.join(", ")}`;
103
122
  }
104
- function candidateOnPath(bin, pathEnv) {
105
- const r = spawnSync("which", [bin], { encoding: "utf8", env: { ...process.env, PATH: pathEnv } });
106
- return r.status === 0 && r.stdout.trim().length > 0;
123
+ // Herdr's supported-kind help is nomination data only. Parse the closed token list and discard
124
+ // everything else before any name can reach a shell resolver.
125
+ export function parseHerdrCliNames(help) {
126
+ const body = /\[possible values:\s*([^\]]+)\]/m.exec(help)?.[1] ?? "";
127
+ return [...new Set(body.split(",").map((name) => name.trim()).filter((name) => /^[a-z0-9-]+$/.test(name)))];
107
128
  }
108
- // Fail open: a broken candidate binary never throws and never fails doctor.
109
- export function probeCandidateVersion(bin) {
110
- try {
111
- const r = spawnSync(bin, ["--version"], { encoding: "utf8", timeout: 10000 });
112
- if (r.error || r.status !== 0)
113
- return undefined;
114
- return (r.stdout || r.stderr).trim().split("\n")[0];
115
- }
116
- catch {
117
- return undefined;
118
- }
129
+ export function herdrNominatedCliNames(cwd = process.cwd(), pathEnv) {
130
+ // This asks a known driver binary for static help. It is deliberately NOT gated on
131
+ // HerdrDriver.available(), which reports HERDR_ENV rather than binary presence.
132
+ const { resolved } = resolveShellBinary("herdr", cwd, pathEnv);
133
+ if (!resolved)
134
+ return [];
135
+ const r = shellSpawnSync(`${shq(resolved)} agent start --help`, cwd);
136
+ return r.code === 0 ? parseHerdrCliNames(`${r.stdout}\n${r.stderr}`) : [];
119
137
  }
120
138
  export function detectCandidateClis(opts = {}) {
121
- const pathEnv = opts.pathEnv ?? process.env.PATH ?? "";
122
- const out = [];
123
- for (const bin of CANDIDATE_CLI_CATALOG) {
124
- if (!candidateOnPath(bin, pathEnv))
125
- continue;
126
- out.push({ binary: bin, version: probeCandidateVersion(bin) });
139
+ const cwd = opts.cwd ?? process.cwd();
140
+ const entries = opts.entries ?? catalogEntries({
141
+ shipped: opts.shipped,
142
+ operatorYaml: opts.operatorYaml,
143
+ operatorPath: opts.operatorPath,
144
+ herdrNames: opts.herdrNames ?? herdrNominatedCliNames(cwd, opts.pathEnv),
145
+ });
146
+ const registered = new Set(registeredAdapterBinaries(opts.adapters ?? allAdapters({ cliEntries: entries })));
147
+ const advisory = projectCliEntries(entries).advisory.filter((entry) => !registered.has(entry.binary));
148
+ const resolveBinary = opts.resolveBinary
149
+ ?? ((binary) => resolveShellBinary(binary, cwd, opts.pathEnv));
150
+ const discovered = [];
151
+ for (const entry of advisory) {
152
+ const { resolved } = resolveBinary(entry.binary);
153
+ if (resolved)
154
+ discovered.push({ binary: entry.binary, path: resolved });
127
155
  }
128
- return out;
156
+ return discovered;
129
157
  }
130
- export function formatCandidateCliRow({ binary, version }) {
131
- const ver = version ?? "version unknown";
132
- return ` ! ${binary.padEnd(14)} detected: ${ver} (no tickmarkr adapter — not routable)`;
158
+ export function formatCandidateCliRow({ binary, path }) {
159
+ return ` ! ${binary.padEnd(14)} detected at ${path} (no drive contract — not routable)`;
133
160
  }
134
- // v1.65 T3: same guarded spawn pattern as probeCandidateVersion — bounded, error/status-checked,
135
- // fail-open. undefined = binary unavailable/broken ⇒ no drift verdict (the existing auth reporting
136
- // already names an uninstalled CLI; drift must never pile on).
161
+ // v1.65 T3: bounded, error/status-checked and fail-open. Unlike candidate detection above, this
162
+ // executes only registered adapter binaries with drive contracts. undefined = unavailable/broken,
163
+ // so no drift verdict piles onto the existing auth report.
137
164
  export function probeHelpText(bin) {
138
165
  try {
139
166
  const r = spawnSync(bin, ["--help"], { encoding: "utf8", timeout: 10000 });
@@ -169,11 +196,103 @@ export function flagDriftWarnings(adapters, health, helpOf = probeHelpText) {
169
196
  }
170
197
  return out;
171
198
  }
199
+ function renderDriveTemplate(template, promptFile, model) {
200
+ return template.replaceAll("{promptFile}", shq(promptFile)).replaceAll("{model}", shq(model));
201
+ }
202
+ function valueAtPath(value, path) {
203
+ if (!path)
204
+ return value;
205
+ return path.split(".").reduce((cursor, part) => {
206
+ if (!cursor || typeof cursor !== "object")
207
+ return undefined;
208
+ return cursor[part];
209
+ }, value);
210
+ }
211
+ function parseDeclaredModels(raw, parser, path) {
212
+ let values;
213
+ if (parser === "json") {
214
+ try {
215
+ const selected = valueAtPath(JSON.parse(raw), path);
216
+ values = Array.isArray(selected) ? selected : [];
217
+ }
218
+ catch {
219
+ return [];
220
+ }
221
+ }
222
+ else if (parser === "pi-table") {
223
+ const lines = raw.trim().split("\n");
224
+ const header = lines.findIndex((line) => /^provider\s+model\b/i.test(line.trim()));
225
+ values = header === -1 ? [] : lines.slice(header + 1).map((line) => {
226
+ const [provider, model] = line.trim().split(/\s+/);
227
+ return provider && model ? `${provider}/${model}` : "";
228
+ });
229
+ }
230
+ else {
231
+ values = raw.split("\n").map((line) => line.trim());
232
+ }
233
+ return [...new Set(values.filter((value) => typeof value === "string" && MODEL_ID_RE.test(value)))];
234
+ }
235
+ function declarativeAdapter(entry) {
236
+ const drive = entry.drive;
237
+ const adapter = {
238
+ id: entry.id,
239
+ vendor: entry.vendor,
240
+ probe: async () => {
241
+ const health = probeVersionShell(entry.binary);
242
+ if (!health.installed)
243
+ return health;
244
+ const version = health.version ?? "";
245
+ if (!new RegExp(entry.identity).test(version)) {
246
+ return {
247
+ installed: true,
248
+ authed: false,
249
+ version,
250
+ models: [],
251
+ note: `identity mismatch for ${entry.binary}`,
252
+ binaryPaths: health.binaryPaths,
253
+ };
254
+ }
255
+ return health;
256
+ },
257
+ channels: (cfg) => channelsFromConfig(entry.id, cfg),
258
+ headlessCommand: (promptFile, model) => renderDriveTemplate(drive.headless, promptFile, model),
259
+ interactiveCommand: (promptFile, model) => drive.interactive === null
260
+ ? null
261
+ : renderDriveTemplate(drive.interactive, promptFile, model),
262
+ invoke(_task, _cwd, assignment, ctx) {
263
+ return { command: this.headlessCommand(ctx.promptFile, assignment.model) };
264
+ },
265
+ parse: parseWorkerResult,
266
+ ...(drive.listModels ? {
267
+ listModels: async () => {
268
+ const { resolved } = resolveShellBinary(entry.binary);
269
+ if (!resolved)
270
+ return [];
271
+ const cmd = [shq(resolved), ...drive.listModels.argv.map(shq)].join(" ");
272
+ const result = shellSpawnSync(cmd, process.cwd(), 15000);
273
+ return result.code === 0
274
+ ? parseDeclaredModels(result.stdout, drive.listModels.parser, drive.listModels.path)
275
+ : [];
276
+ },
277
+ } : {}),
278
+ };
279
+ declarativeBinaries.set(adapter, entry.binary);
280
+ return adapter;
281
+ }
282
+ export function adaptersFromCliEntries(entries) {
283
+ return projectCliEntries(entries).routable.map((entry) => {
284
+ if (isNativeCliDrive(entry.drive))
285
+ return entry.drive.adapter;
286
+ return declarativeAdapter(entry);
287
+ });
288
+ }
172
289
  export function allAdapters(opts = {}) {
173
- // pi + grok + kimi appended LAST: same-tier ties resolve by discovery order (Phase 6 D2), so
174
- // appending keeps the Phase 6 shape→channel matrix byte-identical; inserting anywhere else
175
- // silently reassigns shapes on same-tier ties. kimi is appended AFTER grok for the same reason.
176
- const real = [claudeCode, codex, cursorAgent, opencode, pi, grok, kimi];
290
+ const entries = opts.cliEntries ?? catalogEntries({
291
+ operatorYaml: opts.operatorYaml,
292
+ operatorPath: opts.operatorPath,
293
+ herdrNames: [],
294
+ });
295
+ const real = adaptersFromCliEntries(entries);
177
296
  const fakePath = opts.fakeScriptPath ?? process.env.TICKMARKR_FAKE_SCRIPT;
178
297
  return fakePath ? [new FakeAdapter(fakePath), ...real] : real;
179
298
  }
@@ -233,58 +352,6 @@ function probeFailure(code, stdout, stderr, timedOut, timeoutMs = MODEL_PROBE_TI
233
352
  ? reasonTail(output) || `probe exited ${code}`
234
353
  : undefined;
235
354
  }
236
- export const pendingAutoPreferKey = Symbol.for("tickmarkr.pendingAutoPrefer");
237
- const median = (xs) => {
238
- const s = [...xs].sort((a, b) => a - b);
239
- const m = Math.floor(s.length / 2);
240
- return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
241
- };
242
- // ponytail: 24h TTL matches plan.ts staleness — promote to config when an operator asks.
243
- export const DOCTOR_ROUTING_STALE_MS = 24 * 60 * 60 * 1000;
244
- export function deriveAutoPrefer(cfg, adapters, health, profile) {
245
- const derivedAt = new Date().toISOString();
246
- const out = { derivedAt };
247
- // v1.52 T5: routing.floors is the only band authority — a map entry can no longer carry a tier.
248
- for (const shape of Object.keys(cfg.routing.map)) {
249
- const minTier = cfg.routing.floors[shape] ?? "cheap";
250
- const ranked = [];
251
- for (const a of adapters) {
252
- const h = health[a.id];
253
- if (!h?.installed || !h?.authed || typeof a.channels !== "function")
254
- continue;
255
- const qualifying = a.channels(cfg).filter((c) => TIER_RANK[c.tier] >= TIER_RANK[minTier] && h.modelAuth?.[c.model]?.authed === true);
256
- if (!qualifying.length)
257
- continue;
258
- const durations = qualifying
259
- .map((c) => h.modelAuth?.[c.model]?.durationMs)
260
- .filter((d) => typeof d === "number");
261
- ranked.push({
262
- adapter: a.id,
263
- mc: Math.min(...qualifying.map(marginalCostRank)),
264
- dur: durations.length ? median(durations) : Number.POSITIVE_INFINITY,
265
- learned: profile ? Math.max(...qualifying.map((c) => learnedScore(profile, shape, channelKey(c), c.channel))) : 0,
266
- });
267
- }
268
- ranked.sort((a, b) => a.mc - b.mc || a.dur - b.dur || b.learned - a.learned);
269
- out[shape] = ranked.map((r) => r.adapter);
270
- }
271
- return out;
272
- }
273
- export function readAutoPrefer(repoRoot) {
274
- const raw = readDoctorFile(repoRoot);
275
- if (!raw || typeof raw.autoPrefer !== "object" || raw.autoPrefer === null)
276
- return null;
277
- return raw.autoPrefer;
278
- }
279
- export function routingPreferContext(repoRoot, cfg, opts = {}) {
280
- const age = doctorAgeMs(repoRoot);
281
- const doctorFresh = age !== null && age <= DOCTOR_ROUTING_STALE_MS;
282
- return {
283
- autoPrefer: doctorFresh ? readAutoPrefer(repoRoot) ?? undefined : undefined,
284
- doctorFresh,
285
- overlayPreferShapes: overlayPreferShapes(repoRoot, opts),
286
- };
287
- }
288
355
  const probeModelStatus = (v) => v.authed ? "ok" : v.reason?.includes("timed out") ? "timeout" : "failed";
289
356
  // v1.21: one bounded, headless call per configured model; detected-but-unclassified models never enter this loop.
290
357
  export async function probeModels(cfg, repoRoot, adapters, health, onProgress) {
@@ -296,7 +363,7 @@ export async function probeModels(cfg, repoRoot, adapters, health, onProgress) {
296
363
  if (!h?.installed)
297
364
  return;
298
365
  // Tests inject FakeAdapter; never let an incidental default adapter spend a real token in the suite.
299
- if (process.env.VITEST && [claudeCode, codex, cursorAgent, opencode, pi, grok, kimi].includes(a))
366
+ if (process.env.VITEST && SHIPPED_NATIVE_ADAPTERS.has(a))
300
367
  return;
301
368
  const verdicts = {};
302
369
  const priorModelAuth = priorHealth?.[a.id]?.modelAuth;
@@ -360,7 +427,6 @@ export async function probeModels(cfg, repoRoot, adapters, health, onProgress) {
360
427
  }
361
428
  h.modelAuth = verdicts;
362
429
  }));
363
- health[pendingAutoPreferKey] = deriveAutoPrefer(cfg, adapters, health);
364
430
  }
365
431
  // T2: caps concurrent probes per adapter at 2 — v1.33.5 regression, 4 concurrent codex exec in one
366
432
  // repo made ALL 4 time out where sequential passed 2/4 (suspected CLI self-contention).
@@ -388,20 +454,17 @@ function readDoctorFile(repoRoot) {
388
454
  }
389
455
  export function writeDoctor(repoRoot, health) {
390
456
  tickmarkrDir(repoRoot);
391
- const pending = health[pendingAutoPreferKey];
392
- const payload = { ...health };
393
- delete payload[pendingAutoPreferKey];
394
- if (pending)
395
- payload.autoPrefer = pending;
396
457
  const path = doctorPath(repoRoot);
397
458
  const tmp = `${path}.tmp`;
398
- writeFileSync(tmp, JSON.stringify(payload, null, 2) + "\n");
459
+ writeFileSync(tmp, JSON.stringify(health, null, 2) + "\n");
399
460
  renameSync(tmp, path);
400
461
  }
401
462
  export function readDoctor(repoRoot) {
402
463
  const raw = readDoctorFile(repoRoot);
403
464
  if (!raw)
404
465
  return null;
466
+ // pre-v1.86 caches may still carry an `autoPrefer` key — strip it; the mechanism is deleted and
467
+ // the key is never read back into health.
405
468
  const { autoPrefer: _, ...health } = raw;
406
469
  return health;
407
470
  }
@@ -70,9 +70,21 @@ export function channelsFromConfig(adapterId, cfg) {
70
70
  const e = cfg.tiers[adapterId];
71
71
  if (!e)
72
72
  return [];
73
- return Object.entries(e.models).map(([model, tier]) => ({
74
- adapter: adapterId, vendor: e.vendor, model, channel: e.channel, tier,
75
- }));
73
+ return Object.entries(e.models).flatMap(([model, tier]) => {
74
+ const override = e.modelOverrides?.[model];
75
+ const vendor = override?.vendor ?? e.vendor;
76
+ // Parsed configs guarantee this only occurs for invalid data, but direct callers must fail closed
77
+ // too: an undeclared vendor is not review diversity and provider prefixes are never provenance.
78
+ if (!vendor?.trim())
79
+ return [];
80
+ return [{
81
+ adapter: adapterId,
82
+ vendor,
83
+ model,
84
+ channel: override?.channel ?? e.channel,
85
+ tier,
86
+ }];
87
+ });
76
88
  }
77
89
  export function channelKey(c) {
78
90
  return `${c.adapter}:${c.model}`;
package/dist/brand.d.ts CHANGED
@@ -16,7 +16,11 @@ export declare function bannerShell(): string;
16
16
  export declare const TICKMARKR_EXIT_TRAILER = "printf '\\nTICKMARKR_''EXIT:%s\\n' $?";
17
17
  /** OBS-50: visible-pane bootstrap script — banner + agent command + byte-identical exit trailer. */
18
18
  export declare function paneDispatchScript(body: string[]): string;
19
- /** OBS-50: one short herdr pane-run line; bootstrap lives in the script file beside the prompt. */
19
+ /** Record that the command being passed directly to ExecutorDriver.run is a daemon-built launch. */
20
+ export declare function paneLaunchCommand(command: string): string;
21
+ /** Driver-side half of paneLaunchCommand's one-shot handoff. */
22
+ export declare function consumePaneLaunchIntent(): boolean;
23
+ /** OBS-50/342: one short, intent-bearing pane-run line; bootstrap lives beside the prompt. */
20
24
  export declare function paneDispatchCommand(scriptPath: string): string;
21
25
  /** The settled brand green ramp (256-color), bright → deep. */
22
26
  export declare const BRAND_RAMP: readonly [84, 78, 41, 35];
package/dist/brand.js CHANGED
@@ -78,9 +78,25 @@ export const TICKMARKR_EXIT_TRAILER = `printf '\\nTICKMARKR_''EXIT:%s\\n' $?`;
78
78
  export function paneDispatchScript(body) {
79
79
  return ["export BASH_SILENCE_DEPRECATION_WARNING=1", ...body, TICKMARKR_EXIT_TRAILER].join("\n");
80
80
  }
81
- /** OBS-50: one short herdr pane-run line; bootstrap lives in the script file beside the prompt. */
81
+ const pendingPaneLaunchIntents = [];
82
+ /** Record that the command being passed directly to ExecutorDriver.run is a daemon-built launch. */
83
+ export function paneLaunchCommand(command) {
84
+ const intent = { kind: "launch" };
85
+ pendingPaneLaunchIntents.push(intent);
86
+ queueMicrotask(() => {
87
+ const index = pendingPaneLaunchIntents.indexOf(intent);
88
+ if (index >= 0)
89
+ pendingPaneLaunchIntents.splice(index, 1);
90
+ });
91
+ return command;
92
+ }
93
+ /** Driver-side half of paneLaunchCommand's one-shot handoff. */
94
+ export function consumePaneLaunchIntent() {
95
+ return pendingPaneLaunchIntents.shift()?.kind === "launch";
96
+ }
97
+ /** OBS-50/342: one short, intent-bearing pane-run line; bootstrap lives beside the prompt. */
82
98
  export function paneDispatchCommand(scriptPath) {
83
- return `bash ${shq(scriptPath)}`;
99
+ return paneLaunchCommand(`bash ${shq(scriptPath)}`);
84
100
  }
85
101
  // ── design system (v1.50) ── contract: docs/codebase/CLI-DESIGN.md ──────────
86
102
  // Every cockpit surface styles through these tokens/glyphs/helpers. Styled only
@@ -1,9 +1,12 @@
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 CatalogReadResult } from "../../adapters/catalog-remote.js";
4
5
  export type DoctorOpts = {
5
6
  banner?: boolean;
6
7
  kimiTurnProbe?: (cwd: string) => Promise<KimiDoctorTurnResult>;
7
8
  resolveClaudeAliasIdentity?: (cwd: string, alias: ClaudeAlias) => string | undefined;
9
+ catalog?: CatalogReadResult;
10
+ catalogNow?: () => Date;
8
11
  };
9
12
  export declare function doctor(_argv: string[], cwd?: string, adapters?: WorkerAdapter[], opts?: DoctorOpts): Promise<string>;
@@ -1,19 +1,29 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { allAdapters, binaryShadowWarnings, detectCandidateClis, flagDriftWarnings, modelAliasExclusions, modelAliasLine, probeAll, probeModels, readAutoPrefer, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
3
+ import { allAdapters, binaryShadowWarnings, detectCandidateClis, flagDriftWarnings, modelAliasExclusions, modelAliasLine, probeAll, probeModels, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
4
4
  import { CLAUDE_ALIAS_IDENTITY_STAMPS, claudeCode, resolveClaudeAliasIdentity } from "../../adapters/claude-code.js";
5
5
  import { BANNER, dim, fail, kvRow, legend, ok, rule, statusRow, title } from "../../brand.js";
6
6
  import { tickmarkrDir, stateDirName } from "../../graph/graph.js";
7
- import { declaredModelWindow, hasWindowsConfig, modelLints, suggestOverlay, ttyVisual } from "../../adapters/model-lints.js";
8
- import { DEFAULT_CONFIG, loadConfig, overlayPreferShapes } from "../../config/config.js";
7
+ import { catalogModelAdvisory, declaredModelWindow, hasWindowsConfig, modelLints, suggestOverlay, ttyVisual } from "../../adapters/model-lints.js";
8
+ import { loadConfig, overlayPreferShapes } from "../../config/config.js";
9
9
  import { HerdrDriver } from "../../drivers/herdr.js";
10
10
  import { kimi, probeKimiDoctorTurn } from "../../adapters/kimi.js";
11
11
  import { denyPreferCollisionLine, denyPreferCollisions, disallowedBy, excludedChannels, exclusionLine, preferRanks } from "../../route/preference.js";
12
+ import { readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
12
13
  const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
13
14
  const alignedStatusRow = (verdict, key, value) => ` ${statusRow(verdict, kvRow(key, value).slice(2))}`;
14
15
  const attentionRow = (text) => ` ${statusRow("warn", text)}`;
15
16
  export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(), opts = {}) {
17
+ if (_argv.length === 1 && _argv[0] === "--refresh-catalog") {
18
+ const refreshed = await refreshCatalogCommand({ repoRoot: cwd, now: opts.catalogNow });
19
+ return refreshed.updated
20
+ ? `tickmarkr doctor --refresh-catalog: model catalog refreshed${refreshed.warning ? `; ${refreshed.warning}` : ""}`
21
+ : `tickmarkr doctor --refresh-catalog: catalog refresh unavailable — ${refreshed.warning ?? "unknown failure"}; retained ${refreshed.catalog.source} catalog`;
22
+ }
16
23
  const cfg = loadConfig(cwd);
24
+ // T17: synchronous/cache-only by operator ruling. The only network-bearing catalog function is the
25
+ // separately invoked refreshCatalogCommand; ordinary doctor never calls it.
26
+ const catalog = opts.catalog ?? readCachedCatalog(cwd, { now: opts.catalogNow });
17
27
  // banner at START — the logo greets the operator before the ~60s probe wait, never trailing it (operator report 2026-07-17)
18
28
  if (opts.banner !== false && visual())
19
29
  process.stdout.write(BANNER);
@@ -72,8 +82,12 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
72
82
  const healthy = h.installed && (a.id !== kimi.id || h.authed);
73
83
  return alignedStatusRow(healthy ? "pass" : "fail", a.id, state);
74
84
  });
75
- // v1.48 T1: advisory sweep for known agent CLIs with no adapter — never written to doctor.json health.
76
- rows.push(...detectCandidateClis().map(({ binary, version }) => alignedStatusRow("warn", binary, `detected: ${version ?? "version unknown"} (no tickmarkr adapter — not routable)`)));
85
+ if (catalog.warning) {
86
+ rows.push(attentionRow(`model catalog cache unreadable — ${catalog.warning}; using vendored fallback (advisory — routing unchanged)`));
87
+ }
88
+ // v1.48 T1 / v1.86 T12: advisory sweep for known agent CLIs with no drive contract. Presence is
89
+ // resolved through the worker's login shell; advisory targets are never executed or written to health.
90
+ rows.push(...detectCandidateClis({ cwd }).map(({ binary, path }) => alignedStatusRow("warn", binary, `detected at ${path} (no drive contract — not routable)`)));
77
91
  const herdr = HerdrDriver.available();
78
92
  rows.push(alignedStatusRow(herdr ? "pass" : "fail", "herdr", herdr ? "driver available (HERDR_ENV=1)" : "not detected — subprocess driver will be used"));
79
93
  // v1.22 T5: workspace-trust pre-flight — per installed adapter: trusted | seeded | action-required | n/a.
@@ -143,10 +157,23 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
143
157
  }
144
158
  }
145
159
  }
160
+ const resolvedCatalogModel = (adapter, model) => {
161
+ if (adapter !== "claude-code" || !identityResolver || !(model in CLAUDE_ALIAS_IDENTITY_STAMPS))
162
+ return undefined;
163
+ try {
164
+ return identityResolver(cwd, model);
165
+ }
166
+ catch {
167
+ return undefined;
168
+ }
169
+ };
146
170
  // MODEL-05/06: print-only drift fragment; advisory, whole-line-commented additions, tickmarkr NEVER applies it.
147
171
  // TTY gets a one-line summary + the fragment as a file (the full dump drowned everything else,
148
172
  // v1.33.1 onboarding); machine/CI surface keeps the inline dump — layout is pinned by tests.
149
- const frag = suggestOverlay(cfg, health, adapters, stateDirName(cwd));
173
+ const frag = suggestOverlay(cfg, health, adapters, stateDirName(cwd), {
174
+ catalog,
175
+ resolvedModel: resolvedCatalogModel,
176
+ });
150
177
  let drift = "";
151
178
  if (frag) {
152
179
  if (visual()) {
@@ -191,24 +218,19 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
191
218
  : "";
192
219
  rows.push(` ${m.padEnd(w)} ${classified[m].padEnd(8)}${windowCol} ${auth} ${dim("denied=")}${denied} ${dim("prefer=")}${pref}`);
193
220
  }
194
- if (unclassified.length)
221
+ if (unclassified.length) {
222
+ // Keep the historical count as an index, but it is no longer the whole report: every model gets
223
+ // a cache-backed evidence or explicit uncovered line. Neither branch changes cfg, health, or route().
195
224
  rows.push(` ${dim(`(${unclassified.length} more listed, unclassified)`)}`);
225
+ for (const model of unclassified) {
226
+ const advisory = catalogModelAdvisory(cfg, catalog, a.id, model, resolvedCatalogModel(a.id, model));
227
+ rows.push(` ${dim(`catalog · ${advisory.display}`)}`);
228
+ }
229
+ }
196
230
  return rows;
197
231
  });
198
- const autoPrefer = readAutoPrefer(cwd);
199
- const preferStatus = autoPrefer
200
- ? Object.keys(cfg.routing.map).flatMap((shape) => {
201
- const auto = autoPrefer[shape];
202
- if (!Array.isArray(auto))
203
- return [];
204
- const seed = DEFAULT_CONFIG.routing.map[shape]?.prefer ?? [];
205
- if (!auto.length && !seed.length)
206
- return []; // nothing derived, nothing seeded — an empty line is noise
207
- return [` ${dim(`prefer ${shape} (auto):`)} ${auto.join(" > ")} ${dim("— seed was")} [${seed.join(", ")}]`];
208
- })
209
- : [];
210
- const modelSummary = modelStatus.length || preferStatus.length
211
- ? `\n${legend("model status:")}\n${[...modelStatus, ...preferStatus].join("\n")}`
232
+ const modelSummary = modelStatus.length
233
+ ? `\n${legend("model status:")}\n${modelStatus.join("\n")}`
212
234
  : "";
213
235
  const header = visual()
214
236
  ? `${statusRow("pass", `${title("tickmarkr doctor")} ${legend("· capability matrix")}`)}\n${rule()}`
@@ -15,6 +15,13 @@ export type FleetIO = {
15
15
  debug?: boolean;
16
16
  reloadGuard?: (bytes: string) => string | null;
17
17
  };
18
+ export type FleetWriteHooks = {
19
+ readPrior?: (path: string) => string;
20
+ beforeRename?: () => void;
21
+ };
22
+ /** Atomic sibling-temp writer. Reading and serializing happen before `${path}.tmp` exists, and
23
+ * any pre-rename interruption unlinks only that exact candidate while the original remains intact. */
24
+ export declare function writeFleetOverlay(path: string, serialize: (priorBytes: string) => string, hooks?: FleetWriteHooks): void;
18
25
  export declare function fleet(argv: string[], cwd?: string, adapters?: WorkerAdapter[], io?: FleetIO): Promise<string | {
19
26
  out: string;
20
27
  code: number;