tickmarkr 1.84.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 (87) 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.d.ts +1 -0
  7. package/dist/adapters/claude-code.js +59 -1
  8. package/dist/adapters/fake.js +42 -4
  9. package/dist/adapters/model-lints.d.ts +25 -5
  10. package/dist/adapters/model-lints.js +184 -50
  11. package/dist/adapters/model-windows.d.ts +31 -0
  12. package/dist/adapters/model-windows.js +69 -0
  13. package/dist/adapters/prompt.d.ts +5 -1
  14. package/dist/adapters/prompt.js +13 -4
  15. package/dist/adapters/registry.d.ts +25 -26
  16. package/dist/adapters/registry.js +173 -110
  17. package/dist/adapters/types.d.ts +3 -0
  18. package/dist/adapters/types.js +36 -3
  19. package/dist/brand.d.ts +5 -1
  20. package/dist/brand.js +18 -2
  21. package/dist/cli/commands/doctor.d.ts +3 -0
  22. package/dist/cli/commands/doctor.js +43 -21
  23. package/dist/cli/commands/fleet.d.ts +7 -0
  24. package/dist/cli/commands/fleet.js +94 -74
  25. package/dist/cli/commands/init.js +118 -5
  26. package/dist/cli/commands/status.js +202 -46
  27. package/dist/compile/collateral.d.ts +86 -2
  28. package/dist/compile/collateral.js +294 -3
  29. package/dist/compile/gsd.d.ts +2 -1
  30. package/dist/compile/gsd.js +68 -2
  31. package/dist/compile/native.d.ts +14 -0
  32. package/dist/compile/native.js +161 -12
  33. package/dist/config/config.d.ts +82 -5
  34. package/dist/config/config.js +253 -66
  35. package/dist/config/fleet-overlay.d.ts +25 -20
  36. package/dist/config/fleet-overlay.js +195 -77
  37. package/dist/config/fleet-why.d.ts +23 -0
  38. package/dist/config/fleet-why.js +42 -0
  39. package/dist/drivers/herdr.d.ts +21 -3
  40. package/dist/drivers/herdr.js +344 -110
  41. package/dist/gates/acceptance.js +7 -2
  42. package/dist/gates/baseline.d.ts +1 -0
  43. package/dist/gates/baseline.js +91 -13
  44. package/dist/gates/llm.d.ts +0 -1
  45. package/dist/gates/llm.js +5 -30
  46. package/dist/gates/review.d.ts +9 -1
  47. package/dist/gates/review.js +105 -10
  48. package/dist/gates/run-gates.d.ts +9 -0
  49. package/dist/gates/run-gates.js +285 -41
  50. package/dist/gates/verdict-cause.d.ts +4 -0
  51. package/dist/gates/verdict-cause.js +63 -0
  52. package/dist/graph/schema.d.ts +6 -0
  53. package/dist/graph/schema.js +8 -5
  54. package/dist/route/router.d.ts +0 -5
  55. package/dist/route/router.js +16 -20
  56. package/dist/run/consult.d.ts +6 -0
  57. package/dist/run/consult.js +35 -25
  58. package/dist/run/daemon.d.ts +48 -2
  59. package/dist/run/daemon.js +1488 -330
  60. package/dist/run/journal.d.ts +56 -3
  61. package/dist/run/journal.js +358 -4
  62. package/dist/run/stall.d.ts +35 -1
  63. package/dist/run/stall.js +118 -8
  64. package/dist/tui/cockpit/capture.d.ts +12 -0
  65. package/dist/tui/cockpit/capture.js +37 -1
  66. package/dist/tui/cockpit/components.d.ts +2 -0
  67. package/dist/tui/cockpit/components.js +8 -8
  68. package/dist/tui/cockpit/derive.d.ts +29 -2
  69. package/dist/tui/cockpit/derive.js +219 -23
  70. package/dist/tui/cockpit/run-cockpit.js +128 -27
  71. package/dist/tui/cockpit/theme.d.ts +32 -26
  72. package/dist/tui/cockpit/theme.js +11 -5
  73. package/dist/tui/ink/components.d.ts +0 -15
  74. package/dist/tui/ink/components.js +0 -17
  75. package/dist/tui/ink/fleet-app.d.ts +4 -1
  76. package/dist/tui/ink/fleet-app.js +134 -13
  77. package/fixtures/sample.native.md +1 -1
  78. package/package.json +1 -1
  79. package/skills/tickmarkr-overseer/SKILL.md +354 -34
  80. package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +70 -0
  81. package/skills/tickmarkr-overseer/scripts/watch-panes.sh +1 -1
  82. package/dist/tui/ink/studio-app.d.ts +0 -59
  83. package/dist/tui/ink/studio-app.js +0 -320
  84. package/dist/tui/save.d.ts +0 -38
  85. package/dist/tui/save.js +0 -96
  86. package/dist/tui/staging.d.ts +0 -29
  87. package/dist/tui/staging.js +0 -78
