tickmarkr 2.6.4 → 2.6.6
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 +7 -3
- 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 +36 -0
- package/dist/adapters/codex-commit-check.js +205 -0
- package/dist/adapters/codex.d.ts +1 -0
- package/dist/adapters/codex.js +12 -6
- 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 +1 -1
- package/dist/adapters/model-lints.js +4 -0
- package/dist/adapters/prompt.js +3 -2
- package/dist/adapters/types.d.ts +10 -0
- package/dist/cli/commands/approve.js +10 -4
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +17 -3
- package/dist/cli/commands/fleet.d.ts +10 -1
- package/dist/cli/commands/fleet.js +363 -106
- package/dist/cli/commands/init.js +10 -3
- package/dist/cli/commands/plan.js +76 -4
- package/dist/cli/commands/verify.d.ts +1 -1
- package/dist/cli/commands/verify.js +65 -15
- package/dist/config/fleet-overlay.d.ts +16 -1
- package/dist/config/fleet-overlay.js +230 -28
- package/dist/drivers/orca.d.ts +34 -2
- package/dist/drivers/orca.js +206 -32
- 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 +5 -3
- package/dist/gates/run-gates.js +112 -56
- package/dist/gates/test-manifest.d.ts +3 -0
- package/dist/gates/test-manifest.js +1 -1
- package/dist/gates/timeout-shaped.d.ts +3 -0
- package/dist/gates/timeout-shaped.js +10 -0
- package/dist/run/daemon.js +326 -130
- package/dist/run/git.js +9 -0
- package/dist/run/journal.d.ts +6 -2
- package/dist/run/journal.js +22 -7
- 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 +24 -1
- package/dist/tui/cockpit/live-store.js +99 -8
- 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 +37 -10
- package/dist/tui/ink/fleet-app.js +65 -21
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +32 -2
- package/skills/tickmarkr-loop/SKILL.md +46 -2
- package/skills/tickmarkr-overseer/SKILL.md +83 -8
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +58 -16
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +19 -6
- package/skills/tickmarkr-overseer/scripts/watch-parks.sh +121 -64
package/README.md
CHANGED
|
@@ -258,9 +258,13 @@ two-pane surface: the left rail lists views (**All models**, **Shapes**, **Steer
|
|
|
258
258
|
installed agent CLI with its auth state and model count; the right pane is a searchable model list
|
|
259
259
|
with tier, context, price, and probe-latency columns. `Space` allows/denies, `Enter` classifies an
|
|
260
260
|
unclassified model (with a required benchmark-provenance note) or pins a classified one to a shape,
|
|
261
|
-
`m` opens the routing-mode presets, and `w` renders the unified diff of
|
|
262
|
-
|
|
263
|
-
|
|
261
|
+
`m` opens the routing-mode presets, and `w` renders the unified diff of the one overlay the save
|
|
262
|
+
lands in, named in the review: an allow/deny membership edit goes to the layer that holds it (the
|
|
263
|
+
repo overlay when it declares that family, else your user overlay), everything else to the user
|
|
264
|
+
overlay; a batch needing both is refused — save each half in its own session. Nothing is written
|
|
265
|
+
until you confirm with `y`. A worker-only deny on a partially excluded adapter takes two rail
|
|
266
|
+
presses: out · all seats, then out · workers. The Shapes view pins from a candidate picker ranked by
|
|
267
|
+
the production router.
|
|
264
268
|
|
|
265
269
|
Routing-mode semantics, pin/floor/prefer precedence, review and consult steering syntax,
|
|
266
270
|
provenance rules, and `--quality` / `--mode` flags are documented in
|
|
@@ -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,36 @@
|
|
|
1
|
+
import { type AuthHealth, type CodexCommitStatus, type WorkerAdapter } from "./types.js";
|
|
2
|
+
export type { CodexCommitStatus };
|
|
3
|
+
export interface CodexCommitProbe {
|
|
4
|
+
worktree: string;
|
|
5
|
+
commonDir: string;
|
|
6
|
+
branch: string;
|
|
7
|
+
control: string;
|
|
8
|
+
lock: string;
|
|
9
|
+
hostileHook: string;
|
|
10
|
+
hostileRoot: string;
|
|
11
|
+
token: string;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}
|
|
14
|
+
/** Runs the probe script inside the Codex sandbox and resolves its stdout. Injected in tests. */
|
|
15
|
+
export type CodexSandbox = (probe: CodexCommitProbe) => Promise<string>;
|
|
16
|
+
export interface CodexCommitResult {
|
|
17
|
+
status: CodexCommitStatus;
|
|
18
|
+
detail: string;
|
|
19
|
+
}
|
|
20
|
+
/** The `sh` argv production runs at the fixture worktree: `codex sandbox` under the worker grant fragment. */
|
|
21
|
+
export declare const codexSandboxArgs: (p: CodexCommitProbe, grant?: string) => string[];
|
|
22
|
+
/** A sandbox executing `grant`; production is CODEX_GIT_GRANT. Tests pass a mutant grant to prove discrimination. */
|
|
23
|
+
export declare const codexSandboxFor: (grant: string) => CodexSandbox;
|
|
24
|
+
export declare const codexSandbox: CodexSandbox;
|
|
25
|
+
export declare function probeCodexCommit(repoRoot: string, sandbox?: CodexSandbox, signal?: AbortSignal): Promise<CodexCommitResult>;
|
|
26
|
+
/** A persisted verdict outside the closed table (an older or newer doctor.json) reads unknown, never allowed. */
|
|
27
|
+
export declare const codexCommitStatus: (health: Record<string, AuthHealth>) => CodexCommitStatus;
|
|
28
|
+
/** Doctor's one call: gated on the REAL codex adapter object (a codex-id stub gets no probe and no output
|
|
29
|
+
* unless a test injects a sandbox — the kimiTurnEnabled rule). Mutates only the health record doctor owns. */
|
|
30
|
+
export declare function recordCodexCommit(adapters: WorkerAdapter[], health: Record<string, AuthHealth>, cwd: string, sandbox?: CodexSandbox): Promise<void>;
|
|
31
|
+
/** Plan's headline beneath a Codex worker row; null for every other worker and for an allowed probe.
|
|
32
|
+
* An absent field is unknown, but only the real adapter earns the line — stubs stay byte-identical. */
|
|
33
|
+
export declare function codexCommitHeadline(worker: string, adapters: WorkerAdapter[], health: Record<string, AuthHealth>): string | null;
|
|
34
|
+
/** Plan's one line for Codex's NON-worker roles (judge, review, consult): they launch with the same grant, so
|
|
35
|
+
* only an escape earns it — allowed, protected and unknown concern commits, which those roles never make. */
|
|
36
|
+
export declare function codexEscapeRoleLine(roles: string[], adapters: WorkerAdapter[], health: Record<string, AuthHealth>): string | null;
|
|
@@ -0,0 +1,205 @@
|
|
|
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_GIT_GRANT, codex } from "./codex.js";
|
|
8
|
+
import { shq } from "./types.js";
|
|
9
|
+
const exec = promisify(execFile);
|
|
10
|
+
const git = async (cwd, ...args) => (await exec("git", args, { cwd })).stdout.trim();
|
|
11
|
+
const CONTROL = "tickmarkr-codex-commit-control";
|
|
12
|
+
// $1 control, $2 token, $3 hostile hooks member, $4 hostile common-root member. The commit disables hooks (the
|
|
13
|
+
// probe asks what the sandbox permits, never what a user hook decides) and forces the reflog so common/logs is
|
|
14
|
+
// always exercised. noclobber (set -C) never writes over a foreign occupant of a hostile path.
|
|
15
|
+
const SCRIPT = `printf %s "$2" > "$1" && echo control=ok || echo control=fail
|
|
16
|
+
git add -- "$1" && git -c core.hooksPath=/dev/null -c core.logAllRefUpdates=true -c commit.gpgsign=false -c user.name=tickmarkr -c user.email=probe@tickmarkr.invalid commit -q -m "tickmarkr-probe $2" && echo commit=ok || echo commit=fail
|
|
17
|
+
(set -C; printf %s "$2" > "$3") 2>/dev/null && echo hook=written || echo hook=denied
|
|
18
|
+
(set -C; printf %s "$2" > "$4") 2>/dev/null && echo root=written || echo root=denied`;
|
|
19
|
+
/** The `sh` argv production runs at the fixture worktree: `codex sandbox` under the worker grant fragment. */
|
|
20
|
+
export const codexSandboxArgs = (p, grant = CODEX_GIT_GRANT) => [
|
|
21
|
+
"-c",
|
|
22
|
+
`codex sandbox -c 'sandbox_mode="workspace-write"' ${grant} -- sh -c ${shq(SCRIPT)} sh ${[p.control, p.token, p.hostileHook, p.hostileRoot].map(shq).join(" ")}`,
|
|
23
|
+
];
|
|
24
|
+
/** A sandbox executing `grant`; production is CODEX_GIT_GRANT. Tests pass a mutant grant to prove discrimination. */
|
|
25
|
+
export const codexSandboxFor = (grant) => async (p) => (await exec("sh", codexSandboxArgs(p, grant), { cwd: p.worktree, timeout: 20_000, signal: p.signal })).stdout;
|
|
26
|
+
export const codexSandbox = codexSandboxFor(CODEX_GIT_GRANT);
|
|
27
|
+
const holds = (path, token) => {
|
|
28
|
+
try {
|
|
29
|
+
return readFileSync(path, "utf8") === token;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const unknown = (detail) => ({ status: "unknown", detail });
|
|
36
|
+
// The bytes on disk decide an escape whatever the receipts say — and whether or not the sandbox exited cleanly.
|
|
37
|
+
const escaped = (hostile, token) => {
|
|
38
|
+
const written = hostile.filter((m) => holds(m, token));
|
|
39
|
+
return written.length ? { status: "escape", detail: `the sandbox wrote ${written.join(" and ")} outside the worker grant` } : undefined;
|
|
40
|
+
};
|
|
41
|
+
// One outcome per receipt field: a field reporting two outcomes (commit=ok AND commit=fail) is contradictory.
|
|
42
|
+
const contradictory = (out) => {
|
|
43
|
+
const seen = new Map();
|
|
44
|
+
for (const [, field, outcome] of out.matchAll(/^(control|commit|hook|root)=(\S+)/gm)) {
|
|
45
|
+
if ((seen.get(field) ?? outcome) !== outcome)
|
|
46
|
+
return true;
|
|
47
|
+
seen.set(field, outcome);
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
};
|
|
51
|
+
const tipOf = async (repo, ref) => git(repo, "rev-parse", "--verify", "--quiet", `${ref}^{commit}`).catch(() => undefined);
|
|
52
|
+
// This probe's commit and nothing else: one parent (the fixture base), its token in the subject and the control.
|
|
53
|
+
const isProbeCommit = async (repo, commit, base, token) => {
|
|
54
|
+
try {
|
|
55
|
+
return await git(repo, "log", "-1", "--format=%P %s", commit) === `${base} tickmarkr-probe ${token}`
|
|
56
|
+
&& await git(repo, "cat-file", "blob", `${commit}:${CONTROL}`) === token;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
export async function probeCodexCommit(repoRoot, sandbox = codexSandbox, signal) {
|
|
63
|
+
const token = randomUUID();
|
|
64
|
+
const parent = mkdtempSync(join(tmpdir(), "tickmarkr-codex-commit-"));
|
|
65
|
+
// git names the fixture's metadata directory after the checkout's basename; a unique one makes that
|
|
66
|
+
// directory — and so the lock path — known BEFORE `worktree add`, which can fail after creating it
|
|
67
|
+
const branch = `tickmarkr-probe-${token}`;
|
|
68
|
+
const ref = `refs/heads/${branch}`;
|
|
69
|
+
const worktree = join(parent, branch);
|
|
70
|
+
let lock;
|
|
71
|
+
let base;
|
|
72
|
+
let hostile = [];
|
|
73
|
+
try {
|
|
74
|
+
const commonDir = await git(repoRoot, "rev-parse", "--path-format=absolute", "--git-common-dir");
|
|
75
|
+
lock = join(commonDir, "worktrees", basename(worktree), "index.lock");
|
|
76
|
+
const hostileHook = join(commonDir, "hooks", branch), hostileRoot = join(commonDir, branch);
|
|
77
|
+
hostile = [hostileHook, hostileRoot];
|
|
78
|
+
if (hostile.some((m) => existsSync(m)))
|
|
79
|
+
return unknown("a foreign file already holds a hostile probe path");
|
|
80
|
+
base = await git(repoRoot, "rev-parse", "--verify", "HEAD^{commit}");
|
|
81
|
+
await git(repoRoot, "worktree", "add", "-b", branch, worktree, base);
|
|
82
|
+
if (join(await git(worktree, "rev-parse", "--absolute-git-dir"), "index.lock") !== lock)
|
|
83
|
+
return unknown("the fixture worktree metadata is not where git was expected to put it");
|
|
84
|
+
const control = join(worktree, CONTROL);
|
|
85
|
+
if (existsSync(lock))
|
|
86
|
+
return unknown("a foreign index.lock already holds the fixture worktree");
|
|
87
|
+
const out = await sandbox({ worktree, commonDir, branch, control, lock, hostileHook, hostileRoot, token, signal });
|
|
88
|
+
// An escape on disk dominates; every other verdict needs consistent receipts AND disk.
|
|
89
|
+
const escape = escaped(hostile, token);
|
|
90
|
+
if (escape)
|
|
91
|
+
return escape;
|
|
92
|
+
if (contradictory(out))
|
|
93
|
+
return unknown("the sandbox receipts contradict each other");
|
|
94
|
+
if (!/^control=ok$/m.test(out) || !holds(control, token))
|
|
95
|
+
return unknown("the sandbox did not write the ordinary control file");
|
|
96
|
+
if (!/^hook=denied$/m.test(out) || !/^root=denied$/m.test(out) || hostile.some((m) => existsSync(m))) {
|
|
97
|
+
return unknown("the sandbox result for the shared-metadata members was unreadable");
|
|
98
|
+
}
|
|
99
|
+
const tip = await tipOf(repoRoot, ref);
|
|
100
|
+
if (/^commit=ok$/m.test(out) && tip && tip !== base && await isProbeCommit(repoRoot, tip, base, token)) {
|
|
101
|
+
let reflog = "";
|
|
102
|
+
try {
|
|
103
|
+
reflog = readFileSync(join(commonDir, "logs", ref), "utf8");
|
|
104
|
+
}
|
|
105
|
+
catch { /* absent: unknown below */ }
|
|
106
|
+
if (reflog.trimEnd().split("\n").pop()?.startsWith(`${base} ${tip} `)) {
|
|
107
|
+
return { status: "allowed", detail: "the sandbox committed on its fixture branch and was denied both shared-metadata members" };
|
|
108
|
+
}
|
|
109
|
+
return unknown("the fixture branch moved but its reflog does not record the probe commit");
|
|
110
|
+
}
|
|
111
|
+
if (/^commit=fail$/m.test(out) && tip === base) {
|
|
112
|
+
return { status: "protected", detail: "the sandbox wrote an ordinary worktree file but denied the linked-worktree commit" };
|
|
113
|
+
}
|
|
114
|
+
return unknown("the fixture branch does not match the sandbox commit report (a foreign change may have moved it)");
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
// a sandbox that wrote a hostile member and then failed (or was cancelled) still escaped: inspect before cleanup
|
|
118
|
+
const escape = escaped(hostile, token);
|
|
119
|
+
if (escape)
|
|
120
|
+
return escape;
|
|
121
|
+
return unknown(signal?.aborted ? "probe cancelled" : `probe did not run (${(e instanceof Error ? e.message : String(e)).split("\n")[0]})`);
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
// only files holding this probe's token are ours to remove; a foreign occupant is never touched
|
|
125
|
+
for (const m of hostile)
|
|
126
|
+
if (holds(m, token))
|
|
127
|
+
rmSync(m, { force: true });
|
|
128
|
+
if (lock && holds(lock, token))
|
|
129
|
+
rmSync(lock, { force: true });
|
|
130
|
+
// a foreign lock lives in the fixture's metadata directory, which `worktree remove` would delete with
|
|
131
|
+
// it: drop only the checkout and leave that metadata to the lock's owner. Never a repository-wide
|
|
132
|
+
// prune — that would take an earlier probe's preserved foreign lock (or anyone's prunable worktree).
|
|
133
|
+
const foreign = lock !== undefined && existsSync(lock);
|
|
134
|
+
if (lock && !foreign) {
|
|
135
|
+
await git(repoRoot, "worktree", "remove", "--force", worktree).catch(() => undefined);
|
|
136
|
+
// a failed `worktree add` leaves metadata git no longer removes by path: drop this probe's own directory
|
|
137
|
+
rmSync(dirname(lock), { recursive: true, force: true });
|
|
138
|
+
try {
|
|
139
|
+
rmdirSync(dirname(dirname(lock)));
|
|
140
|
+
}
|
|
141
|
+
catch { /* other worktrees live there */ }
|
|
142
|
+
}
|
|
143
|
+
// The fixture branch and its reflog go only while the tip is still this probe's (the base or its own
|
|
144
|
+
// commit), and update-ref's old-value check refuses if anyone moves it between this read and the delete.
|
|
145
|
+
// Metadata preserved for a foreign lock keeps its branch too: that checkout's HEAD still names it.
|
|
146
|
+
const tip = base === undefined || foreign ? undefined : await tipOf(repoRoot, ref);
|
|
147
|
+
if (tip && base && (tip === base || await isProbeCommit(repoRoot, tip, base, token))) {
|
|
148
|
+
await git(repoRoot, "update-ref", "-d", ref, tip).catch(() => undefined);
|
|
149
|
+
}
|
|
150
|
+
rmSync(parent, { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const NOTE = {
|
|
154
|
+
protected: "linked-worktree git metadata protected",
|
|
155
|
+
allowed: "linked-worktree commit probe: commit allowed, shared hooks/config denied",
|
|
156
|
+
escape: "BLOCKING security warning: codex sandbox escape into shared git metadata",
|
|
157
|
+
unknown: "linked-worktree commit probe unknown",
|
|
158
|
+
};
|
|
159
|
+
const CONSEQUENCE = {
|
|
160
|
+
protected: "; a Codex worker cannot commit in its worktree",
|
|
161
|
+
allowed: "",
|
|
162
|
+
escape: "; every Codex role (worker, judge, review, consult) launches with this grant — stop Codex seats and inspect .git/hooks and git config --list --show-origin (core.hooksPath, core.fsmonitor, filter.*, diff.external)",
|
|
163
|
+
unknown: "",
|
|
164
|
+
};
|
|
165
|
+
const STATUSES = new Set(Object.keys(NOTE));
|
|
166
|
+
/** A persisted verdict outside the closed table (an older or newer doctor.json) reads unknown, never allowed. */
|
|
167
|
+
export const codexCommitStatus = (health) => {
|
|
168
|
+
const s = health[codex.id]?.codexCommit;
|
|
169
|
+
return s !== undefined && STATUSES.has(s) ? s : "unknown";
|
|
170
|
+
};
|
|
171
|
+
/** Doctor's one call: gated on the REAL codex adapter object (a codex-id stub gets no probe and no output
|
|
172
|
+
* unless a test injects a sandbox — the kimiTurnEnabled rule). Mutates only the health record doctor owns. */
|
|
173
|
+
export async function recordCodexCommit(adapters, health, cwd, sandbox) {
|
|
174
|
+
const adapter = adapters.find((a) => a.id === codex.id);
|
|
175
|
+
const h = health[codex.id];
|
|
176
|
+
if (!adapter || (adapter !== codex && !sandbox) || !h?.installed)
|
|
177
|
+
return;
|
|
178
|
+
const r = await probeCodexCommit(cwd, sandbox);
|
|
179
|
+
const say = `${NOTE[r.status]} — ${r.detail}${CONSEQUENCE[r.status]}`;
|
|
180
|
+
health[codex.id] = { ...h, codexCommit: r.status, note: `${h.note ? `${h.note}; ` : ""}${say}` };
|
|
181
|
+
}
|
|
182
|
+
const ESCAPE_ACTION = "stop Codex seats and inspect .git/hooks and git config --list --show-origin (core.hooksPath, core.fsmonitor, filter.*, diff.external) before dispatch";
|
|
183
|
+
/** Plan's headline beneath a Codex worker row; null for every other worker and for an allowed probe.
|
|
184
|
+
* An absent field is unknown, but only the real adapter earns the line — stubs stay byte-identical. */
|
|
185
|
+
export function codexCommitHeadline(worker, adapters, health) {
|
|
186
|
+
if (worker !== codex.id || !adapters.includes(codex))
|
|
187
|
+
return null;
|
|
188
|
+
const status = codexCommitStatus(health);
|
|
189
|
+
if (status === "escape") {
|
|
190
|
+
return ` BLOCKING security warning: codex sandbox escape (doctor probe) — this worker's launch grant let the sandbox write shared git metadata; ${ESCAPE_ACTION}`;
|
|
191
|
+
}
|
|
192
|
+
if (status === "protected") {
|
|
193
|
+
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";
|
|
194
|
+
}
|
|
195
|
+
return status === "unknown"
|
|
196
|
+
? " worker restriction: unknown — the codex linked-worktree commit probe has no verdict; run tickmarkr doctor"
|
|
197
|
+
: null;
|
|
198
|
+
}
|
|
199
|
+
/** Plan's one line for Codex's NON-worker roles (judge, review, consult): they launch with the same grant, so
|
|
200
|
+
* only an escape earns it — allowed, protected and unknown concern commits, which those roles never make. */
|
|
201
|
+
export function codexEscapeRoleLine(roles, adapters, health) {
|
|
202
|
+
if (!roles.length || !adapters.includes(codex) || codexCommitStatus(health) !== "escape")
|
|
203
|
+
return null;
|
|
204
|
+
return `BLOCKING security warning: codex sandbox escape (doctor probe) — Codex ${roles.join("/")} seats launch with the worker grant that let the sandbox write shared git metadata; ${ESCAPE_ACTION}`;
|
|
205
|
+
}
|
package/dist/adapters/codex.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export declare function readCodexModelsCache(path?: string): {
|
|
|
4
4
|
fetchedAt?: string;
|
|
5
5
|
retirements?: Record<string, ModelRetirement | null>;
|
|
6
6
|
};
|
|
7
|
+
export declare const CODEX_GIT_GRANT = "-c \"sandbox_workspace_write.writable_roots=[$(tkr_common=$(git rev-parse --path-format=absolute --git-common-dir); tkr_gitdir=$(git rev-parse --absolute-git-dir); if [ \"$tkr_gitdir\" != \"$tkr_common\" ]; then printf '\"%s\",' \"$tkr_gitdir\"; fi; printf '\"%s/objects\",\"%s/refs\",\"%s/logs\"' \"$tkr_common\" \"$tkr_common\" \"$tkr_common\")]\"";
|
|
7
8
|
export declare const CODEX_TRUST_DIALOG: TrustDialog;
|
|
8
9
|
export declare const CODEX_INPUT_BOX: import("./types.js").InputBox;
|
|
9
10
|
export declare function seedCodexTrust(repoRoot: string, configPath?: string): TrustVerdict;
|
package/dist/adapters/codex.js
CHANGED
|
@@ -96,9 +96,15 @@ function codexRetirement(upgrade) {
|
|
|
96
96
|
}
|
|
97
97
|
// tickmarkr worktrees keep their gitdir under the MAIN repo's .git/worktrees/<name> — outside the
|
|
98
98
|
// workspace-write sandbox root — so git commit dies on index.lock (incident run-20260709-104447
|
|
99
|
-
// P87-08: "Unable to create …/.git/worktrees/…/index.lock"). Expanded by the
|
|
100
|
-
// worktree cwd
|
|
101
|
-
|
|
99
|
+
// P87-08: "Unable to create …/.git/worktrees/…/index.lock"). Expanded by the launching shell at the
|
|
100
|
+
// worktree cwd, for EVERY Codex role (worker, judge, review, consult share these builders).
|
|
101
|
+
// v2.6.6 T9 (K, D-912): the grant is exactly what a linked-worktree branch commit writes — the worktree's
|
|
102
|
+
// own gitdir (only when it differs from the common dir), common/objects, common/refs, common/logs. The
|
|
103
|
+
// shipped [common] grant (and D-910's [common, gitdir]) let a sandboxed seat write <common>/hooks and
|
|
104
|
+
// <common>/config, which unsandboxed git later runs. No common-root fallback: an ordinary checkout gets
|
|
105
|
+
// no gitdir root and its commit fails closed. packed-refs is not granted (packed-ref ops fail closed).
|
|
106
|
+
// doctor's probe (codex-commit-check.ts) executes these exact bytes; it never rebuilds the grant.
|
|
107
|
+
export const CODEX_GIT_GRANT = `-c "sandbox_workspace_write.writable_roots=[$(tkr_common=$(git rev-parse --path-format=absolute --git-common-dir); tkr_gitdir=$(git rev-parse --absolute-git-dir); if [ "$tkr_gitdir" != "$tkr_common" ]; then printf '"%s",' "$tkr_gitdir"; fi; printf '"%s/objects","%s/refs","%s/logs"' "$tkr_common" "$tkr_common" "$tkr_common")]"`;
|
|
102
108
|
// OBS-125: codex 0.144.x gates enabled hooks behind PER-PROJECT-PATH hook trust — a NEW gate distinct
|
|
103
109
|
// from the directory trust seedCodexTrust seeds. Every worker runs in a fresh ephemeral worktree, an
|
|
104
110
|
// untrusted project, so codex re-prompts "Hooks need review" and the interactive worker stalls forever
|
|
@@ -257,12 +263,12 @@ export const codex = {
|
|
|
257
263
|
probe: async () => probeVersion("codex"),
|
|
258
264
|
channels: (cfg) => channelsFromConfig("codex", cfg),
|
|
259
265
|
// v1.65 T3: every flag the command builder below hardcodes (incl. codexMcpSuppressionFlags' -c/
|
|
260
|
-
// --disable and
|
|
266
|
+
// --disable and CODEX_GIT_GRANT's -c) — all listed by top-level `codex --help`, verified 2026-07-22.
|
|
261
267
|
hardcodedFlags: { binary: "codex", flags: ["--sandbox", "-a", "-s", "--model", "-c", "--disable", "--dangerously-bypass-hook-trust"] },
|
|
262
268
|
// --sandbox workspace-write is the autonomous sandbox mode (codex v0.144.1+)
|
|
263
269
|
// MCP suppression built per dispatch (config can change between runs) — see codexMcpSuppressionFlags.
|
|
264
270
|
// CODEX_HOOK_TRUST (OBS-125) clears the per-worktree "Hooks need review" gate while keeping the sandbox.
|
|
265
|
-
headlessCommand: (promptFile, model, effort) => `codex exec --sandbox workspace-write ${CODEX_HOOK_TRUST} ${codexMcpSuppressionFlags()} ${
|
|
271
|
+
headlessCommand: (promptFile, model, effort) => `codex exec --sandbox workspace-write ${CODEX_HOOK_TRUST} ${codexMcpSuppressionFlags()} ${CODEX_GIT_GRANT}${codexEffortFlag(effort)} --model ${shq(model)} - < ${shq(promptFile)}`,
|
|
266
272
|
// OBS-930: the visible pane runs the REAL TUI. Codex's TUI takes its prompt only as the [PROMPT]
|
|
267
273
|
// positional (`codex --help`, 0.153.4 — no file/stdin form), so the launch inlines the file exactly
|
|
268
274
|
// as the claude adapter does: the prompt is the LAST positional and every flag value is followed by
|
|
@@ -274,7 +280,7 @@ export const codex = {
|
|
|
274
280
|
// OBS-930 (Linux): the inlined prompt is ONE argv string and Linux caps one at 131072 bytes, so a
|
|
275
281
|
// prompt over promptArgvCeiling() returns null → worker-mode-fallback → the headless form (types.ts).
|
|
276
282
|
interactiveCommand: (promptFile, model, effort) => promptFitsArgv(promptFile)
|
|
277
|
-
? `codex -a never -s workspace-write ${CODEX_HOOK_TRUST} ${codexMcpSuppressionFlags()} ${
|
|
283
|
+
? `codex -a never -s workspace-write ${CODEX_HOOK_TRUST} ${codexMcpSuppressionFlags()} ${CODEX_GIT_GRANT}${codexEffortFlag(effort)} --model ${shq(model)} "$(cat ${shq(promptFile)})"`
|
|
278
284
|
: null,
|
|
279
285
|
invoke(task, _cwd, a, ctx) {
|
|
280
286
|
return { command: this.headlessCommand(ctx.promptFile, a.model, a.effort) };
|
|
@@ -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;
|
|
@@ -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`,
|
package/dist/adapters/prompt.js
CHANGED
|
@@ -130,8 +130,9 @@ export function parseWorkerResult(raw, nonce) {
|
|
|
130
130
|
}
|
|
131
131
|
// Signatures anchor distinctive CLI-error phrasing — never bare fragments or bare status-code
|
|
132
132
|
// numbers (the QUOTA_RE Pitfall-3 lesson): these run only over no-trailer output, but a stalled
|
|
133
|
-
// worker's harvested pane can still contain ordinary work text.
|
|
134
|
-
|
|
133
|
+
// worker's harvested pane can still contain ordinary work text. E1: the recorded refresh failure
|
|
134
|
+
// ("access token could not be refreshed") is auth-required — classified, never re-probed or relaunched.
|
|
135
|
+
const AUTH_RE = /not logged in|please (?:log ?in|sign in)|please run [^\n]{0,30}log ?in|authentication[ _](?:required|failed|error)|invalid (?:api key|credentials)|api key (?:is )?(?:not set|missing|invalid|required)|credentials? (?:have )?expired|401 unauthorized|access token could not be refreshed/i;
|
|
135
136
|
const SETUP_RE = /command not found|not recognized as an internal or external command|spawn \S+ ENOENT|missing required config|workspace trust (?:required|not granted)/i;
|
|
136
137
|
const OUTAGE_RE = /unable to reach the model provider|cannot reach the model provider|model provider.{0,40}(?:unavailable|unreachable)|service (?:is )?temporarily unavailable|upstream connect error|overloaded_error/i;
|
|
137
138
|
const TIMEOUT_RE = /\bETIMEDOUT\b|request timed out|connection timed out|deadline exceeded|timed out waiting for/i;
|