tickmarkr 2.6.3 → 2.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/dist/adapters/catalog-remote.d.ts +23 -0
- package/dist/adapters/catalog-remote.js +38 -0
- package/dist/adapters/claude-code.d.ts +1 -1
- package/dist/adapters/claude-code.js +4 -1
- package/dist/adapters/codex-commit-check.d.ts +25 -0
- package/dist/adapters/codex-commit-check.js +108 -0
- package/dist/adapters/codex.d.ts +2 -1
- package/dist/adapters/codex.js +26 -5
- package/dist/adapters/model-discovery-check.d.ts +33 -0
- package/dist/adapters/model-discovery-check.js +123 -0
- package/dist/adapters/model-lints.d.ts +2 -1
- package/dist/adapters/model-lints.js +52 -1
- package/dist/adapters/model-windows.d.ts +2 -1
- package/dist/adapters/model-windows.js +18 -7
- package/dist/adapters/prompt.js +3 -2
- package/dist/adapters/types.d.ts +13 -0
- package/dist/cli/commands/approve.js +37 -6
- package/dist/cli/commands/doctor.d.ts +4 -0
- package/dist/cli/commands/doctor.js +20 -3
- package/dist/cli/commands/fleet.d.ts +10 -1
- package/dist/cli/commands/fleet.js +279 -95
- package/dist/cli/commands/init.js +10 -4
- package/dist/cli/commands/plan.js +65 -4
- package/dist/cli/commands/report.d.ts +39 -2
- package/dist/cli/commands/report.js +227 -10
- package/dist/cli/commands/resume.js +2 -4
- package/dist/cli/commands/run.d.ts +11 -0
- package/dist/cli/commands/run.js +33 -5
- package/dist/cli/commands/status.js +30 -3
- package/dist/cli/commands/verify.d.ts +1 -1
- package/dist/cli/commands/verify.js +356 -123
- package/dist/compile/native.js +130 -0
- package/dist/config/config.d.ts +25 -7
- package/dist/config/config.js +92 -11
- package/dist/config/fleet-overlay.d.ts +2 -1
- package/dist/config/fleet-overlay.js +77 -4
- package/dist/drivers/orca.d.ts +19 -1
- package/dist/drivers/orca.js +161 -31
- package/dist/gates/cache.d.ts +4 -0
- package/dist/gates/cache.js +10 -2
- package/dist/gates/llm.d.ts +4 -1
- package/dist/gates/llm.js +6 -1
- package/dist/gates/review.js +8 -3
- package/dist/gates/run-gates.d.ts +3 -2
- package/dist/gates/run-gates.js +91 -50
- package/dist/route/router.d.ts +11 -0
- package/dist/route/router.js +29 -8
- package/dist/run/daemon.js +158 -22
- package/dist/run/git.js +9 -0
- package/dist/run/journal.d.ts +101 -1
- package/dist/run/journal.js +352 -2
- package/dist/run/launchservices-check.d.ts +56 -0
- package/dist/run/launchservices-check.js +117 -0
- package/dist/run/operator-state.d.ts +22 -2
- package/dist/run/operator-state.js +20 -3
- package/dist/run/title-environment.d.ts +45 -0
- package/dist/run/title-environment.js +127 -0
- package/dist/run/title-preload.cjs +38 -0
- package/dist/run/title-preload.d.cts +1 -0
- package/dist/tui/cockpit/board.js +12 -6
- package/dist/tui/cockpit/keys.d.ts +8 -0
- package/dist/tui/cockpit/keys.js +4 -1
- package/dist/tui/cockpit/layout.d.ts +2 -2
- package/dist/tui/cockpit/layout.js +2 -2
- package/dist/tui/cockpit/live-runtime.js +46 -20
- package/dist/tui/cockpit/live-store.d.ts +30 -1
- package/dist/tui/cockpit/live-store.js +134 -18
- package/dist/tui/cockpit/log-view.d.ts +28 -0
- package/dist/tui/cockpit/log-view.js +60 -0
- package/dist/tui/cockpit/run-view.js +4 -0
- package/dist/tui/cockpit/shell.d.ts +2 -1
- package/dist/tui/cockpit/shell.js +6 -4
- package/dist/tui/ink/fleet-app.d.ts +12 -6
- package/dist/tui/ink/fleet-app.js +17 -11
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +33 -3
- package/skills/tickmarkr-loop/SKILL.md +55 -5
- package/skills/tickmarkr-overseer/SKILL.md +83 -8
package/README.md
CHANGED
|
@@ -183,8 +183,16 @@ can enact the release at its next task boundary; a different live run must end b
|
|
|
183
183
|
one resumes. Keep any resume refusal and its remediation visible, including a deny/prefer
|
|
184
184
|
config conflict; repair the source/config as directed, never edit the compiled graph to
|
|
185
185
|
force success. After resume, CURRENT TIP is PENDING until fresh evidence arrives. Completion
|
|
186
|
-
requires the latest run-end, a nonfailed known tip result,
|
|
187
|
-
`blocked` and `pending` buckets
|
|
186
|
+
requires the latest run-end, a nonfailed known tip result, empty `failed`, `human`,
|
|
187
|
+
`blocked` and `pending` buckets, and owed checks outstanding empty AND known in CURRENT
|
|
188
|
+
`tickmarkr status <runId>` (`outstanding 0`); the completed case has 3/3 recorded merges.
|
|
189
|
+
Empty buckets alone are not completion (D-660): 3/3 merged with every bucket empty and the
|
|
190
|
+
tip passed, after the operator waived T2's review, leaves one accepted-risk review check owed.
|
|
191
|
+
`run`/`resume` exit 0 on execution alone, their final line says
|
|
192
|
+
`execution complete; outstanding 1 (T2 review)` rather than `verified`, and status reads
|
|
193
|
+
`outstanding 1`. That run completes only when `tickmarkr verify --record <runId>` discharges
|
|
194
|
+
the check and CURRENT status reads `outstanding 0` — a discharge after run-end counts, while the
|
|
195
|
+
run-end record keeps its old count; `outstanding unknown` never completes. An unrelated
|
|
188
196
|
graph says “not comparable” and supplies no borrowed denominator. Historical GATES RAN
|
|
189
197
|
does not establish current completion.
|
|
190
198
|
|
|
@@ -18,6 +18,8 @@ export interface CatalogReadResult {
|
|
|
18
18
|
source: "cache" | "vendored";
|
|
19
19
|
stale: boolean;
|
|
20
20
|
warning?: string;
|
|
21
|
+
/** v2.6.5 T9: `adapter:model` keys a failed post-discovery refresh left at UNKNOWN coverage, never "uncovered". */
|
|
22
|
+
coverageUnknown?: readonly string[];
|
|
21
23
|
}
|
|
22
24
|
export interface CatalogModelEvidence {
|
|
23
25
|
modelId: string;
|
|
@@ -62,6 +64,27 @@ export interface RefreshCatalogResult {
|
|
|
62
64
|
warning?: string;
|
|
63
65
|
legs: CatalogLegResult[];
|
|
64
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* v2.6.5 T9 (H): records persisted from cited sources, never inferred from a model name or an API
|
|
69
|
+
* price. Prices are normal API USD per million tokens — a subscription seat is never billed from them.
|
|
70
|
+
* The tier is CONTINUITY of the shipped alias seeds (config.ts DEFAULT_CONFIG: sonnet=mid,
|
|
71
|
+
* opus=frontier), not a benchmark ruling: doctor prints it as an inference for operator confirmation
|
|
72
|
+
* and the configured (Fleet-confirmed) tier stays the routing authority.
|
|
73
|
+
*/
|
|
74
|
+
export interface SourcedIdentityRecord {
|
|
75
|
+
identity: string;
|
|
76
|
+
alias: "sonnet" | "opus";
|
|
77
|
+
inputCostPerMtok: number;
|
|
78
|
+
outputCostPerMtok: number;
|
|
79
|
+
contextWindow: number;
|
|
80
|
+
outputWindow: number;
|
|
81
|
+
tier: "mid" | "frontier";
|
|
82
|
+
tierBasis: string;
|
|
83
|
+
sources: readonly string[];
|
|
84
|
+
fetchedAt: string;
|
|
85
|
+
}
|
|
86
|
+
export declare const SOURCED_IDENTITY_RECORDS: readonly SourcedIdentityRecord[];
|
|
87
|
+
export declare const sourcedIdentityRecord: (identity: string | undefined) => SourcedIdentityRecord | undefined;
|
|
65
88
|
export declare const formatCatalogRefreshLegs: (legs: readonly CatalogLegResult[]) => string;
|
|
66
89
|
export declare const catalogCachePath: (repoRoot: string) => string;
|
|
67
90
|
/**
|
|
@@ -13,6 +13,40 @@ export const CATALOG_CACHE_MAX_AGE_MS = 7 * 86_400_000;
|
|
|
13
13
|
export const CATALOG_REFRESH_TIMEOUT_MS = 10_000;
|
|
14
14
|
const ARTIFICIAL_ANALYSIS_PAGE_SIZE = 100;
|
|
15
15
|
const ARTIFICIAL_ANALYSIS_MAX_PAGES = 100;
|
|
16
|
+
export const SOURCED_IDENTITY_RECORDS = [
|
|
17
|
+
{
|
|
18
|
+
identity: "claude-sonnet-5-5",
|
|
19
|
+
alias: "sonnet",
|
|
20
|
+
inputCostPerMtok: 2,
|
|
21
|
+
outputCostPerMtok: 10,
|
|
22
|
+
contextWindow: 1_000_000,
|
|
23
|
+
outputWindow: 128_000,
|
|
24
|
+
tier: "mid",
|
|
25
|
+
tierBasis: "continuity of the existing sonnet=mid configuration, not derived from the name or price",
|
|
26
|
+
sources: [
|
|
27
|
+
"https://raw.githubusercontent.com/anomalyco/models.dev/dev/providers/anthropic/models/claude-sonnet-5-5.toml",
|
|
28
|
+
"https://raw.githubusercontent.com/anomalyco/models.dev/dev/models/anthropic/claude-sonnet-5-5.toml",
|
|
29
|
+
"https://platform.claude.com/docs/en/models/sonnet-5-5/overview",
|
|
30
|
+
],
|
|
31
|
+
fetchedAt: "2026-09-30",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
identity: "claude-opus-5-5",
|
|
35
|
+
alias: "opus",
|
|
36
|
+
inputCostPerMtok: 4,
|
|
37
|
+
outputCostPerMtok: 20,
|
|
38
|
+
contextWindow: 1_000_000,
|
|
39
|
+
outputWindow: 128_000,
|
|
40
|
+
tier: "frontier",
|
|
41
|
+
tierBasis: "continuity of the existing opus=frontier configuration, not derived from the name or price",
|
|
42
|
+
sources: [
|
|
43
|
+
"https://raw.githubusercontent.com/anomalyco/models.dev/dev/providers/anthropic/models/claude-opus-5-5.toml",
|
|
44
|
+
"https://raw.githubusercontent.com/anomalyco/models.dev/dev/models/anthropic/claude-opus-5-5.toml",
|
|
45
|
+
],
|
|
46
|
+
fetchedAt: "2026-09-30",
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
export const sourcedIdentityRecord = (identity) => SOURCED_IDENTITY_RECORDS.find((r) => r.identity === identity);
|
|
16
50
|
const VENDORED_CATALOG = {
|
|
17
51
|
schemaVersion: 1,
|
|
18
52
|
// Package fallback copied from https://models.dev/api.json on 2026-08-05. It is deliberately
|
|
@@ -26,6 +60,10 @@ const VENDORED_CATALOG = {
|
|
|
26
60
|
"claude-fable-5-1": { id: "claude-fable-5-1", cost: { input: 10, output: 50 }, limit: { context: 1_000_000, output: 128_000 }, reasoning: true, tool_call: true, structured_output: true, attachment: true },
|
|
27
61
|
"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 },
|
|
28
62
|
"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 },
|
|
63
|
+
// v2.6.5 T9: the sourced 5-5 records above, so a refresh-less resolution still covers them.
|
|
64
|
+
...Object.fromEntries(SOURCED_IDENTITY_RECORDS.map((r) => [r.identity, {
|
|
65
|
+
id: r.identity, cost: { input: r.inputCostPerMtok, output: r.outputCostPerMtok }, limit: { context: r.contextWindow, output: r.outputWindow }, reasoning: true, tool_call: true, structured_output: true, attachment: true,
|
|
66
|
+
}])),
|
|
29
67
|
"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 },
|
|
30
68
|
},
|
|
31
69
|
},
|
|
@@ -2,7 +2,7 @@ import { type AuthHealth, type TrustDialog, type WorkerAdapter } from "./types.j
|
|
|
2
2
|
export declare const CLAUDE_ALIAS_IDENTITY_STAMPS: {
|
|
3
3
|
readonly fable: "claude-fable-5-1";
|
|
4
4
|
readonly opus: "claude-opus-4-8";
|
|
5
|
-
readonly sonnet: "claude-sonnet-5";
|
|
5
|
+
readonly sonnet: "claude-sonnet-5-5";
|
|
6
6
|
readonly haiku: "claude-haiku-4-5-20251001";
|
|
7
7
|
};
|
|
8
8
|
export type ClaudeAlias = keyof typeof CLAUDE_ALIAS_IDENTITY_STAMPS;
|
|
@@ -32,7 +32,10 @@ export const CLAUDE_ALIAS_IDENTITY_STAMPS = {
|
|
|
32
32
|
// OBS-871, 2026-09-03: Fable's floating alias now resolves to the 5.1 benchmark identity.
|
|
33
33
|
fable: "claude-fable-5-1",
|
|
34
34
|
opus: "claude-opus-4-8",
|
|
35
|
-
|
|
35
|
+
// v2.6.5 T9 (H, PREMISES-v265 P31): the author's live probeClaudeAliasIdentity call on 2026-09-30
|
|
36
|
+
// resolved sonnet to claude-sonnet-5-5. Its sourced price/window and continuity tier basis live in
|
|
37
|
+
// SOURCED_IDENTITY_RECORDS (catalog-remote.ts); this re-stamp re-dates no tier and adopts nothing.
|
|
38
|
+
sonnet: "claude-sonnet-5-5",
|
|
36
39
|
haiku: "claude-haiku-4-5-20251001",
|
|
37
40
|
};
|
|
38
41
|
// v1.75 T2 / OBS-137: current Claude Code workspace-trust prompt (2.1.218). The full question
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AuthHealth, WorkerAdapter } from "./types.js";
|
|
2
|
+
export type CodexCommitStatus = "protected" | "allowed" | "unknown";
|
|
3
|
+
export interface CodexCommitProbe {
|
|
4
|
+
worktree: string;
|
|
5
|
+
commonDir: string;
|
|
6
|
+
control: string;
|
|
7
|
+
lock: string;
|
|
8
|
+
token: string;
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
}
|
|
11
|
+
/** Runs the write script inside the Codex sandbox and resolves its stdout. Injected in tests. */
|
|
12
|
+
export type CodexSandbox = (probe: CodexCommitProbe) => Promise<string>;
|
|
13
|
+
export interface CodexCommitResult {
|
|
14
|
+
status: CodexCommitStatus;
|
|
15
|
+
detail: string;
|
|
16
|
+
}
|
|
17
|
+
export declare const codexSandboxArgs: (p: CodexCommitProbe) => string[];
|
|
18
|
+
export declare const codexSandbox: CodexSandbox;
|
|
19
|
+
export declare function probeCodexCommit(repoRoot: string, sandbox?: CodexSandbox, signal?: AbortSignal): Promise<CodexCommitResult>;
|
|
20
|
+
/** Doctor's one call: gated on the REAL codex adapter object (a codex-id stub gets no probe and no output
|
|
21
|
+
* unless a test injects a sandbox — the kimiTurnEnabled rule). Mutates only the health record doctor owns. */
|
|
22
|
+
export declare function recordCodexCommit(adapters: WorkerAdapter[], health: Record<string, AuthHealth>, cwd: string, sandbox?: CodexSandbox): Promise<void>;
|
|
23
|
+
/** Plan's headline beneath a Codex worker row; null for every other worker and for an allowed probe.
|
|
24
|
+
* An absent field is unknown, but only the real adapter earns the line — stubs stay byte-identical. */
|
|
25
|
+
export declare function codexCommitHeadline(worker: string, adapters: WorkerAdapter[], health: Record<string, AuthHealth>): string | null;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { existsSync, mkdtempSync, readFileSync, rmdirSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { basename, dirname, join } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { codex } from "./codex.js";
|
|
8
|
+
const exec = promisify(execFile);
|
|
9
|
+
const git = async (cwd, ...args) => (await exec("git", args, { cwd })).stdout.trim();
|
|
10
|
+
// $1 control, $2 lock, $3 token. noclobber (set -C) creates the lock the way git does — never over an existing one.
|
|
11
|
+
const SCRIPT = `printf %s "$3" > "$1" && echo control=ok || echo control=fail
|
|
12
|
+
e=$( (set -C; printf %s "$3" > "$2") 2>&1 ) && echo lock=ok || echo "lock=fail $e"`;
|
|
13
|
+
// The grant names the git common dir (the worker launch grant) AND the exact linked metadata directory that
|
|
14
|
+
// holds index.lock, so a denial is protection despite an exact grant — never a grant this probe forgot.
|
|
15
|
+
export const codexSandboxArgs = (p) => [
|
|
16
|
+
"sandbox", "-c", 'sandbox_mode="workspace-write"',
|
|
17
|
+
"-c", `sandbox_workspace_write.writable_roots=${JSON.stringify([p.commonDir, dirname(p.lock)])}`,
|
|
18
|
+
"--", "sh", "-c", SCRIPT, "sh", p.control, p.lock, p.token,
|
|
19
|
+
];
|
|
20
|
+
export const codexSandbox = async (p) => (await exec("codex", codexSandboxArgs(p), { cwd: p.worktree, timeout: 20_000, signal: p.signal })).stdout;
|
|
21
|
+
const holds = (path, token) => {
|
|
22
|
+
try {
|
|
23
|
+
return readFileSync(path, "utf8") === token;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const unknown = (detail) => ({ status: "unknown", detail });
|
|
30
|
+
export async function probeCodexCommit(repoRoot, sandbox = codexSandbox, signal) {
|
|
31
|
+
const token = randomUUID();
|
|
32
|
+
const parent = mkdtempSync(join(tmpdir(), "tickmarkr-codex-commit-"));
|
|
33
|
+
// git names the fixture's metadata directory after the checkout's basename; a unique one makes that
|
|
34
|
+
// directory — and so the lock path — known BEFORE `worktree add`, which can fail after creating it
|
|
35
|
+
const worktree = join(parent, `tickmarkr-probe-${token}`);
|
|
36
|
+
let lock;
|
|
37
|
+
try {
|
|
38
|
+
const commonDir = await git(repoRoot, "rev-parse", "--path-format=absolute", "--git-common-dir");
|
|
39
|
+
lock = join(commonDir, "worktrees", basename(worktree), "index.lock");
|
|
40
|
+
await git(repoRoot, "worktree", "add", "--detach", worktree, "HEAD");
|
|
41
|
+
if (join(await git(worktree, "rev-parse", "--absolute-git-dir"), "index.lock") !== lock)
|
|
42
|
+
return unknown("the fixture worktree metadata is not where git was expected to put it");
|
|
43
|
+
const control = join(worktree, "tickmarkr-codex-commit-control");
|
|
44
|
+
if (existsSync(lock))
|
|
45
|
+
return unknown("a foreign index.lock already holds the fixture worktree");
|
|
46
|
+
const out = await sandbox({ worktree, commonDir, control, lock, token, signal });
|
|
47
|
+
// Every verdict needs the sandbox's own report AND the bytes on disk to agree; anything else is unknown.
|
|
48
|
+
if (!/^control=ok$/m.test(out) || !holds(control, token))
|
|
49
|
+
return unknown("the sandbox did not write the ordinary control file");
|
|
50
|
+
if (/^lock=ok$/m.test(out) && holds(lock, token))
|
|
51
|
+
return { status: "allowed", detail: "the sandbox wrote index.lock" };
|
|
52
|
+
if (/^lock=fail .*(not permitted|permission denied)/mi.test(out) && !existsSync(lock)) {
|
|
53
|
+
return { status: "protected", detail: "the sandbox wrote an ordinary worktree file but denied index.lock" };
|
|
54
|
+
}
|
|
55
|
+
return unknown("the sandbox result for index.lock was unreadable");
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
return unknown(signal?.aborted ? "probe cancelled" : `probe did not run (${(e instanceof Error ? e.message : String(e)).split("\n")[0]})`);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
// only the lock this probe wrote (its token) is ours to remove; a foreign one is never touched
|
|
62
|
+
if (lock && holds(lock, token))
|
|
63
|
+
rmSync(lock, { force: true });
|
|
64
|
+
// a foreign lock lives in the fixture's metadata directory, which `worktree remove` would delete with
|
|
65
|
+
// it: drop only the checkout and leave that metadata to the lock's owner. Never a repository-wide
|
|
66
|
+
// prune — that would take an earlier probe's preserved foreign lock (or anyone's prunable worktree).
|
|
67
|
+
const foreign = lock !== undefined && existsSync(lock);
|
|
68
|
+
if (lock && !foreign) {
|
|
69
|
+
await git(repoRoot, "worktree", "remove", "--force", worktree).catch(() => undefined);
|
|
70
|
+
// a failed `worktree add` leaves metadata git no longer removes by path: drop this probe's own directory
|
|
71
|
+
rmSync(dirname(lock), { recursive: true, force: true });
|
|
72
|
+
try {
|
|
73
|
+
rmdirSync(dirname(dirname(lock)));
|
|
74
|
+
}
|
|
75
|
+
catch { /* other worktrees live there */ }
|
|
76
|
+
}
|
|
77
|
+
rmSync(parent, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const NOTE = {
|
|
81
|
+
protected: "linked-worktree git metadata protected",
|
|
82
|
+
allowed: "linked-worktree commit probe: index.lock writable",
|
|
83
|
+
unknown: "linked-worktree commit probe unknown",
|
|
84
|
+
};
|
|
85
|
+
/** Doctor's one call: gated on the REAL codex adapter object (a codex-id stub gets no probe and no output
|
|
86
|
+
* unless a test injects a sandbox — the kimiTurnEnabled rule). Mutates only the health record doctor owns. */
|
|
87
|
+
export async function recordCodexCommit(adapters, health, cwd, sandbox) {
|
|
88
|
+
const adapter = adapters.find((a) => a.id === codex.id);
|
|
89
|
+
const h = health[codex.id];
|
|
90
|
+
if (!adapter || (adapter !== codex && !sandbox) || !h?.installed)
|
|
91
|
+
return;
|
|
92
|
+
const r = await probeCodexCommit(cwd, sandbox);
|
|
93
|
+
const say = `${NOTE[r.status]} — ${r.detail}${r.status === "protected" ? "; a Codex worker cannot commit in its worktree" : ""}`;
|
|
94
|
+
health[codex.id] = { ...h, codexCommit: r.status, note: `${h.note ? `${h.note}; ` : ""}${say}` };
|
|
95
|
+
}
|
|
96
|
+
/** Plan's headline beneath a Codex worker row; null for every other worker and for an allowed probe.
|
|
97
|
+
* An absent field is unknown, but only the real adapter earns the line — stubs stay byte-identical. */
|
|
98
|
+
export function codexCommitHeadline(worker, adapters, health) {
|
|
99
|
+
if (worker !== codex.id || !adapters.includes(codex))
|
|
100
|
+
return null;
|
|
101
|
+
const status = health[codex.id]?.codexCommit ?? "unknown";
|
|
102
|
+
if (status === "protected") {
|
|
103
|
+
return " worker restriction: codex linked-worktree git metadata is protected (doctor probe) — this worker cannot commit its own work; routing consequence: steer this task to an eligible non-Codex worker";
|
|
104
|
+
}
|
|
105
|
+
return status === "unknown"
|
|
106
|
+
? " worker restriction: unknown — the codex linked-worktree commit probe has no verdict; run tickmarkr doctor"
|
|
107
|
+
: null;
|
|
108
|
+
}
|
package/dist/adapters/codex.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { type TrustDialog, type TrustVerdict, type WorkerAdapter } from "./types.js";
|
|
1
|
+
import { type ModelRetirement, type TrustDialog, type TrustVerdict, type WorkerAdapter } from "./types.js";
|
|
2
2
|
export declare function readCodexModelsCache(path?: string): {
|
|
3
3
|
models: string[];
|
|
4
4
|
fetchedAt?: string;
|
|
5
|
+
retirements?: Record<string, ModelRetirement | null>;
|
|
5
6
|
};
|
|
6
7
|
export declare const CODEX_TRUST_DIALOG: TrustDialog;
|
|
7
8
|
export declare const CODEX_INPUT_BOX: import("./types.js").InputBox;
|
package/dist/adapters/codex.js
CHANGED
|
@@ -61,20 +61,39 @@ function codexSessionDayDirs(sessions, sinceMs) {
|
|
|
61
61
|
// Fails OPEN to { models: [] } on missing/corrupt cache. Version drift observed (installed CLI
|
|
62
62
|
// 0.143.0 vs cache client_version 0.144.0, 2026-07-10) — hence the defensive try/catch and no shape
|
|
63
63
|
// assumptions. CODEX_HOME is codex's own relocation env. fetchedAt is the honest codex knowledge age.
|
|
64
|
+
// v2.6.4 T2 (B1a): each listed entry's `upgrade` is the CLI's own retirement notice (0.159.0 cache,
|
|
65
|
+
// 2026-09-29: gpt-5.5 → {model: gpt-5.6-sol, retirement_at: 2026-10-14T19:00:00Z}; every other listed
|
|
66
|
+
// id carries upgrade: null). null records known clean; a malformed or absent `upgrade` records nothing,
|
|
67
|
+
// so that id stays unknown. An unreadable cache returns no retirements at all (unknown, never clean).
|
|
64
68
|
export function readCodexModelsCache(path) {
|
|
65
69
|
const p = path ?? join(process.env.CODEX_HOME || join(homedir(), ".codex"), "models_cache.json");
|
|
66
70
|
try {
|
|
67
71
|
const d = JSON.parse(readFileSync(p, "utf8"));
|
|
68
|
-
const
|
|
69
|
-
.filter((m) => m?.visibility === "list" && typeof m.slug === "string" && m.slug.length > 0)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
const listed = (d.models ?? [])
|
|
73
|
+
.filter((m) => m?.visibility === "list" && typeof m.slug === "string" && m.slug.length > 0 && MODEL_ID_RE.test(m.slug));
|
|
74
|
+
const retirements = {};
|
|
75
|
+
for (const m of listed) {
|
|
76
|
+
const notice = codexRetirement(m.upgrade);
|
|
77
|
+
if (notice !== undefined)
|
|
78
|
+
retirements[m.slug] = notice;
|
|
79
|
+
}
|
|
80
|
+
return { models: listed.map((m) => m.slug), fetchedAt: typeof d.fetched_at === "string" ? d.fetched_at : undefined, retirements };
|
|
73
81
|
}
|
|
74
82
|
catch {
|
|
75
83
|
return { models: [] };
|
|
76
84
|
}
|
|
77
85
|
}
|
|
86
|
+
function codexRetirement(upgrade) {
|
|
87
|
+
if (upgrade === null)
|
|
88
|
+
return null;
|
|
89
|
+
const u = upgrade;
|
|
90
|
+
if (typeof u?.retirement_at !== "string" || !Number.isFinite(Date.parse(u.retirement_at)))
|
|
91
|
+
return undefined;
|
|
92
|
+
if (u.model === undefined)
|
|
93
|
+
return { retiresAt: u.retirement_at };
|
|
94
|
+
// the successor id comes from an external file and is rendered in lints — same charset gate as slugs
|
|
95
|
+
return typeof u.model === "string" && MODEL_ID_RE.test(u.model) ? { retiresAt: u.retirement_at, successor: u.model } : undefined;
|
|
96
|
+
}
|
|
78
97
|
// tickmarkr worktrees keep their gitdir under the MAIN repo's .git/worktrees/<name> — outside the
|
|
79
98
|
// workspace-write sandbox root — so git commit dies on index.lock (incident run-20260709-104447
|
|
80
99
|
// P87-08: "Unable to create …/.git/worktrees/…/index.lock"). Expanded by the pane shell at the
|
|
@@ -272,6 +291,8 @@ export const codex = {
|
|
|
272
291
|
// v1.5 MODEL-05: codex reads an offline cache, so "now" would re-stamp an ancient cache fresh and the
|
|
273
292
|
// 30-day staleness lint could never fire. Surface the cache's own fetched_at so doctor stamps the real age.
|
|
274
293
|
listModelsFetchedAt: () => readCodexModelsCache().fetchedAt,
|
|
294
|
+
// v2.6.4 T2 (B1a): the cache's per-model retirement notices, persisted beside models in doctor.json.
|
|
295
|
+
listModelsRetirements: () => readCodexModelsCache().retirements,
|
|
275
296
|
collectUsage(cwd, sinceMs) {
|
|
276
297
|
try {
|
|
277
298
|
const real = realpathSync(cwd);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { TickmarkrConfig } from "../config/config.js";
|
|
2
|
+
import { type CatalogFetcher, type CatalogReadResult } from "./catalog-remote.js";
|
|
3
|
+
import { type ClaudeAlias } from "./claude-code.js";
|
|
4
|
+
import type { AuthHealth, WorkerAdapter } from "./types.js";
|
|
5
|
+
export interface ModelDiscoveryCheckInput {
|
|
6
|
+
cwd: string;
|
|
7
|
+
cfg: TickmarkrConfig;
|
|
8
|
+
adapters: WorkerAdapter[];
|
|
9
|
+
/** This doctor's fresh health, BEFORE it is written: the doctor.json on disk is the prior discovery. */
|
|
10
|
+
health: Record<string, AuthHealth>;
|
|
11
|
+
catalog: CatalogReadResult;
|
|
12
|
+
resolved: Partial<Record<ClaudeAlias, string | undefined>>;
|
|
13
|
+
/** False when this doctor already refreshed, was handed a catalog, or may not touch the network. */
|
|
14
|
+
mayRefresh: boolean;
|
|
15
|
+
fetcher?: CatalogFetcher;
|
|
16
|
+
now?: () => Date;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Doctor's post-discovery coverage check. A model this discovery ADDED (absent from the prior
|
|
20
|
+
* doctor.json) that the catalog does not cover earns at most ONE refresh, even under a fresh cache;
|
|
21
|
+
* a model already seen never re-fetches, so a permanently uncovered model cannot become a per-run
|
|
22
|
+
* fetch. A failed refresh leaves coverage unknown — never "uncovered" — until a successful refresh. Advisory: no config, tier or
|
|
23
|
+
* routing write; sourced identity records print as inference for confirmation.
|
|
24
|
+
*/
|
|
25
|
+
export declare function checkModelDiscovery(input: ModelDiscoveryCheckInput): Promise<{
|
|
26
|
+
catalog: CatalogReadResult;
|
|
27
|
+
findings: string[];
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* plan's headline: drift read from the identity doctor PERSISTED for each configured claude alias.
|
|
31
|
+
* Matching identities stay silent; an alias doctor never resolved stays unknown, never clean.
|
|
32
|
+
*/
|
|
33
|
+
export declare function aliasIdentityHeadlines(cfg: TickmarkrConfig, health: Record<string, AuthHealth>): string[];
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { stateDirName } from "../graph/graph.js";
|
|
4
|
+
import { observedIdentity } from "../route/preference.js";
|
|
5
|
+
import { CATALOG_REFRESH_TIMEOUT_MS, formatCatalogRefreshLegs, refreshCatalogCommand, resolveCatalogModel, sourcedIdentityRecord, } from "./catalog-remote.js";
|
|
6
|
+
import { CLAUDE_ALIAS_IDENTITY_STAMPS } from "./claude-code.js";
|
|
7
|
+
import { readDoctor } from "./registry.js";
|
|
8
|
+
const seen = (health, adapter) => {
|
|
9
|
+
const h = health?.[adapter];
|
|
10
|
+
return new Set([...(h?.models ?? []), ...Object.values(h?.modelAuth ?? {}).flatMap((v) => (v.identity ? [v.identity] : []))]);
|
|
11
|
+
};
|
|
12
|
+
// A failed refresh's unknown outlives that doctor: keyed to the models.dev spine stamp it failed against,
|
|
13
|
+
// so any later successful refresh (doctor's guard, fleet, catalog refresh) supersedes it by moving the stamp.
|
|
14
|
+
const unknownPath = (cwd) => join(cwd, stateDirName(cwd), "catalog-coverage-unknown.json");
|
|
15
|
+
const spineStamp = (c) => c.source === "cache" ? c.catalog.legFetchedAt?.modelsDev ?? c.catalog.fetchedAt : null;
|
|
16
|
+
function persistedUnknown(cwd, catalog) {
|
|
17
|
+
try {
|
|
18
|
+
const v = JSON.parse(readFileSync(unknownPath(cwd), "utf8"));
|
|
19
|
+
return v.modelsDevAt === spineStamp(catalog) && Array.isArray(v.keys) ? v.keys.filter((k) => typeof k === "string") : [];
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Doctor's post-discovery coverage check. A model this discovery ADDED (absent from the prior
|
|
27
|
+
* doctor.json) that the catalog does not cover earns at most ONE refresh, even under a fresh cache;
|
|
28
|
+
* a model already seen never re-fetches, so a permanently uncovered model cannot become a per-run
|
|
29
|
+
* fetch. A failed refresh leaves coverage unknown — never "uncovered" — until a successful refresh. Advisory: no config, tier or
|
|
30
|
+
* routing write; sourced identity records print as inference for confirmation.
|
|
31
|
+
*/
|
|
32
|
+
export async function checkModelDiscovery(input) {
|
|
33
|
+
const { cfg, health } = input;
|
|
34
|
+
const prior = readDoctor(input.cwd);
|
|
35
|
+
const findings = [];
|
|
36
|
+
// Discovery is every listed model and resolved alias identity, classified or not: an operator's tier
|
|
37
|
+
// classification never establishes catalog coverage. A claude alias is checked as the identity it
|
|
38
|
+
// resolved to; one with no resolved identity has nothing a refresh could cover (its row says unknown).
|
|
39
|
+
const resolvedOf = (adapter, model) => adapter === "claude-code" && model in CLAUDE_ALIAS_IDENTITY_STAMPS ? input.resolved[model] ?? null : undefined;
|
|
40
|
+
const added = input.adapters.filter((a) => health[a.id]?.installed).flatMap((a) => {
|
|
41
|
+
const before = seen(prior, a.id);
|
|
42
|
+
return [...seen(health, a.id)].filter((model) => !before.has(model) && resolvedOf(a.id, model) !== null)
|
|
43
|
+
.map((model) => ({ adapter: a.id, model, resolvedModel: resolvedOf(a.id, model) ?? undefined }));
|
|
44
|
+
});
|
|
45
|
+
const covered = (catalog, row) => resolveCatalogModel(catalog.catalog, { provider: cfg.tiers[row.adapter]?.vendor ?? undefined, model: row.model, resolvedModel: row.resolvedModel }) !== undefined;
|
|
46
|
+
let catalog = input.catalog;
|
|
47
|
+
const unknown = persistedUnknown(input.cwd, catalog);
|
|
48
|
+
if (unknown.length)
|
|
49
|
+
catalog = { ...catalog, coverageUnknown: unknown };
|
|
50
|
+
const uncovered = added.filter((row) => !covered(catalog, row));
|
|
51
|
+
if (uncovered.length && input.mayRefresh) {
|
|
52
|
+
const refreshed = await refreshCatalogCommand({
|
|
53
|
+
repoRoot: input.cwd, fetcher: input.fetcher, timeoutMs: CATALOG_REFRESH_TIMEOUT_MS, now: input.now,
|
|
54
|
+
});
|
|
55
|
+
const names = (rows) => rows.map((r) => `${r.adapter}:${r.model}`).join(", ");
|
|
56
|
+
if (refreshed.legs.some((leg) => leg.leg === "models.dev" && leg.status === "updated")) {
|
|
57
|
+
catalog = refreshed.catalog;
|
|
58
|
+
const still = uncovered.filter((row) => !covered(catalog, row));
|
|
59
|
+
findings.push(`catalog coverage refreshed once after discovery added ${uncovered.length} uncovered model(s) (${names(uncovered)})${still.length ? `; still uncovered: ${names(still)}` : "; now covered"} — ${formatCatalogRefreshLegs(refreshed.legs)} (advisory — routing unchanged)`);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const keys = [...new Set([...unknown, ...uncovered.map((r) => `${r.adapter}:${r.model}`)])];
|
|
63
|
+
catalog = { ...catalog, coverageUnknown: keys };
|
|
64
|
+
mkdirSync(join(input.cwd, stateDirName(input.cwd)), { recursive: true });
|
|
65
|
+
writeFileSync(unknownPath(input.cwd), JSON.stringify({ modelsDevAt: spineStamp(refreshed.catalog), keys }) + "\n");
|
|
66
|
+
findings.push(`catalog coverage unknown for ${names(uncovered)} — the one post-discovery refresh failed: ${formatCatalogRefreshLegs(refreshed.legs)} (advisory — routing unchanged)`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
findings.push(...identityRecordFindings(cfg, health, input.resolved));
|
|
70
|
+
return { catalog, findings };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Sourced tier/price/window per resolved alias; unknown and unsourced identities claim nothing. An alias
|
|
74
|
+
* the probe proved unauthed is not listed as unknown: its model-status row already names why.
|
|
75
|
+
*/
|
|
76
|
+
function identityRecordFindings(cfg, health, resolved) {
|
|
77
|
+
const configured = cfg.tiers["claude-code"]?.models ?? {};
|
|
78
|
+
const unknown = [];
|
|
79
|
+
const rows = [];
|
|
80
|
+
for (const [alias, identity] of Object.entries(resolved)) {
|
|
81
|
+
if (!identity) {
|
|
82
|
+
if (health["claude-code"]?.modelAuth?.[alias]?.authed !== false)
|
|
83
|
+
unknown.push(alias);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const record = sourcedIdentityRecord(identity);
|
|
87
|
+
if (!record || record.alias !== alias) {
|
|
88
|
+
// A record of another alias family is a disagreement, never a record this alias may borrow.
|
|
89
|
+
rows.push(`identity record: claude-code:${alias} → ${identity} ${record ? `disagrees with the ${alias} family` : "has no sourced record"} — tier/price/window unknown`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
rows.push(`identity record: claude-code:${alias} → ${identity} · $${record.inputCostPerMtok}/$${record.outputCostPerMtok} per Mtok (API) · window ${record.contextWindow}/${record.outputWindow} · sourced ${record.fetchedAt} (${record.sources.join(", ")})`
|
|
93
|
+
+ ` · INFERRED tier ${record.tier} by ${record.tierBasis} — inference for operator confirmation (tickmarkr fleet); doctor writes no configuration`
|
|
94
|
+
+ ` · routing authority: configured claude-code:${alias}=${configured[alias] ?? "unclassified"}`);
|
|
95
|
+
}
|
|
96
|
+
if (unknown.length)
|
|
97
|
+
rows.unshift(`identity record: claude-code:${unknown.join(", ")} identity unknown — no sourced tier/price/window applies`);
|
|
98
|
+
return rows;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* plan's headline: drift read from the identity doctor PERSISTED for each configured claude alias.
|
|
102
|
+
* Matching identities stay silent; an alias doctor never resolved stays unknown, never clean.
|
|
103
|
+
*/
|
|
104
|
+
export function aliasIdentityHeadlines(cfg, health) {
|
|
105
|
+
if (!health["claude-code"]?.installed)
|
|
106
|
+
return [];
|
|
107
|
+
const configured = cfg.tiers["claude-code"]?.models ?? {};
|
|
108
|
+
const unknown = [];
|
|
109
|
+
const lines = [];
|
|
110
|
+
for (const [alias, stamped] of Object.entries(CLAUDE_ALIAS_IDENTITY_STAMPS)) {
|
|
111
|
+
if (!(alias in configured))
|
|
112
|
+
continue;
|
|
113
|
+
const identity = observedIdentity(health, "claude-code", alias);
|
|
114
|
+
if (!identity)
|
|
115
|
+
unknown.push(alias);
|
|
116
|
+
else if (identity !== stamped) {
|
|
117
|
+
lines.push(`alias drift: claude-code:${alias} serves ${identity} (persisted by doctor), stamped identity ${stamped} — reclassify per benchmark policy (advisory — routing unchanged)`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (unknown.length)
|
|
121
|
+
lines.push(`alias identity unknown: claude-code:${unknown.join(", ")} — doctor persisted no resolved identity (run tickmarkr doctor)`);
|
|
122
|
+
return lines;
|
|
123
|
+
}
|
|
@@ -27,7 +27,7 @@ declare const RANKED_BASES: readonly ["agentic-coding", "intelligence"];
|
|
|
27
27
|
type RankedBasis = typeof RANKED_BASES[number];
|
|
28
28
|
export type CatalogSuggestionBasis = RankedBasis | "price";
|
|
29
29
|
export interface CatalogModelAdvisory {
|
|
30
|
-
coverage: "covered" | "uncovered";
|
|
30
|
+
coverage: "covered" | "uncovered" | "unknown";
|
|
31
31
|
evidence?: CatalogModelEvidence;
|
|
32
32
|
suggestion?: {
|
|
33
33
|
tier: Tier;
|
|
@@ -81,6 +81,7 @@ export declare function modelLints(cfg: TickmarkrConfig, health: Record<string,
|
|
|
81
81
|
tty?: boolean;
|
|
82
82
|
stateDir?: string;
|
|
83
83
|
overlayPreferShapes?: ReadonlySet<string>;
|
|
84
|
+
now?: Date;
|
|
84
85
|
}): string[];
|
|
85
86
|
export declare function formatModelAuthLine(excluded: {
|
|
86
87
|
key: string;
|
|
@@ -183,6 +183,10 @@ export function catalogModelAdvisory(cfg, catalog, adapter, model, resolvedModel
|
|
|
183
183
|
const row = { adapter, model, resolvedModel };
|
|
184
184
|
const evidence = catalogEvidenceFor(cfg, catalog, row);
|
|
185
185
|
if (!evidence) {
|
|
186
|
+
// v2.6.5 T9: a failed post-discovery refresh proves nothing about coverage — unknown, never "uncovered".
|
|
187
|
+
if ([model, resolvedModel].some((m) => m !== undefined && catalog.coverageUnknown?.includes(`${adapter}:${m}`))) {
|
|
188
|
+
return { coverage: "unknown", display: `${model} — catalog coverage unknown (post-discovery refresh failed); no tier suggestion` };
|
|
189
|
+
}
|
|
186
190
|
return {
|
|
187
191
|
coverage: "uncovered",
|
|
188
192
|
display: `${model} — uncovered by ${catalog.source === "cache" ? "cached catalogs" : "vendored catalog"}; no tier suggestion`,
|
|
@@ -478,10 +482,55 @@ export function preferEntryLints(cfg, health, mapShapes = new Set()) {
|
|
|
478
482
|
sweep("consult.prefer", cfg.consult.prefer);
|
|
479
483
|
return lints;
|
|
480
484
|
}
|
|
485
|
+
// v2.6.4 T2 (B1a): doctor.json is read without a schema (compat), so a stored notice is re-checked here —
|
|
486
|
+
// anything but null (known clean) or a dated notice reads as unknown, never as clean.
|
|
487
|
+
function storedRetirement(record, model) {
|
|
488
|
+
if (!record || typeof record !== "object")
|
|
489
|
+
return undefined;
|
|
490
|
+
const r = record[model];
|
|
491
|
+
if (r === null)
|
|
492
|
+
return null;
|
|
493
|
+
const n = r;
|
|
494
|
+
if (typeof n?.retiresAt !== "string" || !Number.isFinite(Date.parse(n.retiresAt)))
|
|
495
|
+
return undefined;
|
|
496
|
+
if (n.successor === undefined)
|
|
497
|
+
return { retiresAt: n.retiresAt };
|
|
498
|
+
return typeof n.successor === "string" && MODEL_ID_RE.test(n.successor) ? { retiresAt: n.retiresAt, successor: n.successor } : undefined;
|
|
499
|
+
}
|
|
500
|
+
// Configured AND listed models only — a configured id the CLI stopped listing already carries the
|
|
501
|
+
// tombstone advisory. The CLI's notice is quoted as-is: its successor is the CLI's upgrade target, never
|
|
502
|
+
// a tier or benchmark claim, and no model name or visibility is read as one. Advisory: routing unchanged.
|
|
503
|
+
function retirementLints(id, listedConfigured, record, nowMs) {
|
|
504
|
+
const lints = [];
|
|
505
|
+
const unknown = [];
|
|
506
|
+
for (const model of listedConfigured) {
|
|
507
|
+
const notice = storedRetirement(record, model);
|
|
508
|
+
if (notice === undefined) {
|
|
509
|
+
unknown.push(model);
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (!notice)
|
|
513
|
+
continue; // listed with no notice: known clean
|
|
514
|
+
const successor = notice.successor
|
|
515
|
+
? `successor ${notice.successor} (named by the CLI — not a tier claim; classify it per benchmark policy)`
|
|
516
|
+
: "no successor named";
|
|
517
|
+
lints.push(nowMs >= Date.parse(notice.retiresAt)
|
|
518
|
+
? `${id}: ${model} retired ${notice.retiresAt} per the CLI's notice; ${successor} — tombstone it (${model}: null overlay) or verify the id (advisory — routing unchanged)`
|
|
519
|
+
: `${id}: ${model} retires ${notice.retiresAt} per the CLI's notice; ${successor} — tombstone it (${model}: null overlay) before then (advisory — routing unchanged)`);
|
|
520
|
+
}
|
|
521
|
+
if (unknown.length) {
|
|
522
|
+
const why = record && typeof record === "object"
|
|
523
|
+
? "the CLI's notice is absent or unreadable"
|
|
524
|
+
: "doctor record carries no CLI retirement notices; rerun tickmarkr doctor";
|
|
525
|
+
lints.push(`${id}: retirement unknown for ${unknown.join(", ")} — ${why}`);
|
|
526
|
+
}
|
|
527
|
+
return lints;
|
|
528
|
+
}
|
|
481
529
|
// Diffs detected models (doctor.json) against configured tiers, both directions, per installed adapter.
|
|
482
530
|
// No ` ! ` prefix here — the consumer (doctor rows / plan lints) owns that. Pre-v1.5 doctor.json (models:[], no
|
|
483
531
|
// modelsDetectedAt) is the compat baseline: `?.`/`?? []` everywhere, no zod (would reject old files).
|
|
484
532
|
export function modelLints(cfg, health, adapters, opts) {
|
|
533
|
+
const nowMs = (opts?.now ?? new Date()).getTime();
|
|
485
534
|
const cap = opts?.tty ? TTY_LINT_CAP : LINT_CAP;
|
|
486
535
|
const doctorRef = opts?.tty ? doctorJsonRef(opts.stateDir ?? DEFAULT_STATE_DIR) : "";
|
|
487
536
|
const lints = [];
|
|
@@ -517,6 +566,8 @@ export function modelLints(cfg, health, adapters, opts) {
|
|
|
517
566
|
lints.push(`${id}: tiers lists ${model} — CLI no longer reports it; tombstone it (${model}: null overlay) or verify the id`);
|
|
518
567
|
}
|
|
519
568
|
}
|
|
569
|
+
if (adapter.listModelsRetirements)
|
|
570
|
+
lints.push(...retirementLints(id, configured.filter((m) => detected.includes(m)), h?.modelRetirements, nowMs));
|
|
520
571
|
const extra = collapseUnclassified(detected, new Set(configured));
|
|
521
572
|
if (extra.length) {
|
|
522
573
|
const shown = extra.slice(0, cap).map((row) => row.model).join(", ");
|
|
@@ -525,7 +576,7 @@ export function modelLints(cfg, health, adapters, opts) {
|
|
|
525
576
|
}
|
|
526
577
|
const at = h?.modelsDetectedAt;
|
|
527
578
|
if (at) {
|
|
528
|
-
const days = Math.floor((
|
|
579
|
+
const days = Math.floor((nowMs - Date.parse(at)) / DAY_MS); // completed days — never overstate age
|
|
529
580
|
if (days >= MODEL_STALE_DAYS)
|
|
530
581
|
lints.push(`${id}: model knowledge is ${days} days old — rerun tickmarkr doctor`);
|
|
531
582
|
}
|
|
@@ -15,7 +15,8 @@ export type ModelWindowResolution = {
|
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
17
|
* Validate cited model-window claims before production code can consume them.
|
|
18
|
-
* These values are vendor-published claims,
|
|
18
|
+
* These values are vendor-published claims (a vendor page, or a vendor CLI's own model cache), not
|
|
19
|
+
* measurements made by tickmarkr.
|
|
19
20
|
*/
|
|
20
21
|
export declare function loadModelWindowClaims(input: unknown): readonly ModelWindowClaim[];
|
|
21
22
|
/** The closed, production-readable catalog of cited context-window claims. */
|