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.
- package/README.md +4 -2
- package/dist/adapters/catalog-remote.d.ts +64 -0
- package/dist/adapters/catalog-remote.js +287 -0
- package/dist/adapters/catalog.d.ts +96 -0
- package/dist/adapters/catalog.js +176 -0
- package/dist/adapters/claude-code.d.ts +1 -0
- package/dist/adapters/claude-code.js +59 -1
- package/dist/adapters/fake.js +42 -4
- package/dist/adapters/model-lints.d.ts +25 -5
- package/dist/adapters/model-lints.js +184 -50
- package/dist/adapters/model-windows.d.ts +31 -0
- package/dist/adapters/model-windows.js +69 -0
- package/dist/adapters/prompt.d.ts +5 -1
- package/dist/adapters/prompt.js +13 -4
- package/dist/adapters/registry.d.ts +25 -26
- package/dist/adapters/registry.js +173 -110
- package/dist/adapters/types.d.ts +3 -0
- package/dist/adapters/types.js +36 -3
- package/dist/brand.d.ts +5 -1
- package/dist/brand.js +18 -2
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +43 -21
- package/dist/cli/commands/fleet.d.ts +7 -0
- package/dist/cli/commands/fleet.js +94 -74
- package/dist/cli/commands/init.js +118 -5
- package/dist/cli/commands/status.js +202 -46
- package/dist/compile/collateral.d.ts +86 -2
- package/dist/compile/collateral.js +294 -3
- package/dist/compile/gsd.d.ts +2 -1
- package/dist/compile/gsd.js +68 -2
- package/dist/compile/native.d.ts +14 -0
- package/dist/compile/native.js +161 -12
- package/dist/config/config.d.ts +82 -5
- package/dist/config/config.js +253 -66
- package/dist/config/fleet-overlay.d.ts +25 -20
- package/dist/config/fleet-overlay.js +195 -77
- package/dist/config/fleet-why.d.ts +23 -0
- package/dist/config/fleet-why.js +42 -0
- package/dist/drivers/herdr.d.ts +21 -3
- package/dist/drivers/herdr.js +344 -110
- package/dist/gates/acceptance.js +7 -2
- package/dist/gates/baseline.d.ts +1 -0
- package/dist/gates/baseline.js +91 -13
- package/dist/gates/llm.d.ts +0 -1
- package/dist/gates/llm.js +5 -30
- package/dist/gates/review.d.ts +9 -1
- package/dist/gates/review.js +105 -10
- package/dist/gates/run-gates.d.ts +9 -0
- package/dist/gates/run-gates.js +285 -41
- package/dist/gates/verdict-cause.d.ts +4 -0
- package/dist/gates/verdict-cause.js +63 -0
- package/dist/graph/schema.d.ts +6 -0
- package/dist/graph/schema.js +8 -5
- package/dist/route/router.d.ts +0 -5
- package/dist/route/router.js +16 -20
- package/dist/run/consult.d.ts +6 -0
- package/dist/run/consult.js +35 -25
- package/dist/run/daemon.d.ts +48 -2
- package/dist/run/daemon.js +1488 -330
- package/dist/run/journal.d.ts +56 -3
- package/dist/run/journal.js +358 -4
- package/dist/run/stall.d.ts +35 -1
- package/dist/run/stall.js +118 -8
- package/dist/tui/cockpit/capture.d.ts +12 -0
- package/dist/tui/cockpit/capture.js +37 -1
- package/dist/tui/cockpit/components.d.ts +2 -0
- package/dist/tui/cockpit/components.js +8 -8
- package/dist/tui/cockpit/derive.d.ts +29 -2
- package/dist/tui/cockpit/derive.js +219 -23
- package/dist/tui/cockpit/run-cockpit.js +128 -27
- package/dist/tui/cockpit/theme.d.ts +32 -26
- package/dist/tui/cockpit/theme.js +11 -5
- package/dist/tui/ink/components.d.ts +0 -15
- package/dist/tui/ink/components.js +0 -17
- package/dist/tui/ink/fleet-app.d.ts +4 -1
- package/dist/tui/ink/fleet-app.js +134 -13
- package/fixtures/sample.native.md +1 -1
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +354 -34
- package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +70 -0
- package/skills/tickmarkr-overseer/scripts/watch-panes.sh +1 -1
- package/dist/tui/ink/studio-app.d.ts +0 -59
- package/dist/tui/ink/studio-app.js +0 -320
- package/dist/tui/save.d.ts +0 -38
- package/dist/tui/save.js +0 -96
- package/dist/tui/staging.d.ts +0 -29
- package/dist/tui/staging.js +0 -78
|
@@ -3,7 +3,7 @@ import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { parseWorkerResult } from "./prompt.js";
|
|
6
|
-
import { channelsFromConfig, MODEL_ID_RE, shq, TokenUsageSchema } from "./types.js";
|
|
6
|
+
import { channelsFromConfig, declareInputBox, MODEL_ID_RE, shq, TokenUsageSchema } from "./types.js";
|
|
7
7
|
// SPEND-01/SPEND-11: claude writes a per-session JSONL to ~/.claude/projects/<slug>/ where slug is the
|
|
8
8
|
// realpath'd cwd with every non-alphanumeric char replaced by "-" (verified 114/114 — 36-DIAGNOSIS.md).
|
|
9
9
|
// The old `/`-only formula missed the "." in `.tickmarkr/worktrees/…` — ENOENT on every worktree dispatch.
|
|
@@ -40,6 +40,63 @@ export const CLAUDE_TRUST_DIALOG = {
|
|
|
40
40
|
fingerprint: "Quick safety check: Is this a project you created or one you trust?",
|
|
41
41
|
key: "Enter",
|
|
42
42
|
};
|
|
43
|
+
// OBS-201/262 nudge deliverability: claude-code is the ONLY member of NUDGEABLE_ADAPTERS, and the
|
|
44
|
+
// driver's deliverTyped pincer (readiness stable-frame → type → read-back → verified submit) needs a
|
|
45
|
+
// declared input box for any delivery whose command is not the adapter's own launch line — which a
|
|
46
|
+
// nudge never is. Without this declaration `requireInputBox` was false, readiness fell back to
|
|
47
|
+
// "the pane no longer echoes the command", and submission fell through to the positional fallback:
|
|
48
|
+
// the rescue that OBS-262 widened the gate for could not be verifiably delivered to the one adapter
|
|
49
|
+
// allowed to receive it.
|
|
50
|
+
//
|
|
51
|
+
// CAPTURED, not guessed (claude 2.1.220, tmux 120x30, 2026-08-03 — the state-to-PIXELS law):
|
|
52
|
+
// ──────────────────────────────────────────── ← full-width rule
|
|
53
|
+
// ❯ You are still assigned this task… ← caret, NBSP, then the typed turn
|
|
54
|
+
// ──────────────────────────────────────────── ← full-width rule
|
|
55
|
+
// Two findings the kimi-shaped guess would have got wrong, and both are fatal on their own:
|
|
56
|
+
// 1. claude's editor is NOT a bordered ╭─╮ box — it is one row FENCED BY TWO RULES, so a
|
|
57
|
+
// `│ > ` fingerprint or a border-adjacency walk never fires.
|
|
58
|
+
// 2. the caret is padded with U+00A0, not a space. A `"❯ "` fingerprint with an ASCII space
|
|
59
|
+
// matches nothing, ever — the OBS-152 failure mode exactly (six versions of "kimi is broken"
|
|
60
|
+
// were all anchored matchers meeting a TUI that renders differently than assumed).
|
|
61
|
+
// `match` means THE EDITOR IS PAINTED and is deliberately true for an empty editor (readiness);
|
|
62
|
+
// `emptyMatch` is the stricter "painted AND carrying nothing", which is what proves a submit
|
|
63
|
+
// registered. Callers deciding submission must test emptyMatch first — see submissionRegistered.
|
|
64
|
+
const CLAUDE_ANSI_SGR_RE = /\u001B\[[0-9;]*m/g;
|
|
65
|
+
const CLAUDE_RULE_RE = /^─{8,}$/;
|
|
66
|
+
const CLAUDE_CARET_RE = /^❯\u00A0/;
|
|
67
|
+
const CLAUDE_CARET_EMPTY_RE = /^❯\u00A0\s*$/;
|
|
68
|
+
// A wrapped or multi-line turn grows the editor downward before the closing rule.
|
|
69
|
+
// ponytail: a fixed window, not a parser — raise it if a real capture ever shows a taller editor.
|
|
70
|
+
const CLAUDE_MAX_EDITOR_ROWS = 8;
|
|
71
|
+
function matchesClaudeEditor(paneText, empty) {
|
|
72
|
+
// Trim ASCII margins ONLY: String.trim() eats U+00A0, which would erase the very byte that
|
|
73
|
+
// distinguishes claude's caret padding from an ordinary prompt line.
|
|
74
|
+
const lines = paneText.replace(CLAUDE_ANSI_SGR_RE, "").split("\n").map((l) => l.replace(/^[ \t]+|[ \t]+$/g, ""));
|
|
75
|
+
const caret = empty ? CLAUDE_CARET_EMPTY_RE : CLAUDE_CARET_RE;
|
|
76
|
+
return lines.some((line, i) => {
|
|
77
|
+
if (!caret.test(line))
|
|
78
|
+
return false;
|
|
79
|
+
if (i === 0 || !CLAUDE_RULE_RE.test(lines[i - 1]))
|
|
80
|
+
return false;
|
|
81
|
+
for (let below = i + 1; below < lines.length && below <= i + CLAUDE_MAX_EDITOR_ROWS; below++) {
|
|
82
|
+
if (CLAUDE_RULE_RE.test(lines[below]))
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
export const CLAUDE_INPUT_BOX = declareInputBox("claude-code", {
|
|
89
|
+
fingerprint: "❯\u00A0",
|
|
90
|
+
match: (paneText) => matchesClaudeEditor(paneText, false),
|
|
91
|
+
emptyMatch: (paneText) => matchesClaudeEditor(paneText, true),
|
|
92
|
+
// OBS-342: daemon-built launches carry intent through paneDispatchCommand; their wrapper bytes are
|
|
93
|
+
// never re-parsed here. The first-delivery fact keeps direct driver consumers on the same honest
|
|
94
|
+
// lifecycle: a fresh claude-code worker slot is a shell awaiting its launch, every later delivery
|
|
95
|
+
// is a TUI turn awaiting this box. Both interactive and resume builders share that contract.
|
|
96
|
+
firstDeliveryIsLaunch: true,
|
|
97
|
+
// The 2026-08-03 capture reached a painted editor ~10s after the trust answer on a warm install.
|
|
98
|
+
readinessTimeoutMs: 30_000,
|
|
99
|
+
});
|
|
43
100
|
export function claudeSlug(real) {
|
|
44
101
|
return real.replace(/[^A-Za-z0-9]/g, "-");
|
|
45
102
|
}
|
|
@@ -168,6 +225,7 @@ export const claudeCode = {
|
|
|
168
225
|
// the daemon safely answers only the exact adapter-declared dialog once per slot.
|
|
169
226
|
interactiveCommand: (promptFile, model) => `claude --model ${shq(model)} --strict-mcp-config --mcp-config '{"mcpServers":{}}' --permission-mode bypassPermissions "$(cat ${shq(promptFile)})"`,
|
|
170
227
|
trustDialog: CLAUDE_TRUST_DIALOG,
|
|
228
|
+
inputBox: CLAUDE_INPUT_BOX,
|
|
171
229
|
resumeCommand: (sessionId, promptFile, model) => `claude -r ${shq(sessionId)} --model ${shq(model)} --strict-mcp-config --mcp-config '{"mcpServers":{}}' --permission-mode bypassPermissions "$(cat ${shq(promptFile)})"`,
|
|
172
230
|
invoke(task, _cwd, a, ctx) {
|
|
173
231
|
return { command: this.headlessCommand(ctx.promptFile, a.model) };
|
package/dist/adapters/fake.js
CHANGED
|
@@ -35,21 +35,33 @@ export class FakeAdapter {
|
|
|
35
35
|
// contains TICKMARKR-JUDGE — a review/consult prompt never touches it (research Pitfall 3).
|
|
36
36
|
let prompt = "";
|
|
37
37
|
let isJudge = false;
|
|
38
|
+
// v1.85 T4: the role THIS prompt is, when it can be told. The three scratch files are shared per
|
|
39
|
+
// script path, so a build that writes a role it is not can clobber a concurrently-dispatched
|
|
40
|
+
// sibling's file between that sibling's build and its `cat` — which is exactly what a round that
|
|
41
|
+
// launches judge and review together does. Writing only the matching role removes the race: the
|
|
42
|
+
// other roles' `grep` guards below can never fire for this prompt, so their files are never read.
|
|
43
|
+
let role;
|
|
38
44
|
try {
|
|
39
45
|
prompt = readFileSync(promptFile, "utf8");
|
|
40
46
|
isJudge = /TICKMARKR-(?:JUDGE|SCOPE)/.test(prompt);
|
|
47
|
+
role = isJudge ? "judge" : /TICKMARKR-REVIEW/.test(prompt) ? "review" : /TICKMARKR-CONSULT/.test(prompt) ? "consult" : undefined;
|
|
41
48
|
}
|
|
42
49
|
catch {
|
|
43
50
|
// unreadable promptFile: can't detect role; serve static values (legacy headless-call behavior)
|
|
44
51
|
}
|
|
52
|
+
const nonce = this.nonceFor(promptFile);
|
|
45
53
|
const serve = (key) => {
|
|
54
|
+
if (role !== undefined && key !== role)
|
|
55
|
+
return join(dirname(this.scriptPath), `${key}.json`);
|
|
46
56
|
let val = this.script[key];
|
|
47
57
|
if (key === "judge" && isJudge && Array.isArray(val)) {
|
|
48
58
|
// array served sequentially per judge prompt, clamped to last (steps[min(n,len-1)] idiom)
|
|
49
59
|
val = val[Math.min(this.judgeIdx, val.length - 1)];
|
|
50
60
|
this.judgeIdx++;
|
|
51
61
|
}
|
|
52
|
-
|
|
62
|
+
// Legacy zero-token judge scripts may omit per-criterion evidence. Shape that scripted response
|
|
63
|
+
// here, where the fake authors it, instead of teaching the shared LLM transport about adapter ids.
|
|
64
|
+
if (key === "judge" && prompt.startsWith("TICKMARKR-JUDGE") && val && typeof val === "object" && !Array.isArray(val)) {
|
|
53
65
|
const verdict = val;
|
|
54
66
|
if (verdict.pass === true && Array.isArray(verdict.criteria) && verdict.criteria.length === 0) {
|
|
55
67
|
const items = prompt.match(/## Acceptance criteria \(judge\)\n([\s\S]*?)\n\n## Diff/)?.[1]
|
|
@@ -57,8 +69,31 @@ export class FakeAdapter {
|
|
|
57
69
|
val = { ...verdict, criteria: items.map((criterion) => ({ criterion, met: true, reason: "scripted fake pass" })) };
|
|
58
70
|
}
|
|
59
71
|
}
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
if (key === "judge" && isJudge && val && typeof val === "object" && !Array.isArray(val)) {
|
|
73
|
+
const verdict = val;
|
|
74
|
+
const line = /```diff\n([\s\S]*?)```/.exec(prompt)?.[1].split("\n").find((candidate) => candidate.trim());
|
|
75
|
+
if (line && Array.isArray(verdict.criteria)) {
|
|
76
|
+
val = {
|
|
77
|
+
...verdict,
|
|
78
|
+
criteria: verdict.criteria.map((row) => row && typeof row === "object" && !("evidence" in row) ? { ...row, evidence: line } : row),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// The configured fake adapter binds the nonce read from this prompt before one object is emitted.
|
|
83
|
+
// Renamed subclasses model other CLIs and retain their own producer contract. Missing nonces stay
|
|
84
|
+
// missing so the classifier can distinguish silence from participation.
|
|
85
|
+
if (this.id === "fake" && nonce && val && typeof val === "object" && !Array.isArray(val)) {
|
|
86
|
+
const { nonce: _scriptedNonce, ...verdict } = val;
|
|
87
|
+
val = { nonce, ...verdict };
|
|
88
|
+
}
|
|
89
|
+
// Concurrent gates share one FakeAdapter script. A nonce-specific response path keeps one call's
|
|
90
|
+
// responder-authored bytes from being replaced by a sibling between command construction and cat.
|
|
91
|
+
const responseNonce = this.id === "fake" ? nonce : "";
|
|
92
|
+
const p = join(dirname(this.scriptPath), `${key}${responseNonce ? `-${responseNonce}` : ""}.json`);
|
|
93
|
+
const json = JSON.stringify(val ?? {}, null, 1)
|
|
94
|
+
.split("\n").map((line) => line.trim()).join(" ")
|
|
95
|
+
.replace(/^\{ /, "{").replace(/ \}$/, "}");
|
|
96
|
+
writeFileSync(p, json);
|
|
62
97
|
return p;
|
|
63
98
|
};
|
|
64
99
|
return `bash -c 'grep -Eq "TICKMARKR-(JUDGE|SCOPE)" ${shq(promptFile)} && cat ${shq(serve("judge"))}; grep -q TICKMARKR-REVIEW ${shq(promptFile)} && cat ${shq(serve("review"))}; grep -q TICKMARKR-CONSULT ${shq(promptFile)} && cat ${shq(serve("consult"))}; true'`;
|
|
@@ -111,7 +146,10 @@ export class FakeAdapter {
|
|
|
111
146
|
// parseWorkerResult(output, nonce) succeeds exactly as before the nonce existed
|
|
112
147
|
nonceFor(promptFile) {
|
|
113
148
|
try {
|
|
114
|
-
|
|
149
|
+
const prompt = readFileSync(promptFile, "utf8");
|
|
150
|
+
return /VERDICT_NONCE:\s*([0-9a-f]+)/i.exec(prompt)?.[1]
|
|
151
|
+
?? /TICKMARKR_RESULT_([0-9a-z]+)/.exec(prompt)?.[1]
|
|
152
|
+
?? "";
|
|
115
153
|
}
|
|
116
154
|
catch {
|
|
117
155
|
return "";
|
|
@@ -1,14 +1,31 @@
|
|
|
1
|
-
import { type TickmarkrConfig } from "../config/config.js";
|
|
1
|
+
import { type TickmarkrConfig, type Tier } from "../config/config.js";
|
|
2
2
|
import type { Task } from "../graph/schema.js";
|
|
3
3
|
import { type AuthHealth, type WorkerAdapter } from "./types.js";
|
|
4
|
+
import { type CatalogModelEvidence, type CatalogReadResult } from "./catalog-remote.js";
|
|
4
5
|
export declare const SEED_STAMPED = "2026-07-09";
|
|
5
6
|
export declare const MODEL_STALE_DAYS = 30;
|
|
6
7
|
export declare const ttyVisual: () => boolean;
|
|
7
|
-
|
|
8
|
+
export interface CatalogModelAdvisory {
|
|
9
|
+
coverage: "covered" | "uncovered";
|
|
10
|
+
evidence?: CatalogModelEvidence;
|
|
11
|
+
suggestion?: {
|
|
12
|
+
tier: Tier;
|
|
13
|
+
kind: "inference";
|
|
14
|
+
basis: "intelligence" | "price";
|
|
15
|
+
provenanceNote: string;
|
|
16
|
+
};
|
|
17
|
+
display: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function catalogModelAdvisory(cfg: TickmarkrConfig, catalog: CatalogReadResult, adapter: string, model: string, resolvedModel?: string): CatalogModelAdvisory;
|
|
20
|
+
/**
|
|
21
|
+
* Whether the doctor should add its optional window column. Keep non-TTY default output stable for
|
|
22
|
+
* machine consumers; an interactive seed-only matrix shows T14's fleet windows, and any explicit
|
|
23
|
+
* non-seed/overridden window keeps the historical operator-configured column behavior.
|
|
24
|
+
*/
|
|
8
25
|
export declare function hasWindowsConfig(cfg: TickmarkrConfig): boolean;
|
|
9
26
|
export declare function declaredModelWindow(cfg: TickmarkrConfig, adapter: string, model: string): number | undefined;
|
|
10
|
-
/**
|
|
11
|
-
export declare function estimateTaskPayloadTokens(task: Task, repoRoot: string, feedback?: string): number;
|
|
27
|
+
/** A numeric estimate exists only when every payload path is measurable from the worker-visible tree. */
|
|
28
|
+
export declare function estimateTaskPayloadTokens(task: Task, repoRoot: string, feedback?: string): number | undefined;
|
|
12
29
|
export type RoutedAssignment = {
|
|
13
30
|
taskId: string;
|
|
14
31
|
adapter: string;
|
|
@@ -28,7 +45,10 @@ export declare function formatModelAuthLine(excluded: {
|
|
|
28
45
|
reason: string;
|
|
29
46
|
probedAt: string;
|
|
30
47
|
}[], tty?: boolean, stateDir?: string): string;
|
|
31
|
-
export declare function suggestOverlay(cfg: TickmarkrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], stateDir?: string
|
|
48
|
+
export declare function suggestOverlay(cfg: TickmarkrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], stateDir?: string, opts?: {
|
|
49
|
+
catalog?: CatalogReadResult;
|
|
50
|
+
resolvedModel?: (adapter: string, model: string) => string | undefined;
|
|
51
|
+
}): string;
|
|
32
52
|
/** Unclassified models surfaced for fleet screen 2 (doctor matrix math, no tier fabrication). */
|
|
33
53
|
export declare function fleetUnclassifiedModels(cfg: TickmarkrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[]): {
|
|
34
54
|
adapter: string;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { DEFAULT_CONFIG, TIER_RANK } from "../config/config.js";
|
|
4
5
|
import { buildTaskPrompt } from "./prompt.js";
|
|
5
6
|
import { MODEL_ID_RE } from "./types.js";
|
|
7
|
+
import { resolveCatalogModel } from "./catalog-remote.js";
|
|
6
8
|
export const SEED_STAMPED = "2026-07-09";
|
|
7
9
|
// knowledge past this age gets a "rerun tickmarkr doctor" nudge (BLOCKED_POLL_MS-style named constant).
|
|
8
10
|
export const MODEL_STALE_DAYS = 30;
|
|
@@ -15,41 +17,154 @@ const LINT_CAP = 5;
|
|
|
15
17
|
const TTY_LINT_CAP = 3;
|
|
16
18
|
const DEFAULT_STATE_DIR = ".tickmarkr";
|
|
17
19
|
const doctorJsonRef = (stateDir) => ` — see ${stateDir}/doctor.json`;
|
|
20
|
+
const DECLARED_SEED_PREFER_DISPOSITION = "no declared preference overrides it";
|
|
21
|
+
const LEGACY_DOCTOR_SEED_PREFER_DISPOSITION = "auto-prefer is routing around it";
|
|
18
22
|
export const ttyVisual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
19
23
|
// ponytail: chars/4 token heuristic — good enough for advisory plan lint; no tokenizer dep.
|
|
20
24
|
const CHARS_PER_TOKEN = 4;
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
const intelligenceTier = (index) => index >= 65 ? "frontier" : index >= 40 ? "mid" : "cheap";
|
|
26
|
+
const priceTier = (outputPerMtok) => outputPerMtok >= 12 ? "frontier" : outputPerMtok >= 2 ? "mid" : "cheap";
|
|
27
|
+
function catalogEvidenceNote(evidence, catalog) {
|
|
28
|
+
const cost = evidence.inputCostPerMtok !== undefined && evidence.outputCostPerMtok !== undefined
|
|
29
|
+
? `$${evidence.inputCostPerMtok}/$${evidence.outputCostPerMtok} per Mtok`
|
|
30
|
+
: "not reported";
|
|
31
|
+
const features = evidence.features.length ? evidence.features.join(",") : "none reported";
|
|
32
|
+
const intelligence = evidence.intelligenceIndex !== undefined
|
|
33
|
+
? `; Artificial Analysis Intelligence Index=${evidence.intelligenceIndex}${evidence.intelligenceIndexVersion ? ` (version ${evidence.intelligenceIndexVersion})` : ""}`
|
|
34
|
+
: "";
|
|
35
|
+
const freshness = catalog.stale ? `${catalog.source} stale cache` : catalog.source;
|
|
36
|
+
return `models.dev id=${evidence.modelId}; cost=${cost}; context=${evidence.contextWindow ?? "not reported"}; features=${features}${intelligence}; catalog=${freshness}; fetchedAt=${catalog.catalog.fetchedAt}`;
|
|
37
|
+
}
|
|
38
|
+
export function catalogModelAdvisory(cfg, catalog, adapter, model, resolvedModel) {
|
|
39
|
+
const entry = cfg.tiers[adapter];
|
|
40
|
+
const evidence = resolveCatalogModel(catalog.catalog, {
|
|
41
|
+
// A tier vendor is NULLABLE (config.ts:64) — null means "declared as having none at tier level, so
|
|
42
|
+
// each model must declare its own". `resolveCatalogModel` takes an optional provider HINT
|
|
43
|
+
// (catalog-remote.ts:218), and "no hint" is `undefined` there, so null normalizes to undefined rather
|
|
44
|
+
// than travelling as a third state. Neither T17 nor T19 could see this seam: their files[] are
|
|
45
|
+
// disjoint, so no ordering edge was derived, they dispatched 7ms apart, and each worktree was cut
|
|
46
|
+
// before the other's change existed. Both gates passed truthfully; only the merged pair fails to
|
|
47
|
+
// compile (OBS-371).
|
|
48
|
+
provider: entry?.vendor ?? undefined,
|
|
49
|
+
model,
|
|
50
|
+
...(resolvedModel ? { resolvedModel } : {}),
|
|
51
|
+
});
|
|
52
|
+
if (!evidence) {
|
|
53
|
+
return {
|
|
54
|
+
coverage: "uncovered",
|
|
55
|
+
display: `${model} — uncovered by ${catalog.source === "cache" ? "cached catalogs" : "vendored catalog"}; no tier suggestion`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const note = catalogEvidenceNote(evidence, catalog);
|
|
59
|
+
const basis = evidence.intelligenceIndex !== undefined
|
|
60
|
+
? { basis: "intelligence", tier: intelligenceTier(evidence.intelligenceIndex) }
|
|
61
|
+
: entry?.channel === "api" && evidence.outputCostPerMtok !== undefined
|
|
62
|
+
? { basis: "price", tier: priceTier(evidence.outputCostPerMtok) }
|
|
63
|
+
: undefined;
|
|
64
|
+
if (!basis) {
|
|
65
|
+
const reason = entry?.channel === "sub" && evidence.outputCostPerMtok !== undefined
|
|
66
|
+
? "subscription billing; no price-derived suggestion"
|
|
67
|
+
: "no tier suggestion from available evidence";
|
|
68
|
+
return { coverage: "covered", evidence, display: `${model} → ${note}; ${reason}` };
|
|
69
|
+
}
|
|
70
|
+
const provenanceNote = `SUGGESTED ${basis.tier} (${basis.basis} inference, not a measurement) — ${note}; operator confirmation required`;
|
|
71
|
+
return {
|
|
72
|
+
coverage: "covered",
|
|
73
|
+
evidence,
|
|
74
|
+
suggestion: { tier: basis.tier, kind: "inference", basis: basis.basis, provenanceNote },
|
|
75
|
+
display: `${model} → ${note}; ${provenanceNote}`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function hasAnyWindows(cfg) {
|
|
23
79
|
return Object.values(cfg.tiers).some((t) => t.windows && Object.keys(t.windows).length > 0);
|
|
24
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Whether the doctor should add its optional window column. Keep non-TTY default output stable for
|
|
83
|
+
* machine consumers; an interactive seed-only matrix shows T14's fleet windows, and any explicit
|
|
84
|
+
* non-seed/overridden window keeps the historical operator-configured column behavior.
|
|
85
|
+
*/
|
|
86
|
+
export function hasWindowsConfig(cfg) {
|
|
87
|
+
const hasOperatorWindow = Object.entries(cfg.tiers).some(([adapter, entry]) => Object.entries(entry.windows ?? {}).some(([model, window]) => DEFAULT_CONFIG.tiers[adapter]?.windows?.[model] !== window));
|
|
88
|
+
if (hasOperatorWindow)
|
|
89
|
+
return true;
|
|
90
|
+
const seedOnly = Object.keys(cfg.tiers).every((adapter) => adapter in DEFAULT_CONFIG.tiers);
|
|
91
|
+
return ttyVisual() && seedOnly && hasAnyWindows(cfg);
|
|
92
|
+
}
|
|
25
93
|
export function declaredModelWindow(cfg, adapter, model) {
|
|
26
94
|
return cfg.tiers[adapter]?.windows?.[model];
|
|
27
95
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
96
|
+
const GLOB_CHARS = /[*?{[]/;
|
|
97
|
+
// T13's visibility oracle is the committed base tree: workers are created from HEAD, not from the
|
|
98
|
+
// author's checkout or index. Keep --full-tree load-bearing for callers below the repository root.
|
|
99
|
+
function baseTreeAtHead(dir) {
|
|
100
|
+
const git = (...args) => spawnSync("git", ["-C", dir, ...args], { encoding: "utf8", maxBuffer: 1 << 28 });
|
|
101
|
+
const top = git("rev-parse", "--show-toplevel");
|
|
102
|
+
const tree = git("ls-tree", "--full-tree", "-r", "--name-only", "HEAD");
|
|
103
|
+
if (top.status !== 0 || tree.status !== 0 || typeof top.stdout !== "string" || typeof tree.stdout !== "string")
|
|
104
|
+
return undefined;
|
|
105
|
+
return { root: top.stdout.trim(), tracked: new Set(tree.stdout.split("\n").filter(Boolean)) };
|
|
106
|
+
}
|
|
107
|
+
function fileBytes(repoRoot, rel, tree = baseTreeAtHead(repoRoot)) {
|
|
108
|
+
if (GLOB_CHARS.test(rel))
|
|
109
|
+
return { status: "unreadable", reason: "glob" };
|
|
110
|
+
if (!tree)
|
|
111
|
+
return { status: "unreadable", reason: "base-tree-unavailable" };
|
|
112
|
+
const path = rel.replace(/\/+$/, "");
|
|
113
|
+
if (!tree.tracked.has(path)) {
|
|
114
|
+
if ([...tree.tracked].some((tracked) => tracked.startsWith(`${path}/`))) {
|
|
115
|
+
return { status: "unreadable", reason: "directory" };
|
|
116
|
+
}
|
|
117
|
+
return { status: "unreadable", reason: existsSync(join(tree.root, path)) ? "untracked" : "absent" };
|
|
39
118
|
}
|
|
119
|
+
// Read the blob size from HEAD too: an unstaged/staged checkout edit is not what the worker receives.
|
|
120
|
+
const size = spawnSync("git", ["-C", tree.root, "cat-file", "-s", `HEAD:${path}`], { encoding: "utf8" });
|
|
121
|
+
const bytes = Number.parseInt(typeof size.stdout === "string" ? size.stdout.trim() : "", 10);
|
|
122
|
+
return size.status === 0 && Number.isSafeInteger(bytes) && bytes >= 0
|
|
123
|
+
? { status: "measured", bytes }
|
|
124
|
+
: { status: "unreadable", reason: "base-tree-read-failed" };
|
|
40
125
|
}
|
|
41
|
-
|
|
42
|
-
export function estimateTaskPayloadTokens(task, repoRoot, feedback = "") {
|
|
126
|
+
function taskPayloadResolution(task, repoRoot, feedback) {
|
|
43
127
|
let bytes = buildTaskPrompt(task, feedback).length;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
128
|
+
const unreadable = [];
|
|
129
|
+
const tree = baseTreeAtHead(repoRoot);
|
|
130
|
+
const add = (field, path) => {
|
|
131
|
+
const measured = fileBytes(repoRoot, path, tree);
|
|
132
|
+
if (measured.status === "measured")
|
|
133
|
+
bytes += measured.bytes;
|
|
134
|
+
else
|
|
135
|
+
unreadable.push({ field, path, reason: measured.reason });
|
|
136
|
+
};
|
|
137
|
+
for (const path of task.context)
|
|
138
|
+
add("context", path);
|
|
139
|
+
for (const path of task.files)
|
|
140
|
+
add("files", path);
|
|
141
|
+
return { tokens: Math.ceil(bytes / CHARS_PER_TOKEN), unreadable };
|
|
142
|
+
}
|
|
143
|
+
/** A numeric estimate exists only when every payload path is measurable from the worker-visible tree. */
|
|
144
|
+
export function estimateTaskPayloadTokens(task, repoRoot, feedback = "") {
|
|
145
|
+
const estimate = taskPayloadResolution(task, repoRoot, feedback);
|
|
146
|
+
return estimate.unreadable.length === 0 ? estimate.tokens : undefined;
|
|
147
|
+
}
|
|
148
|
+
function unreadablePayloadLint(taskId, unreadable) {
|
|
149
|
+
const describe = ({ field, path, reason }) => {
|
|
150
|
+
const prefix = `${field} ${JSON.stringify(path)}`;
|
|
151
|
+
if (reason === "untracked")
|
|
152
|
+
return `${prefix} is present in the checkout but not in the base tree`;
|
|
153
|
+
if (reason === "absent")
|
|
154
|
+
return `${prefix} is absent from the base tree and checkout`;
|
|
155
|
+
if (reason === "glob")
|
|
156
|
+
return `${prefix} is a glob and cannot be measured`;
|
|
157
|
+
if (reason === "directory")
|
|
158
|
+
return `${prefix} is a directory and cannot be measured as one file`;
|
|
159
|
+
if (reason === "base-tree-unavailable")
|
|
160
|
+
return `${prefix} cannot be checked because the base tree is unavailable`;
|
|
161
|
+
return `${prefix} could not be read from the base tree`;
|
|
162
|
+
};
|
|
163
|
+
return `${taskId}: payload unreadable — ${unreadable.map(describe).join(", ")}; context-window comparison skipped`;
|
|
49
164
|
}
|
|
50
165
|
/** Advisory only — absent windows config or undeclared model window ⇒ no lint. */
|
|
51
166
|
export function contextWindowLints(tasks, assignments, cfg, repoRoot) {
|
|
52
|
-
if (!
|
|
167
|
+
if (!hasAnyWindows(cfg))
|
|
53
168
|
return [];
|
|
54
169
|
const byId = new Map(assignments.map((a) => [a.taskId, a]));
|
|
55
170
|
const lints = [];
|
|
@@ -60,9 +175,13 @@ export function contextWindowLints(tasks, assignments, cfg, repoRoot) {
|
|
|
60
175
|
const window = declaredModelWindow(cfg, a.adapter, a.model);
|
|
61
176
|
if (window === undefined)
|
|
62
177
|
continue;
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
65
|
-
lints.push(
|
|
178
|
+
const estimate = taskPayloadResolution(t, repoRoot, "");
|
|
179
|
+
if (estimate.unreadable.length > 0) {
|
|
180
|
+
lints.push(unreadablePayloadLint(t.id, estimate.unreadable));
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (estimate.tokens > window) {
|
|
184
|
+
lints.push(`${t.id}: payload ~${estimate.tokens} tokens exceeds ${a.adapter}:${a.model} window ${window}`);
|
|
66
185
|
}
|
|
67
186
|
}
|
|
68
187
|
return lints;
|
|
@@ -78,8 +197,7 @@ const adapterHasAuthedChannel = (adapterId, shape, cfg, health, adapters) => {
|
|
|
78
197
|
return true; // no per-model probe data — compat, not dead
|
|
79
198
|
return a.channels(cfg).some((c) => TIER_RANK[c.tier] >= TIER_RANK[minTier] && h.modelAuth?.[c.model]?.authed === true);
|
|
80
199
|
};
|
|
81
|
-
|
|
82
|
-
export function seedPreferLints(cfg, health, adapters, overlayPreferShapes = new Set()) {
|
|
200
|
+
function collectSeedPreferLints(cfg, health, adapters, overlayPreferShapes, disposition) {
|
|
83
201
|
const lints = [];
|
|
84
202
|
for (const shape of Object.keys(cfg.routing.map)) {
|
|
85
203
|
if (overlayPreferShapes.has(shape))
|
|
@@ -89,18 +207,23 @@ export function seedPreferLints(cfg, health, adapters, overlayPreferShapes = new
|
|
|
89
207
|
if (!cfg.tiers[adapterId])
|
|
90
208
|
continue;
|
|
91
209
|
if (!adapterHasAuthedChannel(adapterId, shape, cfg, health, adapters)) {
|
|
92
|
-
lints.push(`routing seed names dead adapter '${adapterId}' for shape '${shape}' —
|
|
210
|
+
lints.push(`routing seed names dead adapter '${adapterId}' for shape '${shape}' — ${disposition}`);
|
|
93
211
|
}
|
|
94
212
|
}
|
|
95
213
|
}
|
|
96
214
|
return lints;
|
|
97
215
|
}
|
|
216
|
+
// OBS-30 T2 / v1.86 T3: warn when a built-in seed prefer names an adapter with zero authed
|
|
217
|
+
// channels and no operator-declared preference overrides that seed for the shape.
|
|
218
|
+
export function seedPreferLints(cfg, health, adapters, overlayPreferShapes = new Set()) {
|
|
219
|
+
return collectSeedPreferLints(cfg, health, adapters, overlayPreferShapes, DECLARED_SEED_PREFER_DISPOSITION);
|
|
220
|
+
}
|
|
98
221
|
// v1.54 T3: dead-steering sweep — operator prefer entries (routing.map overlay shapes, review.prefer,
|
|
99
222
|
// consult.prefer) that can never match an installed channel are named at plan time; v1.53 T2 pins the
|
|
100
223
|
// no-match case as a silent no-op, which makes a typo invisible. Advisory only: reads config + doctor
|
|
101
|
-
// health (no live probes), never touches routing. Seed map prefers stay seedPreferLints' turf
|
|
102
|
-
//
|
|
103
|
-
//
|
|
224
|
+
// health (no live probes), never touches routing. Seed map prefers stay seedPreferLints' turf;
|
|
225
|
+
// mapShapes limits this sweep to operator-authored entries so a bare default fleet isn't double-linted.
|
|
226
|
+
// Entry grammar mirrors preferIndex: adapter | adapter:model (first colon).
|
|
104
227
|
export function preferEntryLints(cfg, health, mapShapes = new Set()) {
|
|
105
228
|
const lints = [];
|
|
106
229
|
const sweep = (surface, entries) => {
|
|
@@ -122,17 +245,15 @@ export function preferEntryLints(cfg, health, mapShapes = new Set()) {
|
|
|
122
245
|
sweep("consult.prefer", cfg.consult.prefer);
|
|
123
246
|
return lints;
|
|
124
247
|
}
|
|
125
|
-
// Diffs detected models (doctor.json) against configured tiers, both directions, per adapter
|
|
248
|
+
// Diffs detected models (doctor.json) against configured tiers, both directions, per installed adapter.
|
|
126
249
|
// No ` ! ` prefix here — the consumer (doctor rows / plan lints) owns that. Pre-v1.5 doctor.json (models:[], no
|
|
127
250
|
// modelsDetectedAt) is the compat baseline: `?.`/`?? []` everywhere, no zod (would reject old files).
|
|
128
251
|
export function modelLints(cfg, health, adapters, opts) {
|
|
129
252
|
const cap = opts?.tty ? TTY_LINT_CAP : LINT_CAP;
|
|
130
253
|
const doctorRef = opts?.tty ? doctorJsonRef(opts.stateDir ?? DEFAULT_STATE_DIR) : "";
|
|
131
254
|
const lints = [];
|
|
132
|
-
for (const
|
|
133
|
-
const
|
|
134
|
-
if (!adapter)
|
|
135
|
-
continue; // fake/overlay-only tier entry with no adapter — nothing to diff against
|
|
255
|
+
for (const adapter of adapters) {
|
|
256
|
+
const id = adapter.id;
|
|
136
257
|
if (!adapter.listModels) {
|
|
137
258
|
lints.push(`${id}: no model-list surface — seeds stamped ${SEED_STAMPED}; verify manually`);
|
|
138
259
|
continue;
|
|
@@ -144,7 +265,7 @@ export function modelLints(cfg, health, adapters, opts) {
|
|
|
144
265
|
lints.push(`${id}: no detection data — run tickmarkr doctor`);
|
|
145
266
|
continue; // no data to diff or age
|
|
146
267
|
}
|
|
147
|
-
const configured = Object.keys(cfg.tiers[id]
|
|
268
|
+
const configured = Object.keys(cfg.tiers[id]?.models ?? {});
|
|
148
269
|
for (const model of configured) {
|
|
149
270
|
if (!detected.includes(model)) {
|
|
150
271
|
lints.push(`${id}: tiers lists ${model} — CLI no longer reports it; tombstone it (${model}: null overlay) or verify the id`);
|
|
@@ -163,7 +284,13 @@ export function modelLints(cfg, health, adapters, opts) {
|
|
|
163
284
|
lints.push(`${id}: model knowledge is ${days} days old — rerun tickmarkr doctor`);
|
|
164
285
|
}
|
|
165
286
|
}
|
|
166
|
-
|
|
287
|
+
// v1.34 T3 byte-pins non-TTY doctor output. Doctor consumers are the ones that provide stateDir;
|
|
288
|
+
// keep that machine-facing compatibility surface stable while plan and direct lint callers state
|
|
289
|
+
// the current declared-preference mechanism truthfully.
|
|
290
|
+
const seedDisposition = opts?.stateDir !== undefined && opts.tty !== true
|
|
291
|
+
? LEGACY_DOCTOR_SEED_PREFER_DISPOSITION
|
|
292
|
+
: DECLARED_SEED_PREFER_DISPOSITION;
|
|
293
|
+
lints.push(...collectSeedPreferLints(cfg, health, adapters, opts?.overlayPreferShapes ?? new Set(), seedDisposition));
|
|
167
294
|
return lints;
|
|
168
295
|
}
|
|
169
296
|
// T2/T6: one lint per exclusion, naming the probe reason and date. TTY truncates reasons to 60 chars and
|
|
@@ -183,17 +310,17 @@ export function formatModelAuthLine(excluded, tty, stateDir = DEFAULT_STATE_DIR)
|
|
|
183
310
|
// machine never fabricates one — auto-tiering reopens the NaN-routing class). Removals render as LIVE
|
|
184
311
|
// `<id>: null` tombstones (deepMerge deletes the key). Pure function: no fs, no routing contact.
|
|
185
312
|
// Returns "" when no adapter has a delta. Mirrors modelLints' per-adapter guards exactly.
|
|
186
|
-
export function suggestOverlay(cfg, health, adapters, stateDir = DEFAULT_STATE_DIR) {
|
|
313
|
+
export function suggestOverlay(cfg, health, adapters, stateDir = DEFAULT_STATE_DIR, opts = {}) {
|
|
187
314
|
const blocks = [];
|
|
188
|
-
for (const
|
|
189
|
-
const
|
|
190
|
-
if (!adapter
|
|
191
|
-
continue; // no
|
|
315
|
+
for (const adapter of adapters) {
|
|
316
|
+
const id = adapter.id;
|
|
317
|
+
if (!adapter.listModels)
|
|
318
|
+
continue; // no list surface → nothing to diff (mirror modelLints)
|
|
192
319
|
const h = health[id];
|
|
193
320
|
const detected = h?.models ?? [];
|
|
194
321
|
if (detected.length === 0)
|
|
195
322
|
continue; // no detection data → don't guess a delta
|
|
196
|
-
const configured = Object.keys(cfg.tiers[id]
|
|
323
|
+
const configured = Object.keys(cfg.tiers[id]?.models ?? {});
|
|
197
324
|
const date = h?.modelsDetectedAt?.split("T")[0]; // best-effort day stamp
|
|
198
325
|
const detNote = date ? ` (detected ${date})` : "";
|
|
199
326
|
const lines = [];
|
|
@@ -220,11 +347,17 @@ export function suggestOverlay(cfg, health, adapters, stateDir = DEFAULT_STATE_D
|
|
|
220
347
|
for (const model of detected) {
|
|
221
348
|
if (configured.includes(model) || !MODEL_ID_RE.test(model) || LINT_VARIANT_RE.test(model))
|
|
222
349
|
continue;
|
|
223
|
-
if (!cfgPrefixes.has(providerPrefix(model)) && !cfgCanon.has(canonical(model))) {
|
|
350
|
+
if (configured.length > 0 && !cfgPrefixes.has(providerPrefix(model)) && !cfgCanon.has(canonical(model))) {
|
|
224
351
|
omitted++;
|
|
225
352
|
continue;
|
|
226
353
|
}
|
|
227
|
-
|
|
354
|
+
const advisory = opts.catalog
|
|
355
|
+
? catalogModelAdvisory(cfg, opts.catalog, id, model, opts.resolvedModel?.(id, model))
|
|
356
|
+
: undefined;
|
|
357
|
+
const guidance = advisory?.suggestion
|
|
358
|
+
? `provenance note (operator confirmation required): ${advisory.suggestion.provenanceNote}; choose a tier, then uncomment`
|
|
359
|
+
: `classify per benchmark policy (AA Index + SWE-bench Pro, dated), then uncomment${advisory ? ` — ${advisory.display}` : ""}`;
|
|
360
|
+
lines.push(` # ${model}: ??? #${date ? ` detected ${date} —` : ""} ${guidance}`);
|
|
228
361
|
}
|
|
229
362
|
if (omitted)
|
|
230
363
|
lines.push(` # (+${omitted} other detected id${omitted === 1 ? "" : "s"} not related to your configured models — see ${stateDir}/doctor.json)`);
|
|
@@ -258,14 +391,15 @@ function referenceWarning(cfg, adapterId, model) {
|
|
|
258
391
|
/** Unclassified models surfaced for fleet screen 2 (doctor matrix math, no tier fabrication). */
|
|
259
392
|
export function fleetUnclassifiedModels(cfg, health, adapters) {
|
|
260
393
|
const out = [];
|
|
261
|
-
for (const
|
|
262
|
-
|
|
263
|
-
continue;
|
|
394
|
+
for (const adapter of adapters) {
|
|
395
|
+
const id = adapter.id;
|
|
264
396
|
const h = health[id];
|
|
397
|
+
if (!h?.installed)
|
|
398
|
+
continue;
|
|
265
399
|
const detected = h?.models ?? [];
|
|
266
400
|
if (!detected.length)
|
|
267
401
|
continue;
|
|
268
|
-
const configured = new Set(Object.keys(cfg.tiers[id]
|
|
402
|
+
const configured = new Set(Object.keys(cfg.tiers[id]?.models ?? {}));
|
|
269
403
|
const date = h?.modelsDetectedAt?.split("T")[0];
|
|
270
404
|
for (const model of detected) {
|
|
271
405
|
if (configured.has(model) || LINT_VARIANT_RE.test(model))
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
declare const ModelWindowClaimSchema: z.ZodObject<{
|
|
3
|
+
modelId: z.ZodString;
|
|
4
|
+
window: z.ZodNumber;
|
|
5
|
+
source: z.ZodString;
|
|
6
|
+
readDate: z.ZodISODate;
|
|
7
|
+
}, z.core.$strict>;
|
|
8
|
+
export type ModelWindowClaim = Readonly<z.infer<typeof ModelWindowClaimSchema>>;
|
|
9
|
+
export type ModelWindowResolution = {
|
|
10
|
+
status: "declared";
|
|
11
|
+
claim: ModelWindowClaim;
|
|
12
|
+
} | {
|
|
13
|
+
status: "unknown";
|
|
14
|
+
modelId: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Validate cited model-window claims before production code can consume them.
|
|
18
|
+
* These values are vendor-published claims, not measurements made by tickmarkr.
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadModelWindowClaims(input: unknown): readonly ModelWindowClaim[];
|
|
21
|
+
/** The closed, production-readable catalog of cited context-window claims. */
|
|
22
|
+
export declare const CITED_MODEL_WINDOWS: readonly Readonly<{
|
|
23
|
+
modelId: string;
|
|
24
|
+
window: number;
|
|
25
|
+
source: string;
|
|
26
|
+
readDate: string;
|
|
27
|
+
}>[];
|
|
28
|
+
export declare function resolveModelWindowClaim(modelId: string, input?: unknown): ModelWindowResolution;
|
|
29
|
+
/** Fail when either side contains a model id absent from the other side. */
|
|
30
|
+
export declare function assertModelWindowClaimsMatchSeededModels(seededModelIds: readonly string[], input?: unknown): void;
|
|
31
|
+
export {};
|