package/README.md CHANGED
@@ -253,7 +253,9 @@ the frontier-model consult is the *National Office*. The terms below use that vo
253
253
 
254
254
  ### Tab vocabulary
255
255
 
256
- - **WORKERS**: the primary generation tab showing active field-team tasks. The tab header shows a live token (task ID) plus one state glyph:
256
+ - **`<taskId>`**: one tab per TASK. A task's worker pane and every gate pane it earns — judge, review,
257
+ consult — live together in that task's tab, so everything happening to one task is in one place. The
258
+ tab header shows the task token plus one state glyph:
257
259
  - ↻ — the member is a retry attempt (attempt > 0)
258
260
  - ✋ — the driver detected the member is blocked
259
261
  - (bare token) — member is running normally
@@ -288,7 +290,7 @@ This keeps the Partner focused on decisions that require attention, not noise.
288
290
  tickmarkr closes exactly what it owns and no longer needs, no matter how any process died.
289
291
 
290
292
  Every pane and tab tickmarkr creates receives a **parseable ownership name** encoding the pane's role, task, attempt, and run:
291
- - `WORKERS` — active worker generation tab
293
+ - `<taskId>` — the task's tab, holding its worker and its judge/review/consult panes
292
294
  - `cleanup · <taskId>` — teardown generation tab for overflow attempts
293
295
  - `watch` — status monitor pane
294
296
  - `<role> · <taskId> · A<attempt> · R<runId>` — judge, review, consult, and worker panes (formats like `judge · task-abc123 · A1 · Rrun-20260713-175532`)
