tickmarkr 2.2.1 → 2.4.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 +10 -9
- package/dist/adapters/catalog-remote.d.ts +1 -4
- package/dist/adapters/catalog-remote.js +52 -42
- package/dist/adapters/catalog.js +5 -3
- package/dist/adapters/claude-code.d.ts +1 -1
- package/dist/adapters/claude-code.js +8 -5
- package/dist/adapters/model-lints.d.ts +9 -5
- package/dist/adapters/model-lints.js +56 -15
- package/dist/adapters/model-windows.js +11 -0
- package/dist/adapters/prompt.js +1 -0
- package/dist/adapters/qwen.d.ts +5 -0
- package/dist/adapters/qwen.js +153 -0
- package/dist/adapters/types.d.ts +21 -1
- package/dist/adapters/types.js +43 -2
- package/dist/cli/commands/approve.js +5 -4
- package/dist/cli/commands/beat.js +7 -4
- package/dist/cli/commands/compile.js +32 -6
- package/dist/cli/commands/doctor.d.ts +9 -4
- package/dist/cli/commands/doctor.js +87 -13
- package/dist/cli/commands/fleet.d.ts +4 -0
- package/dist/cli/commands/fleet.js +53 -14
- package/dist/cli/commands/init.js +36 -21
- package/dist/cli/commands/plan.js +45 -7
- package/dist/cli/commands/report.js +37 -1
- package/dist/cli/commands/status.d.ts +1 -0
- package/dist/cli/commands/status.js +45 -1
- package/dist/cli/commands/verify.d.ts +6 -0
- package/dist/cli/commands/verify.js +145 -25
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/compile/collateral.d.ts +2 -9
- package/dist/compile/collateral.js +17 -18
- package/dist/compile/index.d.ts +4 -1
- package/dist/compile/index.js +41 -7
- package/dist/compile/native.d.ts +4 -2
- package/dist/compile/native.js +58 -9
- package/dist/compile/ownership.js +34 -9
- package/dist/config/config.d.ts +1 -0
- package/dist/config/config.js +52 -6
- package/dist/drivers/herdr.d.ts +1 -0
- package/dist/drivers/herdr.js +11 -1
- package/dist/drivers/index.d.ts +6 -0
- package/dist/drivers/index.js +19 -4
- package/dist/drivers/orca.d.ts +35 -1
- package/dist/drivers/orca.js +260 -20
- package/dist/drivers/subprocess.d.ts +3 -3
- package/dist/drivers/subprocess.js +16 -9
- package/dist/drivers/types.d.ts +12 -0
- package/dist/gates/baseline.d.ts +2 -0
- package/dist/gates/baseline.js +47 -11
- package/dist/gates/llm.d.ts +6 -0
- package/dist/gates/llm.js +25 -9
- package/dist/gates/review.d.ts +7 -3
- package/dist/gates/review.js +61 -22
- package/dist/gates/run-gates.d.ts +5 -2
- package/dist/gates/run-gates.js +50 -26
- package/dist/gates/verdict-cause.d.ts +6 -2
- package/dist/gates/verdict-cause.js +8 -4
- package/dist/route/preference.d.ts +4 -0
- package/dist/route/preference.js +40 -0
- package/dist/route/router.js +15 -2
- package/dist/run/consult.d.ts +1 -0
- package/dist/run/consult.js +39 -8
- package/dist/run/daemon.d.ts +16 -0
- package/dist/run/daemon.js +345 -74
- package/dist/run/git.d.ts +3 -0
- package/dist/run/git.js +40 -5
- package/dist/run/journal.d.ts +15 -2
- package/dist/run/journal.js +73 -12
- package/dist/run/supervision.d.ts +6 -0
- package/dist/run/supervision.js +29 -1
- package/dist/tui/ink/fleet-app.d.ts +4 -0
- package/dist/tui/ink/fleet-app.js +45 -16
- package/dist/tui/ink/init-app.js +4 -4
- package/package.json +59 -1
- package/skills/tickmarkr-overseer/SKILL.md +77 -18
- package/skills/tickmarkr-overseer/scripts/seat-send.sh +88 -18
- package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +36 -2
- package/skills/tickmarkr-overseer/scripts/watch-contamination.sh +36 -15
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +33 -7
- package/skills/tickmarkr-overseer/scripts/watch-pending-input.sh +32 -8
package/dist/adapters/types.d.ts
CHANGED
|
@@ -116,7 +116,24 @@ export declare const TrustDialogSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
116
116
|
reason: z.ZodString;
|
|
117
117
|
}, z.core.$strict>]>;
|
|
118
118
|
export declare function matchesTrustDialog(paneText: string, dialog: TrustDialog): dialog is CapturedTrustDialog;
|
|
119
|
+
export declare const ADAPTER_PROMPT_GLYPHS: {
|
|
120
|
+
readonly "claude-code": "❯";
|
|
121
|
+
readonly codex: "›";
|
|
122
|
+
readonly "cursor-agent": ">";
|
|
123
|
+
readonly opencode: ">";
|
|
124
|
+
readonly pi: ">";
|
|
125
|
+
readonly grok: ">";
|
|
126
|
+
readonly kimi: ">";
|
|
127
|
+
readonly qwen: ">";
|
|
128
|
+
readonly omp: ">";
|
|
129
|
+
readonly agy: ">";
|
|
130
|
+
readonly "prime-agent": ">";
|
|
131
|
+
readonly fake: ">";
|
|
132
|
+
};
|
|
133
|
+
export type PromptGlyph = (typeof ADAPTER_PROMPT_GLYPHS)[keyof typeof ADAPTER_PROMPT_GLYPHS];
|
|
134
|
+
export declare function declaredPromptGlyphForAdapter(adapterId: string): PromptGlyph | undefined;
|
|
119
135
|
export interface InputBox {
|
|
136
|
+
promptGlyph?: PromptGlyph;
|
|
120
137
|
fingerprint: string;
|
|
121
138
|
match?(paneText: string): boolean;
|
|
122
139
|
emptyMatch?(paneText: string): boolean;
|
|
@@ -124,8 +141,11 @@ export interface InputBox {
|
|
|
124
141
|
launchCommand?(command: string): boolean;
|
|
125
142
|
readinessTimeoutMs?: number;
|
|
126
143
|
}
|
|
127
|
-
export declare function declareInputBox(adapterId: string, inputBox: InputBox
|
|
144
|
+
export declare function declareInputBox(adapterId: string, inputBox: Omit<InputBox, "promptGlyph"> & {
|
|
145
|
+
promptGlyph?: PromptGlyph;
|
|
146
|
+
}): InputBox;
|
|
128
147
|
export declare function declaredInputBoxForWorkerName(workerName: string): InputBox | undefined;
|
|
148
|
+
export declare function declaredPromptGlyphForWorkerName(workerName: string): PromptGlyph | undefined;
|
|
129
149
|
export declare function matchesInputBox(paneText: string, inputBox: InputBox): boolean;
|
|
130
150
|
export declare function matchesEmptyInputBox(paneText: string, inputBox: InputBox): boolean;
|
|
131
151
|
export declare function matchesOccupiedInputBox(paneText: string, inputBox: InputBox): boolean;
|
package/dist/adapters/types.js
CHANGED
|
@@ -129,15 +129,56 @@ export function matchesTrustDialog(paneText, dialog) {
|
|
|
129
129
|
return false;
|
|
130
130
|
return dialog.fingerprint.trim().length > 0 && paneText.includes(dialog.fingerprint);
|
|
131
131
|
}
|
|
132
|
+
// v1.75 T1 / OBS-136: an adapter whose steady-state TUI presents a bordered input box declares
|
|
133
|
+
// one distinctive pane-text fingerprint. The herdr driver associates declarations with worker
|
|
134
|
+
// slots by the existing adapter-bearing dispatch name before that name is canonicalized.
|
|
135
|
+
// v1.77 / OBS-142: launchCommand identifies the adapter-owned bootstrap command that necessarily
|
|
136
|
+
// precedes the box; that command settles on a clean shell line, while every other delivery waits
|
|
137
|
+
// for the declared box itself. readinessTimeoutMs bounds that evidence loop per adapter.
|
|
138
|
+
// v1.85 T5 / OBS-140: typed delivery is licensed by DECLARED states, never by transcript shape.
|
|
139
|
+
// `match` is the box painted, `emptyMatch` the box carrying nothing, `occupiedMatch` the box still
|
|
140
|
+
// holding a prompt. An adapter may pin `occupiedMatch` directly, or leave it derived from the other
|
|
141
|
+
// two — but an adapter that declares neither cannot acknowledge a submission and is refused.
|
|
142
|
+
// OBS-620: prompt glyphs are adapter facts, just like the input-box matchers below. Keep the shipped
|
|
143
|
+
// set in one shell-readable table: the overseer receipt cannot import TypeScript, but it reads these
|
|
144
|
+
// exact declarations from src/ in a checkout or dist/ in an installed package. A newly shipped
|
|
145
|
+
// adapter therefore has one visible place where omission can be enumerated and refused.
|
|
146
|
+
export const ADAPTER_PROMPT_GLYPHS = {
|
|
147
|
+
"claude-code": "❯",
|
|
148
|
+
"codex": "›",
|
|
149
|
+
"cursor-agent": ">",
|
|
150
|
+
"opencode": ">",
|
|
151
|
+
"pi": ">",
|
|
152
|
+
"grok": ">",
|
|
153
|
+
"kimi": ">",
|
|
154
|
+
"qwen": ">",
|
|
155
|
+
"omp": ">",
|
|
156
|
+
"agy": ">",
|
|
157
|
+
"prime-agent": ">",
|
|
158
|
+
// Test-only, but declared so a fake interactive slot exercises the same fail-closed contract.
|
|
159
|
+
"fake": ">",
|
|
160
|
+
};
|
|
161
|
+
export function declaredPromptGlyphForAdapter(adapterId) {
|
|
162
|
+
return ADAPTER_PROMPT_GLYPHS[adapterId];
|
|
163
|
+
}
|
|
132
164
|
const inputBoxes = new Map();
|
|
133
165
|
export function declareInputBox(adapterId, inputBox) {
|
|
134
|
-
|
|
135
|
-
|
|
166
|
+
const promptGlyph = inputBox.promptGlyph ?? declaredPromptGlyphForAdapter(adapterId);
|
|
167
|
+
// The shipped-adapter registry is audited separately against ADAPTER_PROMPT_GLYPHS. Keep this
|
|
168
|
+
// declaration helper open to synthetic and extension adapters: Herdr's input-state tests register
|
|
169
|
+
// those at module load, and prompt glyphs are not part of its typed-delivery decision.
|
|
170
|
+
const declared = promptGlyph === undefined ? inputBox : { ...inputBox, promptGlyph };
|
|
171
|
+
inputBoxes.set(adapterId, declared);
|
|
172
|
+
return declared;
|
|
136
173
|
}
|
|
137
174
|
export function declaredInputBoxForWorkerName(workerName) {
|
|
138
175
|
const adapterId = /^.+-worker-(.+)-a\d+-.+$/.exec(workerName)?.[1];
|
|
139
176
|
return adapterId === undefined ? undefined : inputBoxes.get(adapterId);
|
|
140
177
|
}
|
|
178
|
+
export function declaredPromptGlyphForWorkerName(workerName) {
|
|
179
|
+
const adapterId = /^.+-worker-(.+)-a\d+-.+$/.exec(workerName)?.[1];
|
|
180
|
+
return adapterId === undefined ? undefined : declaredPromptGlyphForAdapter(adapterId);
|
|
181
|
+
}
|
|
141
182
|
export function matchesInputBox(paneText, inputBox) {
|
|
142
183
|
return inputBox.match?.(paneText) ?? paneText.includes(inputBox.fingerprint);
|
|
143
184
|
}
|
|
@@ -11,7 +11,7 @@ export const APPROVAL_DISPOSITIONS = ["dispatch", "waive-gate", "re-dispatch", "
|
|
|
11
11
|
export const APPROVAL_ENACTS = {
|
|
12
12
|
dispatch: "dispatch it",
|
|
13
13
|
"waive-gate": "continue past the approved gate",
|
|
14
|
-
"re-dispatch": "re-dispatch against the full gate suite",
|
|
14
|
+
"re-dispatch": "re-dispatch against the full gate suite only if re-running the whole declared battery on the parked commit before any worker is red",
|
|
15
15
|
"fund-fixed-attempt": "dispatch a fixed attempt carrying the findings",
|
|
16
16
|
"fresh-budget": "dispatch it on a fresh attempt budget",
|
|
17
17
|
};
|
|
@@ -85,6 +85,7 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
85
85
|
const lastHuman = events[lastHumanIndex];
|
|
86
86
|
const capPark = lastHuman?.data.kind === ATTEMPT_CAP_RELEASE;
|
|
87
87
|
const gateFailPark = lastHuman?.data.kind === "gate-fail";
|
|
88
|
+
const infraPark = lastHuman?.data.kind === "infra";
|
|
88
89
|
const failedGate = gateFailPark ? failedGateForNewestPark(events, taskId, lastHumanIndex) : undefined;
|
|
89
90
|
if (gateFailPark && !failedGate) {
|
|
90
91
|
throw new Error(`task ${taskId} is parked on gate-fail but has no failed gate result on the newest park — refusing to infer one`);
|
|
@@ -104,8 +105,8 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
104
105
|
return disposition(cwd, runId, "fund-fixed-attempt", `upheld the reviewer for ${taskId} in ${runId} — by ${by}`, serialization.contended);
|
|
105
106
|
}
|
|
106
107
|
if (recheck) {
|
|
107
|
-
if (!gateFailPark || !failedGate) {
|
|
108
|
-
throw new Error(`--recheck applies to a gate-fail park; ${taskId}'s newest park is ${String(lastHuman?.data.kind ?? "none")} with failed gate ${failedGate ?? "none"} — refusing`);
|
|
108
|
+
if ((!gateFailPark || !failedGate) && !infraPark) {
|
|
109
|
+
throw new Error(`--recheck applies to a gate-fail or infra park; ${taskId}'s newest park is ${String(lastHuman?.data.kind ?? "none")} with failed gate ${failedGate ?? "none"} — refusing`);
|
|
109
110
|
}
|
|
110
111
|
journal.append("task-approved", taskId, {
|
|
111
112
|
by,
|
|
@@ -114,7 +115,7 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
114
115
|
release: RECHECK_RELEASE,
|
|
115
116
|
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
|
|
116
117
|
});
|
|
117
|
-
return disposition(cwd, runId, "re-dispatch", `re-checking ${taskId} in ${runId} — by ${by}; failed gate ${failedGate}; no gate marked satisfied`, serialization.contended);
|
|
118
|
+
return disposition(cwd, runId, "re-dispatch", `re-checking ${taskId} in ${runId} — by ${by}; ${failedGate ? `failed gate ${failedGate}` : "infra park"}; no gate marked satisfied`, serialization.contended);
|
|
118
119
|
}
|
|
119
120
|
if (waive) {
|
|
120
121
|
if (!gateFailPark || !failedGate) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { tickmarkrDir } from "../../graph/graph.js";
|
|
4
|
-
import { SUPERVISION_BEAT_MS, SUPERVISION_DEFAULT_THRESHOLD_PCT, SUPERVISION_STALE_MS, SUPERVISION_TIERS, beatSupervision, supervisionStandDownPath, } from "../../run/supervision.js";
|
|
4
|
+
import { SUPERVISION_BEAT_MS, SUPERVISION_DEFAULT_THRESHOLD_PCT, SUPERVISION_STALE_MS, SUPERVISION_TIERS, beatSupervision, resolveSupervisionRoot, supervisionStandDownPath, } from "../../run/supervision.js";
|
|
5
5
|
// SUP-04: the writer side of supervision, as a VERB. `beatSupervision` and `SUPERVISION_BEAT_MS` shipped
|
|
6
6
|
// with exactly one in-repo caller — the daemon, on one tier — so `status` printed
|
|
7
7
|
// `orchestrator ARMED / overseer ABSENT / watch ABSENT` while a real overseer worked the run: two thirds
|
|
@@ -41,14 +41,17 @@ export async function beat(argv, cwd = process.cwd()) {
|
|
|
41
41
|
throw new Error(`${named} needs --seat <identity> — a beat that names no seat arms a tier nobody occupies` +
|
|
42
42
|
" (pass the seat's own pane id or agent name)");
|
|
43
43
|
}
|
|
44
|
+
// A beat names repository state, never the caller's incidental directory. Resolution is read-only
|
|
45
|
+
// and happens before every write, so a non-repository invocation cannot create the state it claims.
|
|
46
|
+
const repoRoot = resolveSupervisionRoot(cwd);
|
|
44
47
|
if (standDown)
|
|
45
|
-
return standDownTier(
|
|
48
|
+
return standDownTier(repoRoot, named, seat);
|
|
46
49
|
// Clear a stand-down marker left by an earlier session BEFORE beating: a valid marker outranks every
|
|
47
50
|
// beat that does not strictly follow it, and two writes landing in the same millisecond do not. An
|
|
48
51
|
// uncleared marker would render DISARMED while this verb claimed ARMED, so the removal is unguarded
|
|
49
52
|
// too — `force` makes the ordinary "no marker" case a no-op, and anything else is a real failure.
|
|
50
|
-
rmSync(supervisionStandDownPath(
|
|
51
|
-
beatSupervision(
|
|
53
|
+
rmSync(supervisionStandDownPath(repoRoot, named), { force: true, recursive: true });
|
|
54
|
+
beatSupervision(repoRoot, named, seat, pct === undefined ? undefined : { armId: armId ?? seat, pct, thresholdPct });
|
|
52
55
|
return `${named} ARMED as ${seat} — beat again every ${SUPERVISION_BEAT_MS / 1_000}s; the tier reads STALE ${SUPERVISION_STALE_MS / 1_000}s after the last beat`;
|
|
53
56
|
}
|
|
54
57
|
/** `--seat <identity>` or `--seat=<identity>`; blank and missing are the same answer — none. */
|
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
import { isAbsolute, join } from "node:path";
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
3
|
import { collateralLints, sourceScopeLints } from "../../compile/collateral.js";
|
|
4
|
+
import { CompileError } from "../../compile/common.js";
|
|
4
5
|
import { compileSource } from "../../compile/index.js";
|
|
5
6
|
import { saveGraph, stateDirName } from "../../graph/graph.js";
|
|
6
7
|
import { formatPriorFindingEvidence, readPriorRunEvidence } from "../../run/journal.js";
|
|
7
8
|
import { shGit } from "../../run/git.js";
|
|
8
9
|
import { acquireRunLock, releaseRunLock } from "../../run/lock.js";
|
|
9
10
|
import { harnessLine, resolveHarness } from "../harness.js";
|
|
11
|
+
function nativeSourceScopeErrors(graph, lints) {
|
|
12
|
+
const byId = new Map(graph.tasks.map((task) => [task.id, task]));
|
|
13
|
+
const errors = [];
|
|
14
|
+
for (const lint of lints) {
|
|
15
|
+
const match = lint.match(/^(\S+): criteria implicate out-of-scope source not in files\[\]: (.*)$/);
|
|
16
|
+
if (!match)
|
|
17
|
+
continue;
|
|
18
|
+
const task = byId.get(match[1]);
|
|
19
|
+
if (!task)
|
|
20
|
+
continue;
|
|
21
|
+
const paths = match[2].replace(/ \(capped\)$/, "").split(", ").filter(Boolean);
|
|
22
|
+
for (const path of paths) {
|
|
23
|
+
if (task.goal.includes(`scope-waiver: ${path}`))
|
|
24
|
+
continue;
|
|
25
|
+
errors.push(`${task.id}: ${path} requires scope-waiver: ${path} in the task goal`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return errors;
|
|
29
|
+
}
|
|
10
30
|
async function mergedPendingDiagnostics(cwd, pending, merges) {
|
|
11
31
|
const head = await shGit("git rev-parse HEAD", cwd);
|
|
12
32
|
const base = head.code === 0 ? head.stdout.trim() : "";
|
|
@@ -33,15 +53,25 @@ export async function compile(argv, cwd = process.cwd(), harnessFrom = process.a
|
|
|
33
53
|
options: {
|
|
34
54
|
type: { type: "string" },
|
|
35
55
|
"dry-run": { type: "boolean" },
|
|
56
|
+
strict: { type: "boolean" },
|
|
36
57
|
},
|
|
37
58
|
allowPositionals: true,
|
|
38
59
|
});
|
|
39
60
|
const src = positionals[0];
|
|
40
61
|
if (!src)
|
|
41
|
-
throw new Error("usage: tickmarkr compile <spec-dir-or-md> [--type speckit|prd|gsd|native] [--dry-run]");
|
|
62
|
+
throw new Error("usage: tickmarkr compile <spec-dir-or-md> [--type speckit|prd|gsd|native] [--dry-run] [--strict]");
|
|
42
63
|
// resolve against the target repo, not the process cwd (the CLI test passes a tmp repo)
|
|
43
64
|
// Both modes reach the same pure compiler; --dry-run only removes the lock/write side effect below.
|
|
44
|
-
const g = compileSource(isAbsolute(src) ? src : join(cwd, src), values.type, cwd
|
|
65
|
+
const g = compileSource(isAbsolute(src) ? src : join(cwd, src), values.type, cwd, // repo root: gsd stores context[0] repo-relative so workers resolve it inside their worktree
|
|
66
|
+
(plan) => plan, { strict: values.strict });
|
|
67
|
+
const scopeLints = [...collateralLints(g.tasks, cwd), ...sourceScopeLints(g.tasks, cwd)];
|
|
68
|
+
const diagnostics = scopeLints.length
|
|
69
|
+
? `\nscope lints:\n${scopeLints.map((lint) => ` ! ${lint}`).join("\n")}`
|
|
70
|
+
: "";
|
|
71
|
+
const unwaived = g.spec.source === "native" ? nativeSourceScopeErrors(g, scopeLints) : [];
|
|
72
|
+
if (unwaived.length > 0) {
|
|
73
|
+
throw new CompileError(`${src} has unwaived native source-scope authoring errors:\n${unwaived.map((line) => ` - ${line}`).join("\n")}${diagnostics}`);
|
|
74
|
+
}
|
|
45
75
|
// One bounded read supplies both cross-run surfaces: unresolved findings below and merge facts for
|
|
46
76
|
// the ancestry check. Neither fact mutates the compiled graph; status and every readiness predicate
|
|
47
77
|
// remain the source compiler's answer.
|
|
@@ -62,10 +92,6 @@ export async function compile(argv, cwd = process.cwd(), harnessFrom = process.a
|
|
|
62
92
|
const summary = values["dry-run"]
|
|
63
93
|
? `validated ${src} (${g.tasks.length} tasks, source ${g.spec.source}, hash ${g.spec.hash.slice(0, 12)}) — dry run; no graph written`
|
|
64
94
|
: `compiled ${src} → ${stateDir}/graph.json (${g.tasks.length} tasks, source ${g.spec.source}, hash ${g.spec.hash.slice(0, 12)})`;
|
|
65
|
-
const scopeLints = [...collateralLints(g.tasks, cwd), ...sourceScopeLints(g.tasks, cwd)];
|
|
66
|
-
const diagnostics = scopeLints.length
|
|
67
|
-
? `\nscope lints:\n${scopeLints.map((lint) => ` ! ${lint}`).join("\n")}`
|
|
68
|
-
: "";
|
|
69
95
|
const priorFindings = prior.findings.length
|
|
70
96
|
? `\nprior-run evidence:\n${prior.findings.map((finding) => ` ${formatPriorFindingEvidence(finding)}`).join("\n")}`
|
|
71
97
|
: "";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ClaudeAlias } from "../../adapters/claude-code.js";
|
|
2
2
|
import type { WorkerAdapter } from "../../adapters/types.js";
|
|
3
3
|
import { type KimiDoctorTurnResult } from "../../adapters/kimi.js";
|
|
4
|
-
import { type CatalogReadResult } from "../../adapters/catalog-remote.js";
|
|
4
|
+
import { type CatalogFetcher, type CatalogReadResult } from "../../adapters/catalog-remote.js";
|
|
5
5
|
import { type ShResult } from "../../run/git.js";
|
|
6
6
|
import { type VitestListResult } from "../../gates/acceptance.js";
|
|
7
7
|
/** Where a newer `table_<date>.csv` is discovered — the deployed site builds filenames by
|
|
@@ -14,11 +14,16 @@ export type DoctorOpts = {
|
|
|
14
14
|
resolveClaudeAliasIdentity?: (cwd: string, alias: ClaudeAlias) => string | undefined;
|
|
15
15
|
catalog?: CatalogReadResult;
|
|
16
16
|
catalogNow?: () => Date;
|
|
17
|
+
catalogFetcher?: CatalogFetcher;
|
|
17
18
|
/** init's between-acts surface: status rows only — the model matrix and inline drift stay
|
|
18
19
|
* behind `tickmarkr doctor` (files are still written; only the RETURNED string shrinks). */
|
|
19
20
|
compact?: boolean;
|
|
20
21
|
/** Test seam for the same `orca status --json` transport production invokes. */
|
|
21
22
|
orcaStatusProbe?: (cwd: string, binary: string) => Promise<ShResult>;
|
|
23
|
+
/** Test seam for Orca's authoritative per-agent hook coverage listing. */
|
|
24
|
+
orcaHooksStatusProbe?: (cwd: string, binary: string) => Promise<ShResult>;
|
|
25
|
+
/** Environment used only to explain whether auto-selection applies in this terminal. */
|
|
26
|
+
orcaEnv?: NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
22
27
|
/** Test seam for shell-path discovery; absence remains a normal doctor row, never an exception. */
|
|
23
28
|
resolveOrcaBinary?: (cwd: string) => string | undefined;
|
|
24
29
|
/** Test seam for the runner-owned JSON listing used by the acceptance-oracle report row. */
|
|
@@ -36,7 +41,7 @@ type OrcaCapability = {
|
|
|
36
41
|
* Capability-row `detail` stays hermetic — never `OrcaError.message`, which embeds volatile CLI
|
|
37
42
|
* stderr (Electron timestamps), so the row is byte-stable across runs.
|
|
38
43
|
*/
|
|
39
|
-
export declare function probeOrcaCapability(cwd: string, opts?: Pick<DoctorOpts, "orcaStatusProbe" | "resolveOrcaBinary">): Promise<OrcaCapability>;
|
|
44
|
+
export declare function probeOrcaCapability(cwd: string, opts?: Pick<DoctorOpts, "orcaStatusProbe" | "orcaHooksStatusProbe" | "resolveOrcaBinary" | "orcaEnv">): Promise<OrcaCapability>;
|
|
40
45
|
export declare function runnerIgnoreFinding(cwd: string): {
|
|
41
46
|
verdict: "pass" | "warn";
|
|
42
47
|
detail: string;
|
|
@@ -87,8 +92,8 @@ export declare function selfShadowFinding(ownVersion: string, cwd?: string, reso
|
|
|
87
92
|
* §4.4: `LIVEBENCH_TABLE_DATE` is a hand-bumped constant, and a constant nobody lints is a stale
|
|
88
93
|
* constant — the pinned table ages silently while every tier suggestion keeps citing it. Warns past
|
|
89
94
|
* 90 days, naming the pinned date and the listing where a newer table is discovered.
|
|
90
|
-
* ponytail:
|
|
91
|
-
*
|
|
95
|
+
* ponytail: the listing itself is never fetched; the operator (or RELEASING.md's release-time
|
|
96
|
+
* check) reads it and bumps the constant.
|
|
92
97
|
*/
|
|
93
98
|
export declare function liveBenchStalenessFinding(now: Date): string | undefined;
|
|
94
99
|
export declare function doctor(_argv: string[], cwd?: string, adapters?: WorkerAdapter[], opts?: DoctorOpts): Promise<string>;
|
|
@@ -15,7 +15,7 @@ import { HerdrDriver } from "../../drivers/herdr.js";
|
|
|
15
15
|
import { ORCA_FIXTURE_VERSION, parseEnvelope, resolveOrcaCliBinary } from "../../drivers/orca.js";
|
|
16
16
|
import { kimi, probeKimiDoctorTurn } from "../../adapters/kimi.js";
|
|
17
17
|
import { denyPreferCollisionLine, denyPreferCollisions, disallowedBy, excludedChannels, exclusionLine, preferRanks } from "../../route/preference.js";
|
|
18
|
-
import { LIVEBENCH_TABLE_DATE, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
|
|
18
|
+
import { CATALOG_REFRESH_TIMEOUT_MS, LIVEBENCH_TABLE_DATE, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
|
|
19
19
|
import { sh } from "../../run/git.js";
|
|
20
20
|
import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
|
|
21
21
|
/** Where a newer `table_<date>.csv` is discovered — the deployed site builds filenames by
|
|
@@ -25,6 +25,62 @@ export const LIVEBENCH_TABLE_MAX_AGE_DAYS = 90;
|
|
|
25
25
|
const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
26
26
|
const alignedStatusRow = (verdict, key, value) => ` ${statusRow(verdict, kvRow(key, value).slice(2))}`;
|
|
27
27
|
const attentionRow = (text) => ` ${statusRow("warn", text)}`;
|
|
28
|
+
const ORCA_HOOK_ADAPTERS = {
|
|
29
|
+
claude: "claude-code",
|
|
30
|
+
"claude-code": "claude-code",
|
|
31
|
+
codex: "codex",
|
|
32
|
+
cursor: "cursor-agent",
|
|
33
|
+
"cursor-agent": "cursor-agent",
|
|
34
|
+
grok: "grok",
|
|
35
|
+
kimi: "kimi",
|
|
36
|
+
opencode: "opencode",
|
|
37
|
+
pi: "pi",
|
|
38
|
+
};
|
|
39
|
+
function orcaSelectionDetail(env) {
|
|
40
|
+
const inside = env.TERM_PROGRAM === "Orca" && !!env.ORCA_TERMINAL_HANDLE?.trim();
|
|
41
|
+
return inside
|
|
42
|
+
? "auto picks orca in this Orca terminal"
|
|
43
|
+
: "not an Orca terminal; auto picks orca only inside one; use --driver orca";
|
|
44
|
+
}
|
|
45
|
+
async function orcaHookCoverage(cwd, binary, opts) {
|
|
46
|
+
// A supplied status transport is a hermetic boundary: do not escape it to a real binary for the
|
|
47
|
+
// second command. Tests (and embedders) that want coverage supply the matching hook transport.
|
|
48
|
+
if (opts.orcaHooksStatusProbe === undefined && opts.orcaStatusProbe !== undefined) {
|
|
49
|
+
return "hooks status unavailable";
|
|
50
|
+
}
|
|
51
|
+
let response;
|
|
52
|
+
try {
|
|
53
|
+
response = await (opts.orcaHooksStatusProbe ?? ((probeCwd, executable) => sh(`${shq(executable)} agent hooks status --json`, probeCwd, 10_000)))(cwd, binary);
|
|
54
|
+
if (response.code !== 0 || response.timedOut)
|
|
55
|
+
return "hooks status unavailable";
|
|
56
|
+
// Until the driver's response-family expansion lands, this still uses its one strict envelope
|
|
57
|
+
// parser. Coverage comes only from this command's `statuses[].state`; managedHooksPresent and
|
|
58
|
+
// local agent config files are deliberately not alternative oracles.
|
|
59
|
+
const envelope = parseEnvelope("status", response.stdout);
|
|
60
|
+
const statuses = envelope.result.statuses;
|
|
61
|
+
if (!Array.isArray(statuses))
|
|
62
|
+
return "hooks status unavailable";
|
|
63
|
+
const coverage = new Map();
|
|
64
|
+
for (const raw of statuses) {
|
|
65
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
66
|
+
continue;
|
|
67
|
+
const row = raw;
|
|
68
|
+
const adapter = typeof row.agent === "string" ? ORCA_HOOK_ADAPTERS[row.agent] : undefined;
|
|
69
|
+
if (!adapter)
|
|
70
|
+
continue;
|
|
71
|
+
if (row.state === "installed")
|
|
72
|
+
coverage.set(adapter, "hooked");
|
|
73
|
+
else if (row.state === "not_installed")
|
|
74
|
+
coverage.set(adapter, "unhooked");
|
|
75
|
+
}
|
|
76
|
+
return coverage.size
|
|
77
|
+
? `hooks: ${[...coverage].map(([adapter, state]) => `${adapter} ${state}`).join(", ")}`
|
|
78
|
+
: "hooks: no tickmarkr adapter status reported";
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return "hooks status unavailable";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
28
84
|
/**
|
|
29
85
|
* Orca's status body is deliberately interpreted by T1's one shared envelope parser. Doctor owns
|
|
30
86
|
* only capability presentation: it may classify an absent executable, but it never invents a second
|
|
@@ -63,13 +119,15 @@ export async function probeOrcaCapability(cwd, opts = {}) {
|
|
|
63
119
|
? runtime.reachable
|
|
64
120
|
: undefined;
|
|
65
121
|
if (reachable === true) {
|
|
122
|
+
const hooks = await orcaHookCoverage(cwd, binary, opts);
|
|
123
|
+
const selection = orcaSelectionDetail(opts.orcaEnv ?? process.env);
|
|
66
124
|
const appVersion = typeof runtime === "object" && runtime !== null && !Array.isArray(runtime)
|
|
67
125
|
? runtime.appVersion
|
|
68
126
|
: undefined;
|
|
69
127
|
if (typeof appVersion !== "string" || !appVersion) {
|
|
70
|
-
return { verdict: "warn", detail: `runtime reachable (${envelope.runtimeId}, appVersion absent; fixture pin ${ORCA_FIXTURE_VERSION})` };
|
|
128
|
+
return { verdict: "warn", detail: `runtime reachable (${envelope.runtimeId}, appVersion absent; fixture pin ${ORCA_FIXTURE_VERSION}); ${hooks} — ${selection}` };
|
|
71
129
|
}
|
|
72
|
-
const detail = `runtime reachable (${envelope.runtimeId}, appVersion ${appVersion}; fixture pin ${ORCA_FIXTURE_VERSION})`;
|
|
130
|
+
const detail = `runtime reachable (${envelope.runtimeId}, appVersion ${appVersion}; fixture pin ${ORCA_FIXTURE_VERSION}); ${hooks} — ${selection}`;
|
|
73
131
|
return { verdict: appVersion === ORCA_FIXTURE_VERSION ? "pass" : "warn", detail };
|
|
74
132
|
}
|
|
75
133
|
if (reachable === false)
|
|
@@ -278,8 +336,8 @@ export function selfShadowFinding(ownVersion, cwd = process.cwd(), resolve = res
|
|
|
278
336
|
* §4.4: `LIVEBENCH_TABLE_DATE` is a hand-bumped constant, and a constant nobody lints is a stale
|
|
279
337
|
* constant — the pinned table ages silently while every tier suggestion keeps citing it. Warns past
|
|
280
338
|
* 90 days, naming the pinned date and the listing where a newer table is discovered.
|
|
281
|
-
* ponytail:
|
|
282
|
-
*
|
|
339
|
+
* ponytail: the listing itself is never fetched; the operator (or RELEASING.md's release-time
|
|
340
|
+
* check) reads it and bumps the constant.
|
|
283
341
|
*/
|
|
284
342
|
export function liveBenchStalenessFinding(now) {
|
|
285
343
|
const pinnedMs = Date.parse(`${LIVEBENCH_TABLE_DATE.replace(/_/g, "-")}T00:00:00Z`);
|
|
@@ -298,9 +356,20 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
298
356
|
// Operator directive 2026-08-12 (declutter): long per-model lists render only when asked for.
|
|
299
357
|
const listAllModels = _argv.includes("--models");
|
|
300
358
|
const cfg = loadConfig(cwd);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
359
|
+
let catalog = opts.catalog ?? readCachedCatalog(cwd, { now: opts.catalogNow });
|
|
360
|
+
let catalogRefreshWarning;
|
|
361
|
+
// RULING-222-17 reverses cache-only for these two operator-facing commands only. The refresh
|
|
362
|
+
// remains age-guarded; compile, plan, and run never import this path.
|
|
363
|
+
if (!opts.catalog && catalog.source === "cache" && catalog.stale) {
|
|
364
|
+
const refreshed = await refreshCatalogCommand({
|
|
365
|
+
repoRoot: cwd,
|
|
366
|
+
fetcher: opts.catalogFetcher,
|
|
367
|
+
timeoutMs: CATALOG_REFRESH_TIMEOUT_MS,
|
|
368
|
+
now: opts.catalogNow,
|
|
369
|
+
});
|
|
370
|
+
catalog = refreshed.catalog;
|
|
371
|
+
catalogRefreshWarning = refreshed.warning;
|
|
372
|
+
}
|
|
304
373
|
// banner at START — the logo greets the operator before the ~60s probe wait, never trailing it (operator report 2026-07-17)
|
|
305
374
|
if (opts.banner !== false && visual())
|
|
306
375
|
process.stdout.write(BANNER);
|
|
@@ -397,6 +466,9 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
397
466
|
if (catalog.warning) {
|
|
398
467
|
rows.push(attentionRow(`model catalog cache unreadable — ${catalog.warning}; using vendored fallback (advisory — routing unchanged)`));
|
|
399
468
|
}
|
|
469
|
+
if (catalogRefreshWarning) {
|
|
470
|
+
rows.push(attentionRow(`model catalog auto-refresh failed open — ${catalogRefreshWarning}; retained ${catalog.source} catalog`));
|
|
471
|
+
}
|
|
400
472
|
// v1.48 T1 / v1.86 T12: advisory sweep for known agent CLIs with no drive contract. Presence is
|
|
401
473
|
// resolved through the worker's login shell; advisory targets are never executed or written to health.
|
|
402
474
|
rows.push(...detectCandidateClis({ cwd }).map(({ binary, path }) => alignedStatusRow("warn", binary, `detected at ${path} (no drive contract — not routable)`)));
|
|
@@ -459,8 +531,8 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
459
531
|
}
|
|
460
532
|
if (trustNa.length)
|
|
461
533
|
rows.push(` ${dim("=")} ${dim(`n/a (${trustNa.length}): ${trustNa.join(", ")}`)}`);
|
|
462
|
-
//
|
|
463
|
-
//
|
|
534
|
+
// Capability is diagnostic only: auto-selection is decided from terminal identity markers, never
|
|
535
|
+
// from this runtime probe, and a failed probe cannot silently substitute another driver.
|
|
464
536
|
const orca = await probeOrcaCapability(cwd, opts);
|
|
465
537
|
rows.push(legend("execution runtime:"));
|
|
466
538
|
rows.push(alignedStatusRow(orca.verdict, "orca", orca.detail));
|
|
@@ -577,9 +649,11 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
577
649
|
const probed = probedMs !== undefined ? dim(` ${(probedMs / 1000).toFixed(1)}s`) : "";
|
|
578
650
|
const auth = !v
|
|
579
651
|
? dim("unknown")
|
|
580
|
-
: v.
|
|
581
|
-
? `${
|
|
582
|
-
:
|
|
652
|
+
: v.probeError
|
|
653
|
+
? `${fail(`probe error (${v.probeError})`)}${probed}`
|
|
654
|
+
: v.authed
|
|
655
|
+
? `${ok("authed")}${probed}`
|
|
656
|
+
: `${fail("unauthed:")} ${trunc(v.reason ?? "probe failed", 40)} (${dateOf(v.probedAt)})`;
|
|
583
657
|
const d = disallowedBy({ adapter: a.id, model: m }, cfg.routing);
|
|
584
658
|
const denied = d?.by === "deny" ? d.entry : "—";
|
|
585
659
|
const pref = preferRanks({ adapter: a.id, model: m }, cfg).map((p) => `${p.shape}#${p.rank}`).join(",") || "—";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CatalogFetcher, type CatalogReadResult } from "../../adapters/catalog-remote.js";
|
|
1
2
|
import type { WorkerAdapter } from "../../adapters/types.js";
|
|
2
3
|
import type { FleetEditorResult } from "../../tui/ink/fleet-app.js";
|
|
3
4
|
type FleetEditorProps = Parameters<typeof import("../../tui/ink/fleet-app.js").runFleetInkEditor>[0];
|
|
@@ -16,6 +17,8 @@ export type FleetIO = {
|
|
|
16
17
|
output?: FleetOutput;
|
|
17
18
|
debug?: boolean;
|
|
18
19
|
reloadGuard?: (bytes: string) => string | null;
|
|
20
|
+
catalogFetcher?: CatalogFetcher;
|
|
21
|
+
catalogNow?: () => Date;
|
|
19
22
|
};
|
|
20
23
|
export type FleetWriteHooks = {
|
|
21
24
|
readPrior?: (path: string) => string;
|
|
@@ -36,6 +39,7 @@ export declare function fleet(argv: string[], cwd?: string, adapters?: WorkerAda
|
|
|
36
39
|
export declare function assembleFleetEditor(cwd: string, adapters: WorkerAdapter[], io: FleetIO, opts: {
|
|
37
40
|
globalDir: string;
|
|
38
41
|
entry?: "presets" | "probe";
|
|
42
|
+
catalog?: CatalogReadResult;
|
|
39
43
|
}): Promise<{
|
|
40
44
|
props: FleetEditorProps;
|
|
41
45
|
commit: (result: FleetEditorResult) => string;
|
|
@@ -3,7 +3,7 @@ import { dirname } from "node:path";
|
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse, modelAuthExclusions } from "../../adapters/registry.js";
|
|
5
5
|
import { catalogModelAdvisory, catalogTierRanking, declaredModelWindow, fleetUnclassifiedModels } from "../../adapters/model-lints.js";
|
|
6
|
-
import { readCachedCatalog } from "../../adapters/catalog-remote.js";
|
|
6
|
+
import { CATALOG_REFRESH_TIMEOUT_MS, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
|
|
7
7
|
import { CLAUDE_ALIAS_IDENTITY_STAMPS, readClaudeAliasIdentity } from "../../adapters/claude-code.js";
|
|
8
8
|
import { fleetEditableFromConfig, fleetEditableEquals, formatFleetPrint, globalConfigDir, overlayBytesLoadError, renderFleetOverlayWrite, repoOverlayPath, ROUTING_MODES, unifiedYamlDiff, } from "../../config/config.js";
|
|
9
9
|
import { projectFleetWhy, renderFleetWhy } from "../../config/fleet-why.js";
|
|
@@ -96,6 +96,19 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
96
96
|
const input = io.input ?? process.stdin;
|
|
97
97
|
const output = io.output ?? process.stdout;
|
|
98
98
|
const interactive = input.isTTY === true && output.isTTY === true;
|
|
99
|
+
let catalog = readCachedCatalog(cwd, { now: io.catalogNow });
|
|
100
|
+
let refreshReason = "";
|
|
101
|
+
if (catalog.source === "cache" && catalog.stale) {
|
|
102
|
+
const refreshed = await refreshCatalogCommand({
|
|
103
|
+
repoRoot: cwd,
|
|
104
|
+
fetcher: io.catalogFetcher,
|
|
105
|
+
timeoutMs: CATALOG_REFRESH_TIMEOUT_MS,
|
|
106
|
+
now: io.catalogNow,
|
|
107
|
+
});
|
|
108
|
+
catalog = refreshed.catalog;
|
|
109
|
+
if (refreshed.warning)
|
|
110
|
+
refreshReason = `fleet: catalog auto-refresh failed open — ${refreshed.warning}; retained ${catalog.source} catalog`;
|
|
111
|
+
}
|
|
99
112
|
if (print) {
|
|
100
113
|
// v1.51 T4: the print surface names the mode and its source layer right under the header —
|
|
101
114
|
// comment-prefixed so the YAML body stays machine-parseable and regex-stable.
|
|
@@ -104,10 +117,10 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
104
117
|
const nl = body.indexOf("\n");
|
|
105
118
|
// Steering comes from the same resolved config snapshot the editor consumes below,
|
|
106
119
|
// not from another parse of either raw overlay.
|
|
107
|
-
return `${body.slice(0, nl)}\n# mode: ${rm.mode.mode} (${rm.source})${body.slice(nl)}${formatFleetSteering(rm.cfg)}`;
|
|
120
|
+
return `${body.slice(0, nl)}\n# mode: ${rm.mode.mode} (${rm.source})${refreshReason ? `\n# ${refreshReason}` : ""}${body.slice(nl)}${formatFleetSteering(rm.cfg)}`;
|
|
108
121
|
}
|
|
109
122
|
if (!why && !interactive)
|
|
110
|
-
return { out: NON_TTY_MSG
|
|
123
|
+
return { out: `${refreshReason ? `${refreshReason}\n` : ""}${NON_TTY_MSG}`, code: 1 };
|
|
111
124
|
// OBS-528: `--fresh` parsed since v1.92 but nothing ever RAN the probe — it forced the reuse
|
|
112
125
|
// gate false and guaranteed the "probe data missing or stale" refusal. The law stands — the
|
|
113
126
|
// EDITOR never probes (previews stay cache-only, `r` still exits to the operator) — but the
|
|
@@ -119,11 +132,11 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
119
132
|
output.write(`${await doctor([], cwd, adapters, { banner: false, compact: true })}\n`);
|
|
120
133
|
}
|
|
121
134
|
}
|
|
122
|
-
const assembled = await assembleFleetEditor(cwd, adapters, io, { globalDir });
|
|
135
|
+
const assembled = await assembleFleetEditor(cwd, adapters, io, { globalDir, catalog });
|
|
123
136
|
if ("unavailable" in assembled)
|
|
124
|
-
return { out: assembled.unavailable
|
|
137
|
+
return { out: `${refreshReason ? `${refreshReason}\n` : ""}${assembled.unavailable}`, code: 1 };
|
|
125
138
|
if (why)
|
|
126
|
-
return assembled.renderWhy()
|
|
139
|
+
return `${refreshReason ? `${refreshReason}\n` : ""}${assembled.renderWhy()}`;
|
|
127
140
|
// Keep Ink out of print, non-TTY, and missing-probe paths: the component runtime belongs
|
|
128
141
|
// exclusively to the interactive editor. The capture window brackets only the dynamic Ink
|
|
129
142
|
// import — keys typed while the module loads land in props.initialInput, never get dropped.
|
|
@@ -155,7 +168,7 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
155
168
|
}
|
|
156
169
|
assembled.props.initialInput = initialInput;
|
|
157
170
|
const result = await runFleetInkEditor(assembled.props);
|
|
158
|
-
return assembled.commit(result)
|
|
171
|
+
return `${refreshReason ? `${refreshReason}\n` : ""}${assembled.commit(result)}`;
|
|
159
172
|
}
|
|
160
173
|
/** Everything between the doctor-reuse gate and the Ink render, packaged for reuse: `tickmarkr init`
|
|
161
174
|
* runs the same editor (entry="presets": Esc is HOME to the preset overlay, never a bare quit)
|
|
@@ -192,7 +205,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
192
205
|
// OBS-508: the same catalog evidence doctor's drift overlay prints now rides each unclassified
|
|
193
206
|
// row — it prefills the classify flow and feeds the bulk `s` stage. Suggestions stay advisory:
|
|
194
207
|
// only the review-diff confirm writes, so "tickmarkr never applies" holds with less typing.
|
|
195
|
-
const catalog = readCachedCatalog(cwd);
|
|
208
|
+
const catalog = opts.catalog ?? readCachedCatalog(cwd);
|
|
196
209
|
// The same alias→identity resolution doctor hands its advisory rows: models.dev has never heard of
|
|
197
210
|
// `opus`, so without it the fleet's own frontier models drop out of the universe they are supposed
|
|
198
211
|
// to anchor and fleet bands a different set than doctor for one fleet. The stored identity wins;
|
|
@@ -203,9 +216,32 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
203
216
|
: readClaudeAliasIdentity(cwd, model) ?? CLAUDE_ALIAS_IDENTITY_STAMPS[model];
|
|
204
217
|
// One ranking universe for the whole screen: every unclassified row bands fleet-relatively
|
|
205
218
|
// against the same set, so a suggestion never depends on which adapter group renders first.
|
|
206
|
-
const
|
|
219
|
+
const detectedRows = fleetUnclassifiedModels(cfg, health, adapters)
|
|
207
220
|
.map((row) => ({ ...row, resolvedModel: resolvedCatalogModel(row.adapter, row.model) }));
|
|
208
|
-
const catalogRanking = catalogTierRanking(cfg, catalog,
|
|
221
|
+
const catalogRanking = catalogTierRanking(cfg, catalog, detectedRows, resolvedCatalogModel);
|
|
222
|
+
const foldedRows = [];
|
|
223
|
+
const folds = new Map();
|
|
224
|
+
for (const row of detectedRows) {
|
|
225
|
+
const advisory = catalogModelAdvisory(cfg, catalog, row.adapter, row.model, row.resolvedModel, catalogRanking);
|
|
226
|
+
const evidence = advisory.coverage === "covered" ? advisory.evidence : undefined;
|
|
227
|
+
const score = evidence?.agenticCodingScore ?? evidence?.intelligenceIndex ?? evidence?.codingScore;
|
|
228
|
+
const next = { ...row, advisory, ...(score !== undefined ? { score } : {}) };
|
|
229
|
+
const foldKey = evidence ? `${row.adapter}:${evidence.catalogId}` : undefined;
|
|
230
|
+
const first = foldKey ? folds.get(foldKey) : undefined;
|
|
231
|
+
if (first) {
|
|
232
|
+
first.foldedModels ??= [first.model];
|
|
233
|
+
first.foldedModels.push(row.model);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
foldedRows.push(next);
|
|
237
|
+
if (foldKey)
|
|
238
|
+
folds.set(foldKey, next);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const unclassifiedRows = foldedRows.sort((a, b) => Number(!!b.advisory.suggestion) - Number(!!a.advisory.suggestion)
|
|
242
|
+
|| (b.detectedAt ?? "").localeCompare(a.detectedAt ?? "")
|
|
243
|
+
|| (b.score ?? Number.NEGATIVE_INFINITY) - (a.score ?? Number.NEGATIVE_INFINITY)
|
|
244
|
+
|| a.model.localeCompare(b.model));
|
|
209
245
|
// OBS-508 follow-through: the browser renders the metadata the assembler always had — catalog
|
|
210
246
|
// ctx/price evidence and doctor's model-probe wall clock — as columns instead of dropping them.
|
|
211
247
|
const rowEvidence = (adapter, model, evidence) => {
|
|
@@ -239,14 +275,17 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
239
275
|
...unclassified
|
|
240
276
|
.filter((row) => !editable.tiers[adapter.id]?.[row.model])
|
|
241
277
|
.map((row) => {
|
|
242
|
-
const
|
|
243
|
-
const evidence = rowEvidence(adapter.id, row.model, advisory.coverage === "covered" ? advisory.evidence : undefined);
|
|
278
|
+
const evidence = rowEvidence(adapter.id, row.classifyModel ?? row.model, row.advisory.coverage === "covered" ? row.advisory.evidence : undefined);
|
|
244
279
|
return {
|
|
245
280
|
model: row.model,
|
|
246
281
|
detectedAt: row.detectedAt,
|
|
282
|
+
...(row.classifyModel ? { classifyModel: row.classifyModel } : {}),
|
|
283
|
+
...(row.variants ? { variants: row.variants } : {}),
|
|
284
|
+
...(row.foldedModels ? { foldedModels: row.foldedModels } : {}),
|
|
285
|
+
...(row.score !== undefined ? { score: row.score } : {}),
|
|
247
286
|
...(evidence ? { evidence } : {}),
|
|
248
|
-
...(advisory.suggestion
|
|
249
|
-
? { suggestion: { tier: advisory.suggestion.tier, note: advisory.suggestion.provenanceNote } }
|
|
287
|
+
...(row.advisory.suggestion
|
|
288
|
+
? { suggestion: { tier: row.advisory.suggestion.tier, note: row.advisory.suggestion.provenanceNote } }
|
|
250
289
|
: {}),
|
|
251
290
|
};
|
|
252
291
|
}),
|