@@ -0,0 +1,64 @@
1
+ export declare const MODELS_DEV_CATALOG_URL = "https://models.dev/api.json";
2
+ export declare const ARTIFICIAL_ANALYSIS_CATALOG_URL = "https://artificialanalysis.ai/api/v2/language/models/free";
3
+ export declare const CATALOG_CACHE_MAX_AGE_MS: number;
4
+ export declare const CATALOG_REFRESH_TIMEOUT_MS = 10000;
5
+ export interface CatalogCache {
6
+ schemaVersion: 1;
7
+ fetchedAt: string;
8
+ modelsDev: unknown;
9
+ artificialAnalysis?: unknown;
10
+ }
11
+ export interface CatalogReadResult {
12
+ catalog: CatalogCache;
13
+ source: "cache" | "vendored";
14
+ stale: boolean;
15
+ warning?: string;
16
+ }
17
+ export interface CatalogModelEvidence {
18
+ modelId: string;
19
+ inputCostPerMtok?: number;
20
+ outputCostPerMtok?: number;
21
+ contextWindow?: number;
22
+ outputWindow?: number;
23
+ features: string[];
24
+ intelligenceIndex?: number;
25
+ intelligenceIndexVersion?: string;
26
+ }
27
+ type CatalogResponse = {
28
+ ok: boolean;
29
+ status: number;
30
+ json: () => Promise<unknown>;
31
+ };
32
+ export type CatalogFetcher = (input: string | URL | Request, init?: RequestInit) => Promise<CatalogResponse>;
33
+ export interface RefreshCatalogOptions {
34
+ repoRoot: string;
35
+ fetcher?: CatalogFetcher;
36
+ artificialAnalysisKey?: string;
37
+ timeoutMs?: number;
38
+ now?: () => Date;
39
+ }
40
+ export interface RefreshCatalogResult {
41
+ updated: boolean;
42
+ catalog: CatalogReadResult;
43
+ warning?: string;
44
+ }
45
+ export declare const catalogCachePath: (repoRoot: string) => string;
46
+ /**
47
+ * Doctor's catalog seam: one synchronous local read with a vendored fail-open fallback.
48
+ * This function deliberately has no callback, promise, fetch, or adapter dependency.
49
+ */
50
+ export declare function readCachedCatalog(repoRoot: string, opts?: {
51
+ now?: () => Date;
52
+ }): CatalogReadResult;
53
+ /** Resolve the alias identity first; the floating alias is only a fallback when identity is unknown. */
54
+ export declare function resolveCatalogModel(catalog: CatalogCache, query: {
55
+ provider?: string;
56
+ model: string;
57
+ resolvedModel?: string;
58
+ }): CatalogModelEvidence | undefined;
59
+ /**
60
+ * The named, explicit refresh path. No other function in this module can reach fetch.
61
+ * A failed refresh preserves the previous cache byte-for-byte and returns it fail-open.
62
+ */
63
+ export declare function refreshCatalogCommand(opts: RefreshCatalogOptions): Promise<RefreshCatalogResult>;
64
+ export {};
@@ -0,0 +1,287 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ export const MODELS_DEV_CATALOG_URL = "https://models.dev/api.json";
5
+ export const ARTIFICIAL_ANALYSIS_CATALOG_URL = "https://artificialanalysis.ai/api/v2/language/models/free";
6
+ export const CATALOG_CACHE_MAX_AGE_MS = 30 * 86_400_000;
7
+ export const CATALOG_REFRESH_TIMEOUT_MS = 10_000;
8
+ const ARTIFICIAL_ANALYSIS_PAGE_SIZE = 100;
9
+ const ARTIFICIAL_ANALYSIS_MAX_PAGES = 100;
10
+ const VENDORED_CATALOG = {
11
+ schemaVersion: 1,
12
+ // Package fallback copied from https://models.dev/api.json on 2026-08-05. It is deliberately
13
+ // small: an explicit refresh owns broad/current coverage, while doctor remains cache-only.
14
+ fetchedAt: "2026-08-05T20:00:05.000Z",
15
+ modelsDev: {
16
+ anthropic: {
17
+ id: "anthropic",
18
+ models: {
19
+ "claude-fable-5": { id: "claude-fable-5", cost: { input: 10, output: 50 }, limit: { context: 1_000_000, output: 128_000 }, reasoning: true, tool_call: true, structured_output: true, attachment: true },
20
+ "claude-opus-4-8": { id: "claude-opus-4-8", cost: { input: 5, output: 25 }, limit: { context: 1_000_000, output: 128_000 }, reasoning: true, tool_call: true, structured_output: true, attachment: true },
21
+ "claude-sonnet-5": { id: "claude-sonnet-5", cost: { input: 2, output: 10 }, limit: { context: 1_000_000, output: 128_000 }, reasoning: true, tool_call: true, structured_output: true, attachment: true },
22
+ "claude-haiku-4-5-20251001": { id: "claude-haiku-4-5-20251001", cost: { input: 1, output: 5 }, limit: { context: 200_000, output: 64_000 }, reasoning: true, tool_call: true, structured_output: true, attachment: true },
23
+ },
24
+ },
25
+ },
26
+ };
27
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
28
+ ? value
29
+ : undefined;
30
+ const finite = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
31
+ const nonNegative = (value) => {
32
+ const n = finite(value);
33
+ return n !== undefined && n >= 0 ? n : undefined;
34
+ };
35
+ const positive = (value) => {
36
+ const n = finite(value);
37
+ return n !== undefined && n > 0 ? n : undefined;
38
+ };
39
+ const validCache = (value) => {
40
+ const root = record(value);
41
+ return root?.schemaVersion === 1
42
+ && typeof root.fetchedAt === "string"
43
+ && Number.isFinite(Date.parse(root.fetchedAt))
44
+ && validModelsDevCatalog(root.modelsDev);
45
+ };
46
+ const validModelsDevCatalog = (value) => {
47
+ const providers = record(value);
48
+ if (!providers || Object.keys(providers).length === 0)
49
+ return false;
50
+ return Object.values(providers).some((provider) => {
51
+ const models = record(record(provider)?.models);
52
+ return models !== undefined && Object.keys(models).length > 0;
53
+ });
54
+ };
55
+ const staleAt = (fetchedAt, now) => {
56
+ const age = now.getTime() - Date.parse(fetchedAt);
57
+ return !Number.isFinite(age) || age > CATALOG_CACHE_MAX_AGE_MS;
58
+ };
59
+ export const catalogCachePath = (repoRoot) => join(repoRoot, ".tickmarkr", "catalog-cache.json");
60
+ /**
61
+ * Doctor's catalog seam: one synchronous local read with a vendored fail-open fallback.
62
+ * This function deliberately has no callback, promise, fetch, or adapter dependency.
63
+ */
64
+ export function readCachedCatalog(repoRoot, opts = {}) {
65
+ const now = (opts.now ?? (() => new Date()))();
66
+ try {
67
+ const parsed = JSON.parse(readFileSync(catalogCachePath(repoRoot), "utf8"));
68
+ if (!validCache(parsed))
69
+ throw new Error("catalog cache schema is invalid");
70
+ return { catalog: parsed, source: "cache", stale: staleAt(parsed.fetchedAt, now) };
71
+ }
72
+ catch (error) {
73
+ const missing = error.code === "ENOENT";
74
+ return {
75
+ catalog: VENDORED_CATALOG,
76
+ source: "vendored",
77
+ stale: staleAt(VENDORED_CATALOG.fetchedAt, now),
78
+ ...(!missing ? { warning: error instanceof Error ? error.message : String(error) } : {}),
79
+ };
80
+ }
81
+ }
82
+ function providerModels(modelsDev, preferred) {
83
+ const providers = record(modelsDev);
84
+ if (!providers)
85
+ return [];
86
+ const entries = Object.entries(providers);
87
+ const preferredEntries = preferred
88
+ ? entries.filter(([key, value]) => key === preferred || record(value)?.id === preferred)
89
+ : [];
90
+ // Price is provider-specific. A provider-qualified query must never borrow an identically named
91
+ // model from another provider, where subscription and metered costs can differ materially.
92
+ const selected = preferred ? preferredEntries : entries;
93
+ return selected
94
+ .map(([, provider]) => record(record(provider)?.models))
95
+ .filter((models) => models !== undefined);
96
+ }
97
+ function findModelsDevModel(catalog, provider, modelId) {
98
+ for (const models of providerModels(catalog.modelsDev, provider)) {
99
+ const direct = record(models[modelId]);
100
+ if (direct)
101
+ return direct;
102
+ const byId = Object.values(models).map(record).find((candidate) => candidate?.id === modelId);
103
+ if (byId)
104
+ return byId;
105
+ }
106
+ return undefined;
107
+ }
108
+ function artificialAnalysisRows(value) {
109
+ if (Array.isArray(value))
110
+ return value;
111
+ const root = record(value);
112
+ for (const key of ["data", "models", "results"]) {
113
+ if (Array.isArray(root?.[key]))
114
+ return root[key];
115
+ }
116
+ return [];
117
+ }
118
+ const canonicalCatalogIdentity = (identity) => identity.trim().toLowerCase().replace(/[^a-z0-9]+/g, "").replace(/(?:19|20)\d{6}$/, "");
119
+ function artificialAnalysisIndex(value, identities, provider) {
120
+ const root = record(value);
121
+ const wanted = identities.map(canonicalCatalogIdentity).filter(Boolean);
122
+ for (const raw of artificialAnalysisRows(value)) {
123
+ const row = record(raw);
124
+ if (!row)
125
+ continue;
126
+ const nestedModel = record(row.model);
127
+ const creator = record(row.model_creator) ?? record(row.creator);
128
+ const creatorIds = [row.provider, creator?.id, creator?.slug, creator?.name]
129
+ .filter((candidate) => typeof candidate === "string")
130
+ .map(canonicalCatalogIdentity);
131
+ if (provider && creatorIds.length > 0 && !creatorIds.includes(canonicalCatalogIdentity(provider)))
132
+ continue;
133
+ const ids = [row.id, row.model_id, row.slug, row.name, nestedModel?.id, nestedModel?.slug, nestedModel?.name]
134
+ .filter((candidate) => typeof candidate === "string")
135
+ .map(canonicalCatalogIdentity);
136
+ if (!ids.some((identity) => wanted.includes(identity)))
137
+ continue;
138
+ const evaluations = record(row.evaluations);
139
+ for (const candidate of [
140
+ evaluations?.artificial_analysis_intelligence_index,
141
+ evaluations?.intelligence_index,
142
+ row.artificial_analysis_intelligence_index,
143
+ row.intelligence_index,
144
+ ]) {
145
+ const n = nonNegative(candidate);
146
+ if (n !== undefined) {
147
+ const version = root?.intelligence_index_version;
148
+ return {
149
+ index: n,
150
+ ...((typeof version === "string" || typeof version === "number") ? { version: String(version) } : {}),
151
+ };
152
+ }
153
+ }
154
+ }
155
+ return undefined;
156
+ }
157
+ /** Resolve the alias identity first; the floating alias is only a fallback when identity is unknown. */
158
+ export function resolveCatalogModel(catalog, query) {
159
+ const modelId = query.resolvedModel ?? query.model;
160
+ const model = findModelsDevModel(catalog, query.provider, modelId);
161
+ if (!model)
162
+ return undefined;
163
+ const cost = record(model.cost);
164
+ const limit = record(model.limit);
165
+ const intelligence = artificialAnalysisIndex(catalog.artificialAnalysis, [
166
+ modelId,
167
+ ...(typeof model.name === "string" ? [model.name] : []),
168
+ ], query.provider);
169
+ const features = [
170
+ ["reasoning", model.reasoning],
171
+ ["tool-call", model.tool_call],
172
+ ["structured-output", model.structured_output],
173
+ ["attachment", model.attachment],
174
+ ["open-weights", model.open_weights],
175
+ ].filter((entry) => entry[1] === true).map((entry) => entry[0]);
176
+ return {
177
+ modelId,
178
+ ...(nonNegative(cost?.input) !== undefined ? { inputCostPerMtok: nonNegative(cost?.input) } : {}),
179
+ ...(nonNegative(cost?.output) !== undefined ? { outputCostPerMtok: nonNegative(cost?.output) } : {}),
180
+ ...(positive(limit?.context) !== undefined ? { contextWindow: positive(limit?.context) } : {}),
181
+ ...(positive(limit?.output) !== undefined ? { outputWindow: positive(limit?.output) } : {}),
182
+ features,
183
+ ...(intelligence !== undefined ? {
184
+ intelligenceIndex: intelligence.index,
185
+ ...(intelligence.version ? { intelligenceIndexVersion: intelligence.version } : {}),
186
+ } : {}),
187
+ };
188
+ }
189
+ async function fetchJson(fetcher, url, init, timeoutMs) {
190
+ const controller = new AbortController();
191
+ let timer;
192
+ const timeout = new Promise((_resolve, reject) => {
193
+ timer = setTimeout(() => {
194
+ const error = new Error(`catalog request timed out after ${timeoutMs}ms`);
195
+ controller.abort(error);
196
+ reject(error);
197
+ }, timeoutMs);
198
+ });
199
+ try {
200
+ const request = (async () => {
201
+ const response = await fetcher(url, { ...init, signal: controller.signal });
202
+ if (!response.ok)
203
+ throw new Error(`${url === MODELS_DEV_CATALOG_URL ? "models.dev" : "Artificial Analysis"} HTTP ${response.status}`);
204
+ return response.json();
205
+ })();
206
+ return await Promise.race([request, timeout]);
207
+ }
208
+ finally {
209
+ if (timer)
210
+ clearTimeout(timer);
211
+ }
212
+ }
213
+ function writeCatalogCache(repoRoot, catalog) {
214
+ const path = catalogCachePath(repoRoot);
215
+ mkdirSync(dirname(path), { recursive: true });
216
+ const tmp = `${path}.${process.pid}-${randomUUID()}.tmp`;
217
+ try {
218
+ writeFileSync(tmp, JSON.stringify(catalog, null, 2) + "\n");
219
+ renameSync(tmp, path);
220
+ }
221
+ catch (error) {
222
+ rmSync(tmp, { force: true });
223
+ throw error;
224
+ }
225
+ }
226
+ async function fetchArtificialAnalysis(fetcher, apiKey, timeoutMs) {
227
+ const rows = [];
228
+ let first;
229
+ let lastPagination;
230
+ let intelligenceIndexVersion;
231
+ for (let page = 1; page <= ARTIFICIAL_ANALYSIS_MAX_PAGES; page++) {
232
+ const url = `${ARTIFICIAL_ANALYSIS_CATALOG_URL}?page=${page}&page_size=${ARTIFICIAL_ANALYSIS_PAGE_SIZE}`;
233
+ const value = await fetchJson(fetcher, url, { headers: { "x-api-key": apiKey } }, timeoutMs);
234
+ const root = record(value);
235
+ if (!root || !Array.isArray(root.data))
236
+ throw new Error("Artificial Analysis catalog schema is invalid");
237
+ first ??= root;
238
+ if (typeof root.intelligence_index_version === "string" || typeof root.intelligence_index_version === "number") {
239
+ intelligenceIndexVersion ??= root.intelligence_index_version;
240
+ }
241
+ rows.push(...root.data);
242
+ lastPagination = record(root.pagination);
243
+ if (lastPagination?.has_more !== true) {
244
+ return {
245
+ ...first,
246
+ ...(intelligenceIndexVersion !== undefined ? { intelligence_index_version: intelligenceIndexVersion } : {}),
247
+ data: rows,
248
+ ...(lastPagination ? { pagination: { ...lastPagination, has_more: false } } : {}),
249
+ };
250
+ }
251
+ }
252
+ throw new Error(`Artificial Analysis catalog exceeds ${ARTIFICIAL_ANALYSIS_MAX_PAGES} pages`);
253
+ }
254
+ /**
255
+ * The named, explicit refresh path. No other function in this module can reach fetch.
256
+ * A failed refresh preserves the previous cache byte-for-byte and returns it fail-open.
257
+ */
258
+ export async function refreshCatalogCommand(opts) {
259
+ const now = opts.now ?? (() => new Date());
260
+ const current = readCachedCatalog(opts.repoRoot, { now });
261
+ try {
262
+ const fetcher = opts.fetcher ?? globalThis.fetch.bind(globalThis);
263
+ const timeoutMs = opts.timeoutMs ?? CATALOG_REFRESH_TIMEOUT_MS;
264
+ const modelsDev = await fetchJson(fetcher, MODELS_DEV_CATALOG_URL, {}, timeoutMs);
265
+ if (!validModelsDevCatalog(modelsDev))
266
+ throw new Error("models.dev catalog schema is invalid");
267
+ const apiKey = opts.artificialAnalysisKey ?? process.env.ARTIFICIAL_ANALYSIS_API_KEY?.trim();
268
+ const artificialAnalysis = apiKey
269
+ ? await fetchArtificialAnalysis(fetcher, apiKey, timeoutMs)
270
+ : undefined;
271
+ const catalog = {
272
+ schemaVersion: 1,
273
+ fetchedAt: now().toISOString(),
274
+ modelsDev,
275
+ ...(artificialAnalysis !== undefined ? { artificialAnalysis } : {}),
276
+ };
277
+ writeCatalogCache(opts.repoRoot, catalog);
278
+ return { updated: true, catalog: readCachedCatalog(opts.repoRoot, { now }) };
279
+ }
280
+ catch (error) {
281
+ return {
282
+ updated: false,
283
+ catalog: current,
284
+ warning: error instanceof Error ? error.message : String(error),
285
+ };
286
+ }
287
+ }
@@ -0,0 +1,96 @@
1
+ import { z } from "zod";
2
+ import type { WorkerAdapter } from "./types.js";
3
+ export declare const CLI_NAME_RE: RegExp;
4
+ export declare const CliDriveSchema: z.ZodObject<{
5
+ headless: z.ZodString;
6
+ interactive: z.ZodNullable<z.ZodString>;
7
+ listModels: z.ZodOptional<z.ZodObject<{
8
+ argv: z.ZodArray<z.ZodString>;
9
+ parser: z.ZodEnum<{
10
+ lines: "lines";
11
+ "pi-table": "pi-table";
12
+ json: "json";
13
+ }>;
14
+ path: z.ZodOptional<z.ZodString>;
15
+ }, z.core.$strict>>;
16
+ }, z.core.$strict>;
17
+ declare const CliEntryBaseSchema: z.ZodObject<{
18
+ id: z.ZodString;
19
+ binary: z.ZodString;
20
+ identity: z.ZodString;
21
+ vendor: z.ZodNullable<z.ZodString>;
22
+ }, z.core.$strict>;
23
+ export declare const CliEntrySchema: z.ZodObject<{
24
+ id: z.ZodString;
25
+ binary: z.ZodString;
26
+ identity: z.ZodString;
27
+ vendor: z.ZodNullable<z.ZodString>;
28
+ drive: z.ZodOptional<z.ZodObject<{
29
+ headless: z.ZodString;
30
+ interactive: z.ZodNullable<z.ZodString>;
31
+ listModels: z.ZodOptional<z.ZodObject<{
32
+ argv: z.ZodArray<z.ZodString>;
33
+ parser: z.ZodEnum<{
34
+ lines: "lines";
35
+ "pi-table": "pi-table";
36
+ json: "json";
37
+ }>;
38
+ path: z.ZodOptional<z.ZodString>;
39
+ }, z.core.$strict>>;
40
+ }, z.core.$strict>>;
41
+ }, z.core.$strict>;
42
+ export type DeclarativeCliDrive = z.infer<typeof CliDriveSchema>;
43
+ export type NativeCliDrive = {
44
+ adapter: WorkerAdapter;
45
+ };
46
+ export type CliDrive = DeclarativeCliDrive | NativeCliDrive;
47
+ export type CliEntry = z.infer<typeof CliEntryBaseSchema> & {
48
+ drive?: CliDrive;
49
+ };
50
+ export declare function isNativeCliDrive(drive: CliDrive | undefined): drive is NativeCliDrive;
51
+ export declare const CLI_CATALOG: readonly CliEntry[];
52
+ export declare const SHIPPED_CLI_CATALOG: readonly CliEntry[];
53
+ export declare function parseOperatorCliCatalog(text: string): CliEntry[];
54
+ /** The one operator-owned definition file. Never relative to a repository and never enumerated. */
55
+ export declare function operatorCliCatalogPath(env?: NodeJS.ProcessEnv): string;
56
+ export declare function loadOperatorCliCatalog(path?: string): CliEntry[];
57
+ export interface CatalogEntriesOptions {
58
+ shipped?: readonly CliEntry[];
59
+ operatorYaml?: string | null;
60
+ operatorPath?: string | false;
61
+ herdrNames?: readonly string[];
62
+ }
63
+ /**
64
+ * Compose package data, the single operator YAML layer, and Herdr name nominations into one array.
65
+ * Herdr cannot define or override a drive contract: valid tokens nominate only names not already
66
+ * defined. Invalid tokens are discarded before any resolver can receive them.
67
+ */
68
+ export declare function catalogEntries(opts?: CatalogEntriesOptions): CliEntry[];
69
+ export interface CliCatalogConflict {
70
+ binary: string;
71
+ ids: string[];
72
+ }
73
+ export declare function projectCliEntries(entries: readonly CliEntry[]): {
74
+ routable: CliEntry[];
75
+ advisory: CliEntry[];
76
+ conflicts: CliCatalogConflict[];
77
+ };
78
+ export interface CliPresence {
79
+ id: string;
80
+ binary: string;
81
+ path: string;
82
+ routable: boolean;
83
+ }
84
+ export interface CliDiscoveryDeps {
85
+ resolveBinary: (binary: string) => {
86
+ resolved?: string;
87
+ all: string[];
88
+ };
89
+ execute?: (entry: CliEntry, path: string) => unknown;
90
+ }
91
+ /** Resolve presence for both projections; only a declared drive contract licenses target execution. */
92
+ export declare function discoverCliEntries(entries: readonly CliEntry[], deps: CliDiscoveryDeps): {
93
+ present: CliPresence[];
94
+ conflicts: CliCatalogConflict[];
95
+ };
96
+ export {};
@@ -0,0 +1,176 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { isAbsolute, join } from "node:path";
4
+ import { parse as parseYaml } from "yaml";
5
+ import { z } from "zod";
6
+ import { claudeCode } from "./claude-code.js";
7
+ import { codex } from "./codex.js";
8
+ import { cursorAgent } from "./cursor-agent.js";
9
+ import { grok } from "./grok.js";
10
+ import { kimi } from "./kimi.js";
11
+ import { opencode } from "./opencode.js";
12
+ import { pi } from "./pi.js";
13
+ export const CLI_NAME_RE = /^[a-z0-9-]+$/;
14
+ const ListModelsSchema = z.object({
15
+ argv: z.array(z.string()).min(1),
16
+ parser: z.enum(["lines", "pi-table", "json"]),
17
+ path: z.string().min(1).optional(),
18
+ }).strict();
19
+ export const CliDriveSchema = z.object({
20
+ headless: z.string().min(1),
21
+ interactive: z.string().min(1).nullable(),
22
+ listModels: ListModelsSchema.optional(),
23
+ }).strict().superRefine((drive, ctx) => {
24
+ for (const [field, template] of [["headless", drive.headless], ["interactive", drive.interactive]]) {
25
+ if (template === null)
26
+ continue;
27
+ for (const placeholder of ["{promptFile}", "{model}"]) {
28
+ if (!template.includes(placeholder)) {
29
+ ctx.addIssue({ code: "custom", path: [field], message: `${field} must contain ${placeholder}` });
30
+ }
31
+ }
32
+ }
33
+ });
34
+ const CliEntryBaseSchema = z.object({
35
+ id: z.string().regex(CLI_NAME_RE, "id must match [a-z0-9-]+"),
36
+ binary: z.string().regex(CLI_NAME_RE, "binary must match [a-z0-9-]+"),
37
+ // A declarative drive may execute `--version`; its first line must identify the expected CLI.
38
+ // Advisory entries carry the field too so shipped and operator data have one schema, but never use it.
39
+ identity: z.string().min(1).refine((source) => {
40
+ try {
41
+ new RegExp(source);
42
+ return true;
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }, "identity must be a valid regular expression"),
48
+ vendor: z.string().min(1).nullable(),
49
+ }).strict();
50
+ export const CliEntrySchema = CliEntryBaseSchema.extend({
51
+ drive: CliDriveSchema.optional(),
52
+ }).strict().superRefine((entry, ctx) => {
53
+ if (entry.drive && entry.vendor === null) {
54
+ ctx.addIssue({
55
+ code: "custom",
56
+ path: ["vendor"],
57
+ message: "a drive contract requires a vendor until per-model vendor configuration lands",
58
+ });
59
+ }
60
+ });
61
+ export function isNativeCliDrive(drive) {
62
+ return !!drive && "adapter" in drive;
63
+ }
64
+ function validateCliEntry(entry) {
65
+ if (!isNativeCliDrive(entry.drive))
66
+ return CliEntrySchema.parse(entry);
67
+ const { drive, ...fields } = entry;
68
+ const base = CliEntryBaseSchema.parse(fields);
69
+ if (drive.adapter.id !== base.id || drive.adapter.vendor !== base.vendor) {
70
+ throw new Error(`native drive identity mismatch for ${base.id}`);
71
+ }
72
+ return { ...base, drive };
73
+ }
74
+ // Package-owned, deterministic order. This is the sole shipped definition array: candidate-name
75
+ // compatibility and advisory/routable projections below are all derived from it.
76
+ const native = (adapter, binary) => ({
77
+ id: adapter.id,
78
+ binary,
79
+ identity: ".+",
80
+ vendor: adapter.vendor,
81
+ drive: { adapter },
82
+ });
83
+ export const CLI_CATALOG = [
84
+ // Preserve the historical tie-break order byte-for-byte: pi, grok and kimi stay last among the
85
+ // native adapters, with kimi after grok. Catalog-driven adapters append after this sequence.
86
+ native(claudeCode, "claude"),
87
+ native(codex, "codex"),
88
+ native(cursorAgent, "cursor-agent"),
89
+ native(opencode, "opencode"),
90
+ native(pi, "pi"),
91
+ native(grok, "grok"),
92
+ native(kimi, "kimi"),
93
+ "gemini", "qwen", "aider", "goose", "amp", "droid", "auggie", "crush", "omp",
94
+ ].flatMap((entry) => typeof entry === "string"
95
+ ? [{ id: entry, binary: entry, identity: ".+", vendor: null }]
96
+ : [entry]);
97
+ export const SHIPPED_CLI_CATALOG = CLI_CATALOG;
98
+ const OperatorCatalogSchema = z.union([
99
+ z.array(CliEntrySchema),
100
+ z.object({ clis: z.array(CliEntrySchema) }).strict().transform(({ clis }) => clis),
101
+ ]);
102
+ export function parseOperatorCliCatalog(text) {
103
+ return OperatorCatalogSchema.parse(parseYaml(text));
104
+ }
105
+ /** The one operator-owned definition file. Never relative to a repository and never enumerated. */
106
+ export function operatorCliCatalogPath(env = process.env) {
107
+ const xdg = env.XDG_CONFIG_HOME?.trim();
108
+ const configRoot = xdg && isAbsolute(xdg) ? xdg : join(env.HOME?.trim() || homedir(), ".config");
109
+ return join(configRoot, "tickmarkr", "clis.yaml");
110
+ }
111
+ export function loadOperatorCliCatalog(path = operatorCliCatalogPath()) {
112
+ if (!isAbsolute(path))
113
+ throw new Error(`operator CLI catalog path must be absolute: ${path}`);
114
+ try {
115
+ return parseOperatorCliCatalog(readFileSync(path, "utf8"));
116
+ }
117
+ catch (error) {
118
+ const missing = error.code === "ENOENT"
119
+ || (error instanceof Error && error.message.includes("ENOENT"));
120
+ if (missing)
121
+ return [];
122
+ throw error;
123
+ }
124
+ }
125
+ /**
126
+ * Compose package data, the single operator YAML layer, and Herdr name nominations into one array.
127
+ * Herdr cannot define or override a drive contract: valid tokens nominate only names not already
128
+ * defined. Invalid tokens are discarded before any resolver can receive them.
129
+ */
130
+ export function catalogEntries(opts = {}) {
131
+ const shipped = [...(opts.shipped ?? SHIPPED_CLI_CATALOG)].map(validateCliEntry);
132
+ const operator = opts.operatorYaml !== undefined
133
+ ? opts.operatorYaml === null ? [] : parseOperatorCliCatalog(opts.operatorYaml)
134
+ : opts.operatorPath === false ? [] : loadOperatorCliCatalog(opts.operatorPath);
135
+ const definitions = [...shipped, ...operator];
136
+ const claimed = new Set(definitions.map((entry) => entry.binary));
137
+ const nominated = [];
138
+ for (const name of opts.herdrNames ?? []) {
139
+ if (!CLI_NAME_RE.test(name) || claimed.has(name))
140
+ continue;
141
+ claimed.add(name);
142
+ nominated.push({ id: name, binary: name, identity: ".+", vendor: null });
143
+ }
144
+ return [...definitions, ...nominated];
145
+ }
146
+ export function projectCliEntries(entries) {
147
+ const parsed = entries.map(validateCliEntry);
148
+ const claims = new Map();
149
+ for (const entry of parsed)
150
+ claims.set(entry.binary, [...(claims.get(entry.binary) ?? []), entry]);
151
+ const conflicts = [...claims]
152
+ .filter(([, owners]) => owners.length > 1)
153
+ .map(([binary, owners]) => ({ binary, ids: owners.map((entry) => entry.id).sort() }))
154
+ .sort((a, b) => a.binary.localeCompare(b.binary));
155
+ const conflicted = new Set(conflicts.map((conflict) => conflict.binary));
156
+ const eligible = parsed.filter((entry) => !conflicted.has(entry.binary));
157
+ return {
158
+ routable: eligible.filter((entry) => entry.drive !== undefined),
159
+ advisory: eligible.filter((entry) => entry.drive === undefined),
160
+ conflicts,
161
+ };
162
+ }
163
+ /** Resolve presence for both projections; only a declared drive contract licenses target execution. */
164
+ export function discoverCliEntries(entries, deps) {
165
+ const projected = projectCliEntries(entries);
166
+ const present = [];
167
+ for (const entry of [...projected.routable, ...projected.advisory]) {
168
+ const { resolved } = deps.resolveBinary(entry.binary);
169
+ if (!resolved)
170
+ continue;
171
+ if (entry.drive)
172
+ deps.execute?.(entry, resolved);
173
+ present.push({ id: entry.id, binary: entry.binary, path: resolved, routable: entry.drive !== undefined });
174
+ }
175
+ return { present, conflicts: projected.conflicts };
176
+ }
@@ -8,6 +8,7 @@ export declare const CLAUDE_ALIAS_IDENTITY_STAMPS: {
8
8
  export type ClaudeAlias = keyof typeof CLAUDE_ALIAS_IDENTITY_STAMPS;
9
9
  export type ClaudeAliasIdentityProbe = (cwd: string, alias: ClaudeAlias) => string | undefined;
10
10
  export declare const CLAUDE_TRUST_DIALOG: TrustDialog;
11
+ export declare const CLAUDE_INPUT_BOX: import("./types.js").InputBox;
11
12
  export declare function claudeSlug(real: string): string;
12
13
  export declare function readClaudeAliasIdentity(cwd: string, alias: ClaudeAlias): string | undefined;
13
14
  export declare function probeClaudeAliasIdentity(cwd: string, alias: ClaudeAlias): string | undefined;