tickmarkr 1.70.0 → 1.72.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/dist/adapters/codex.js +13 -3
- package/dist/adapters/kimi.js +12 -35
- package/dist/adapters/registry.d.ts +25 -1
- package/dist/adapters/registry.js +95 -4
- package/dist/adapters/types.d.ts +8 -0
- package/dist/cli/commands/doctor.js +8 -3
- package/dist/cli/commands/fleet.d.ts +2 -0
- package/dist/cli/commands/fleet.js +212 -449
- package/dist/cli/commands/resume.js +7 -1
- package/dist/cli/commands/status.js +20 -8
- package/dist/drivers/herdr.d.ts +7 -0
- package/dist/drivers/herdr.js +119 -66
- package/dist/gates/acceptance.js +41 -17
- package/dist/route/preference.d.ts +9 -0
- package/dist/route/preference.js +61 -0
- package/dist/run/daemon.d.ts +4 -0
- package/dist/run/daemon.js +67 -10
- package/dist/run/interactive-seed.d.ts +1 -0
- package/dist/run/interactive-seed.js +12 -4
- package/dist/run/journal.d.ts +9 -1
- package/dist/run/journal.js +53 -2
- package/dist/tui/ink/components.d.ts +20 -0
- package/dist/tui/ink/components.js +14 -0
- package/dist/tui/ink/fleet-app.d.ts +113 -0
- package/dist/tui/ink/fleet-app.js +678 -0
- package/package.json +4 -1
package/dist/adapters/codex.js
CHANGED
|
@@ -80,6 +80,15 @@ export function readCodexModelsCache(path) {
|
|
|
80
80
|
// P87-08: "Unable to create …/.git/worktrees/…/index.lock"). Expanded by the pane shell at the
|
|
81
81
|
// worktree cwd; in a plain repo it resolves to ./.git (already writable, harmless).
|
|
82
82
|
const GITDIR_WRITABLE = `-c "sandbox_workspace_write.writable_roots=[\\"$(git rev-parse --path-format=absolute --git-common-dir)\\"]"`;
|
|
83
|
+
// OBS-125: codex 0.144.x gates enabled hooks behind PER-PROJECT-PATH hook trust — a NEW gate distinct
|
|
84
|
+
// from the directory trust seedCodexTrust seeds. Every worker runs in a fresh ephemeral worktree, an
|
|
85
|
+
// untrusted project, so codex re-prompts "Hooks need review" and the interactive worker stalls forever
|
|
86
|
+
// (run-20260722/23 incidents). This flag runs the operator's OWN already-trusted hooks — including the
|
|
87
|
+
// ~/.codex/hooks.json herdr-agent-state hook herdr relies on for agent detection, so the hooks can't
|
|
88
|
+
// just be disabled — WITHOUT requiring persisted per-path trust for this invocation. It KEEPS the
|
|
89
|
+
// -s/--sandbox workspace-write sandbox (deliberately NOT --dangerously-bypass-approvals-and-sandbox,
|
|
90
|
+
// which would drop the sandbox). Listed by `codex --help` and `codex exec --help` (verified 2026-07-23).
|
|
91
|
+
const CODEX_HOOK_TRUST = "--dangerously-bypass-hook-trust";
|
|
83
92
|
// v1.22 T5 / OBS-16: codex keys trust on absolute path under [projects."<root>"] trust_level="trusted"
|
|
84
93
|
// in ~/.codex/config.toml (CODEX_HOME relocates the dir). Worktrees inherit parent-project trust when
|
|
85
94
|
// the REPO ROOT is trusted — seed the root once, cover every future worktree. Idempotent: a second
|
|
@@ -171,14 +180,15 @@ export const codex = {
|
|
|
171
180
|
channels: (cfg) => channelsFromConfig("codex", cfg),
|
|
172
181
|
// v1.65 T3: every flag the command builders below hardcode (incl. codexMcpSuppressionFlags' -c/
|
|
173
182
|
// --disable and GITDIR_WRITABLE's -c) — all listed by top-level `codex --help`, verified 2026-07-22.
|
|
174
|
-
hardcodedFlags: { binary: "codex", flags: ["--sandbox", "--model", "-a", "-s", "-c", "--disable"] },
|
|
183
|
+
hardcodedFlags: { binary: "codex", flags: ["--sandbox", "--model", "-a", "-s", "-c", "--disable", "--dangerously-bypass-hook-trust"] },
|
|
175
184
|
// --sandbox workspace-write is the autonomous sandbox mode (codex v0.144.1+)
|
|
176
185
|
// MCP suppression built per dispatch (config can change between runs) — see codexMcpSuppressionFlags.
|
|
177
|
-
|
|
186
|
+
// CODEX_HOOK_TRUST (OBS-125) clears the per-worktree "Hooks need review" gate while keeping the sandbox.
|
|
187
|
+
headlessCommand: (promptFile, model) => `codex exec --sandbox workspace-write ${CODEX_HOOK_TRUST} ${codexMcpSuppressionFlags()} ${GITDIR_WRITABLE} --model ${shq(model)} "$(cat ${shq(promptFile)})"`,
|
|
178
188
|
// TUI uses expanded -a never -s workspace-write (exec-only flags do not apply)
|
|
179
189
|
// (--help 2026-07-09: valid approval policies are untrusted|on-request|never; the previously
|
|
180
190
|
// used `on-failure` is invalid and made codex exit 2 pre-inference)
|
|
181
|
-
interactiveCommand: (promptFile, model) => `codex -a never -s workspace-write ${codexMcpSuppressionFlags()} ${GITDIR_WRITABLE} --model ${shq(model)} "$(cat ${shq(promptFile)})"`,
|
|
191
|
+
interactiveCommand: (promptFile, model) => `codex -a never -s workspace-write ${CODEX_HOOK_TRUST} ${codexMcpSuppressionFlags()} ${GITDIR_WRITABLE} --model ${shq(model)} "$(cat ${shq(promptFile)})"`,
|
|
182
192
|
invoke(task, _cwd, a, ctx) {
|
|
183
193
|
return { command: this.headlessCommand(ctx.promptFile, a.model) };
|
|
184
194
|
},
|
package/dist/adapters/kimi.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
2
|
import { homedir } from "node:os";
|
|
4
3
|
import { join } from "node:path";
|
|
5
4
|
import { probeVersion } from "./claude-code.js";
|
|
6
5
|
import { parseWorkerResult } from "./prompt.js";
|
|
6
|
+
import { runInteractiveSeed } from "../run/interactive-seed.js";
|
|
7
|
+
import { sh } from "../run/git.js";
|
|
7
8
|
import { channelsFromConfig, MODEL_ID_RE, shq, TokenUsageSchema } from "./types.js";
|
|
8
9
|
// KIMI-03 → v1.58 T5: the "no harness-readable counter" block (research F-6, 2026-07-17) is
|
|
9
10
|
// LIFTED for collectUsage — kimi 0.27.0 writes a wire journal per agent at
|
|
@@ -117,40 +118,18 @@ export function confirmKimiSeedBanner(banner, assignedModel) {
|
|
|
117
118
|
}
|
|
118
119
|
return { ok: true, sessionId: kimiBannerSessionId(banner) };
|
|
119
120
|
}
|
|
120
|
-
// Shared launch-then-seed surface (T6) + banner confirm
|
|
121
|
-
//
|
|
121
|
+
// Shared launch-then-seed surface (T6) + banner confirm (T7/T2). One definition so the adapter
|
|
122
|
+
// property and the daemon's generic runInteractiveSeed path cannot drift.
|
|
122
123
|
const KIMI_SEED = {
|
|
123
124
|
launch: (model) => `kimi -y -m ${shq(kimiAlias(model))}`,
|
|
124
125
|
readinessMatch: "Send /help for help information.",
|
|
125
126
|
seedLine: (promptFile) => `Read ${promptFile} and do exactly what it says.`,
|
|
127
|
+
confirmBanner: confirmKimiSeedBanner,
|
|
126
128
|
};
|
|
127
|
-
// v1.69 T7:
|
|
128
|
-
// from the SAME pane text already present for the readiness match. No extra probe, no extra
|
|
129
|
-
// dispatch — one read of the launch banner, then seed or fail closed.
|
|
129
|
+
// v1.69 T7 / v1.71 T2: thin wrapper over the daemon's generic dispatch path for kimi-only tests.
|
|
130
130
|
export async function runKimiInteractiveSeed(opts) {
|
|
131
|
-
await
|
|
132
|
-
|
|
133
|
-
// Banner text already on the pane for the readiness match — the only text model/session use.
|
|
134
|
-
const banner = await opts.driver.read(opts.slot, 1000);
|
|
135
|
-
if (!ready) {
|
|
136
|
-
return { output: banner, seedFailed: true, seedError: `readiness pattern not seen: ${KIMI_SEED.readinessMatch}` };
|
|
137
|
-
}
|
|
138
|
-
const confirm = confirmKimiSeedBanner(banner, opts.assignment.model);
|
|
139
|
-
if (!confirm.ok) {
|
|
140
|
-
return { output: banner, seedFailed: true, seedError: confirm.error };
|
|
141
|
-
}
|
|
142
|
-
const seedText = KIMI_SEED.seedLine(opts.promptFile);
|
|
143
|
-
await opts.driver.run(opts.slot, seedText);
|
|
144
|
-
let output = "";
|
|
145
|
-
for (let attempt = 0; attempt < 5; attempt++) {
|
|
146
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
147
|
-
output = await opts.driver.read(opts.slot, 1000);
|
|
148
|
-
const bottom = output.trimEnd().split("\n").pop() ?? "";
|
|
149
|
-
if (!bottom.includes(seedText)) {
|
|
150
|
-
return { output, seedFailed: false, sessionId: confirm.sessionId };
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return { output, seedFailed: true, seedError: "seed line never left the input box", sessionId: confirm.sessionId };
|
|
131
|
+
const r = await runInteractiveSeed({ ...opts, adapter: kimi });
|
|
132
|
+
return { output: r.output, seedFailed: r.seedFailed, seedError: r.seedError, sessionId: r.sessionId };
|
|
154
133
|
}
|
|
155
134
|
export const kimi = {
|
|
156
135
|
id: "kimi",
|
|
@@ -183,10 +162,8 @@ export const kimi = {
|
|
|
183
162
|
// null → the daemon's print fallback (types.ts:101) keeps kimi workers visible without a TUI.
|
|
184
163
|
interactiveCommand: () => null,
|
|
185
164
|
// v1.69 T6/T7: launch the real TUI, wait for the cold-start readiness marker, then submit the
|
|
186
|
-
// prompt as one user turn. Banner model/session confirmation
|
|
187
|
-
//
|
|
188
|
-
// extra probe). Live-probed 2026-07-22 (kimi 0.28.1): `kimi -y` opens the TUI with yolo active
|
|
189
|
-
// and prints the model/session lines plus "Send /help for help information." before the input box.
|
|
165
|
+
// prompt as one user turn. Banner model/session confirmation runs on the daemon's generic
|
|
166
|
+
// runInteractiveSeed path via confirmBanner on KIMI_SEED (not a separate dispatch helper).
|
|
190
167
|
interactiveSeed: KIMI_SEED,
|
|
191
168
|
// v1.53 T3 resume — live-probed 2026-07-18: `-p` + `-S <id>` compose cleanly (no OBS-67-class
|
|
192
169
|
// flag rejection) and the resumed session carries prior conversation state. `-S <id>` is the
|
|
@@ -298,7 +275,7 @@ export const kimi = {
|
|
|
298
275
|
}
|
|
299
276
|
},
|
|
300
277
|
listModels: async () => {
|
|
301
|
-
const r =
|
|
302
|
-
return r.
|
|
278
|
+
const r = await sh("kimi provider list --json", process.cwd(), 15000);
|
|
279
|
+
return r.code !== 0 ? [] : parseKimiModels(r.stdout || "");
|
|
303
280
|
},
|
|
304
281
|
};
|
|
@@ -3,6 +3,28 @@ import { type RoutingProfile } from "../route/profile.js";
|
|
|
3
3
|
import { type AuthHealth, type BillingChannel, type WorkerAdapter } from "./types.js";
|
|
4
4
|
export declare const CANDIDATE_CLI_CATALOG: readonly ["kimi", "gemini", "qwen", "aider", "goose", "amp", "droid", "auggie", "crush"];
|
|
5
5
|
export declare const REGISTERED_ADAPTER_BINARIES: readonly ["claude", "codex", "cursor-agent", "opencode", "pi", "grok"];
|
|
6
|
+
export declare function resolveShellBinary(bin: string, cwd?: string): {
|
|
7
|
+
resolved?: string;
|
|
8
|
+
all: string[];
|
|
9
|
+
};
|
|
10
|
+
export declare function probeVersionShell(bin: string, cwd?: string): AuthHealth & {
|
|
11
|
+
binaryPaths?: string[];
|
|
12
|
+
};
|
|
13
|
+
export declare function binaryShadowWarnings(adapters: WorkerAdapter[], health: Record<string, AuthHealth>, cwd?: string, resolve?: (bin: string, cwd: string) => {
|
|
14
|
+
resolved?: string;
|
|
15
|
+
all: string[];
|
|
16
|
+
}): string[];
|
|
17
|
+
export declare function invalidConfiguredModels(cfg: TickmarkrConfig, adapterId: string, h: AuthHealth | undefined): string[];
|
|
18
|
+
export declare function modelAliasExclusions(cfg: TickmarkrConfig, adapters: WorkerAdapter[], health: Record<string, AuthHealth>): {
|
|
19
|
+
key: string;
|
|
20
|
+
adapter: string;
|
|
21
|
+
model: string;
|
|
22
|
+
}[];
|
|
23
|
+
export declare function modelAliasLine(excluded: {
|
|
24
|
+
key: string;
|
|
25
|
+
adapter: string;
|
|
26
|
+
model: string;
|
|
27
|
+
}[]): string;
|
|
6
28
|
export type CandidateCliDetection = {
|
|
7
29
|
binary: string;
|
|
8
30
|
version: string | undefined;
|
|
@@ -19,7 +41,9 @@ export declare function allAdapters(opts?: {
|
|
|
19
41
|
fakeScriptPath?: string;
|
|
20
42
|
}): WorkerAdapter[];
|
|
21
43
|
export declare function getAdapter(id: string, adapters: WorkerAdapter[]): WorkerAdapter;
|
|
22
|
-
export declare function probeAll(adapters: WorkerAdapter[]
|
|
44
|
+
export declare function probeAll(adapters: WorkerAdapter[], opts?: {
|
|
45
|
+
cwd?: string;
|
|
46
|
+
}): Promise<Record<string, AuthHealth>>;
|
|
23
47
|
export type ProbeModelStatus = "ok" | "timeout" | "failed";
|
|
24
48
|
export type ProbeModelProgress = (adapter: string, model: string, status: ProbeModelStatus, durationMs: number) => void;
|
|
25
49
|
export interface AutoPreferDoc {
|
|
@@ -18,11 +18,89 @@ import { grok } from "./grok.js";
|
|
|
18
18
|
import { kimi } from "./kimi.js";
|
|
19
19
|
import { opencode } from "./opencode.js";
|
|
20
20
|
import { pi } from "./pi.js";
|
|
21
|
-
import {
|
|
21
|
+
import { sealHerdrEnv } from "../drivers/subprocess.js";
|
|
22
|
+
import { channelKey, modelAuthed, QUOTA_RE, shq } from "./types.js";
|
|
22
23
|
// Agent-CLI binary names with no tickmarkr adapter — doctor sweeps these advisory-only (v1.48 T1).
|
|
23
24
|
export const CANDIDATE_CLI_CATALOG = ["kimi", "gemini", "qwen", "aider", "goose", "amp", "droid", "auggie", "crush"];
|
|
24
25
|
// Binaries probed by registered adapters — kept beside the catalog so a test can forbid overlap.
|
|
25
26
|
export const REGISTERED_ADAPTER_BINARIES = ["claude", "codex", "cursor-agent", "opencode", "pi", "grok"];
|
|
27
|
+
// OBS-117: worker panes run commands through `bash -lc` (SubprocessDriver + sh()), not bare spawnSync(bin).
|
|
28
|
+
// Doctor must resolve adapter binaries the same way so a shadow install on PATH cannot disagree with dispatch.
|
|
29
|
+
function shellSpawnSync(cmd, cwd, timeoutMs = 10000) {
|
|
30
|
+
const r = spawnSync("bash", ["-lc", cmd], {
|
|
31
|
+
encoding: "utf8",
|
|
32
|
+
cwd,
|
|
33
|
+
timeout: timeoutMs,
|
|
34
|
+
env: sealHerdrEnv(process.env),
|
|
35
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
36
|
+
});
|
|
37
|
+
return { code: r.status ?? 1, stdout: (r.stdout || "").trim(), stderr: (r.stderr || "").trim() };
|
|
38
|
+
}
|
|
39
|
+
export function resolveShellBinary(bin, cwd = process.cwd()) {
|
|
40
|
+
const r = shellSpawnSync(`type -a ${shq(bin)} 2>/dev/null | sed -n 's/^.* is \\(.*\\)$/\\1/p'`, cwd);
|
|
41
|
+
const all = r.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
42
|
+
return { resolved: all[0], all };
|
|
43
|
+
}
|
|
44
|
+
export function probeVersionShell(bin, cwd = process.cwd()) {
|
|
45
|
+
const { resolved, all } = resolveShellBinary(bin, cwd);
|
|
46
|
+
if (!resolved)
|
|
47
|
+
return { installed: false, authed: false, models: [] };
|
|
48
|
+
const r = shellSpawnSync(`${shq(resolved)} --version`, cwd);
|
|
49
|
+
if (r.code !== 0)
|
|
50
|
+
return { installed: false, authed: false, models: [], binaryPaths: all };
|
|
51
|
+
return {
|
|
52
|
+
installed: true,
|
|
53
|
+
authed: true,
|
|
54
|
+
version: (r.stdout || r.stderr).trim().split("\n")[0],
|
|
55
|
+
models: [],
|
|
56
|
+
note: "auth assumed; verified at dispatch (failover on auth/quota errors)",
|
|
57
|
+
binaryPaths: all,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function binaryShadowWarnings(adapters, health, cwd = process.cwd(), resolve) {
|
|
61
|
+
const resolveFn = resolve ?? resolveShellBinary;
|
|
62
|
+
const out = [];
|
|
63
|
+
for (const a of adapters) {
|
|
64
|
+
const bin = a.hardcodedFlags?.binary;
|
|
65
|
+
if (!bin || !health[a.id]?.installed)
|
|
66
|
+
continue;
|
|
67
|
+
const { resolved, all } = resolveFn(bin, cwd);
|
|
68
|
+
if (all.length <= 1)
|
|
69
|
+
continue;
|
|
70
|
+
out.push(`binary shadow: ${a.id} resolves to ${resolved ?? bin} but ${all.length} installs on PATH — ${all.join(", ")}`);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
function modelListValidated(h) {
|
|
75
|
+
return !!h?.modelsDetectedAt && Array.isArray(h.models);
|
|
76
|
+
}
|
|
77
|
+
export function invalidConfiguredModels(cfg, adapterId, h) {
|
|
78
|
+
if (!modelListValidated(h))
|
|
79
|
+
return [];
|
|
80
|
+
const listed = new Set(h.models);
|
|
81
|
+
const configured = Object.keys(cfg.tiers[adapterId]?.models ?? {});
|
|
82
|
+
// Fail open until the CLI list intersects at least one configured model — proves the list is authoritative
|
|
83
|
+
// for this adapter's tier namespace (MODEL-04: unrelated detected ids must not invalidate routing).
|
|
84
|
+
if (!configured.some((m) => listed.has(m)))
|
|
85
|
+
return [];
|
|
86
|
+
return configured.filter((m) => !listed.has(m));
|
|
87
|
+
}
|
|
88
|
+
export function modelAliasExclusions(cfg, adapters, health) {
|
|
89
|
+
const out = [];
|
|
90
|
+
for (const a of adapters) {
|
|
91
|
+
const h = health[a.id];
|
|
92
|
+
if (!h?.installed || !h?.authed)
|
|
93
|
+
continue;
|
|
94
|
+
for (const model of invalidConfiguredModels(cfg, a.id, h)) {
|
|
95
|
+
out.push({ key: channelKey({ adapter: a.id, model }), adapter: a.id, model });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
export function modelAliasLine(excluded) {
|
|
101
|
+
const parts = excluded.map(({ key, adapter }) => `${key} (not in ${adapter}'s reported model list)`);
|
|
102
|
+
return `model alias: ${excluded.length} channel(s) invalid — ${parts.join(", ")}`;
|
|
103
|
+
}
|
|
26
104
|
function candidateOnPath(bin, pathEnv) {
|
|
27
105
|
const r = spawnSync("which", [bin], { encoding: "utf8", env: { ...process.env, PATH: pathEnv } });
|
|
28
106
|
return r.status === 0 && r.stdout.trim().length > 0;
|
|
@@ -105,9 +183,20 @@ export function getAdapter(id, adapters) {
|
|
|
105
183
|
throw new Error(`unknown adapter ${id} (have: ${adapters.map((a) => a.id).join(", ")})`);
|
|
106
184
|
return a;
|
|
107
185
|
}
|
|
108
|
-
export async function probeAll(adapters) {
|
|
186
|
+
export async function probeAll(adapters, opts = {}) {
|
|
187
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
109
188
|
const out = {};
|
|
110
|
-
await Promise.all(adapters.map(async (a) => {
|
|
189
|
+
await Promise.all(adapters.map(async (a) => {
|
|
190
|
+
let h = await a.probe();
|
|
191
|
+
const bin = a.hardcodedFlags?.binary;
|
|
192
|
+
if (bin) {
|
|
193
|
+
const shell = probeVersionShell(bin, cwd);
|
|
194
|
+
h = { ...h, installed: shell.installed, version: shell.version ?? h.version };
|
|
195
|
+
if (!shell.installed)
|
|
196
|
+
h = { ...h, installed: false, authed: false };
|
|
197
|
+
}
|
|
198
|
+
out[a.id] = h;
|
|
199
|
+
}));
|
|
111
200
|
return out;
|
|
112
201
|
}
|
|
113
202
|
const MODEL_PROBE_PROMPT = "Reply with exactly OK and nothing else.";
|
|
@@ -317,10 +406,12 @@ export function discoverChannels(cfg, adapters, health) {
|
|
|
317
406
|
.flatMap((a) => {
|
|
318
407
|
const h = health[a.id];
|
|
319
408
|
const s = h?.servable;
|
|
409
|
+
const invalid = new Set(invalidConfiguredModels(cfg, a.id, h));
|
|
320
410
|
// T2 (2026-07-13): only a model doctor marked authed (modelAuth[model].authed===true) advertises a
|
|
321
411
|
// channel — routing an unknown or 403 into dispatch is fail-closed. Operators with pre-v1.21 doctor.json
|
|
322
412
|
// can opt into the prior unknown-is-routable behavior with routing.allowUnverifiedModels.
|
|
323
|
-
|
|
413
|
+
// OBS-117: configured aliases absent from the adapter CLI's own model list are not dispatchable.
|
|
414
|
+
return a.channels(cfg).filter((c) => !invalid.has(c.model) && modelAuthed(h, c.model, cfg.routing.allowUnverifiedModels) && (!s || s.includes(c.model)));
|
|
324
415
|
});
|
|
325
416
|
if (!cfg.routing.allow && !cfg.routing.deny)
|
|
326
417
|
return base;
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -48,10 +48,18 @@ export interface WorkerResult {
|
|
|
48
48
|
deviations: string[];
|
|
49
49
|
raw: string;
|
|
50
50
|
}
|
|
51
|
+
export type SeedBannerConfirmResult = {
|
|
52
|
+
ok: true;
|
|
53
|
+
sessionId?: string;
|
|
54
|
+
} | {
|
|
55
|
+
ok: false;
|
|
56
|
+
error: string;
|
|
57
|
+
};
|
|
51
58
|
export interface InteractiveSeed {
|
|
52
59
|
launch(model: string): string;
|
|
53
60
|
readinessMatch: string;
|
|
54
61
|
seedLine(promptFile: string): string;
|
|
62
|
+
confirmBanner?(banner: string, assignedModel: string): SeedBannerConfirmResult;
|
|
55
63
|
}
|
|
56
64
|
export interface ContextUsage {
|
|
57
65
|
tokens: number;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { allAdapters, detectCandidateClis, flagDriftWarnings, probeAll, probeModels, readAutoPrefer, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
|
|
3
|
+
import { allAdapters, binaryShadowWarnings, detectCandidateClis, flagDriftWarnings, modelAliasExclusions, modelAliasLine, probeAll, probeModels, readAutoPrefer, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
|
|
4
4
|
import { BANNER, dim, fail, kvRow, legend, ok, rule, statusRow, title } from "../../brand.js";
|
|
5
5
|
import { tickmarkrDir, stateDirName } from "../../graph/graph.js";
|
|
6
6
|
import { declaredModelWindow, hasWindowsConfig, modelLints, suggestOverlay, ttyVisual } from "../../adapters/model-lints.js";
|
|
7
7
|
import { DEFAULT_CONFIG, loadConfig, overlayPreferShapes } from "../../config/config.js";
|
|
8
8
|
import { HerdrDriver } from "../../drivers/herdr.js";
|
|
9
|
-
import { disallowedBy, excludedChannels, exclusionLine, preferRanks } from "../../route/preference.js";
|
|
9
|
+
import { denyPreferCollisionLine, denyPreferCollisions, disallowedBy, excludedChannels, exclusionLine, preferRanks } from "../../route/preference.js";
|
|
10
10
|
const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
11
11
|
const alignedStatusRow = (verdict, key, value) => ` ${statusRow(verdict, kvRow(key, value).slice(2))}`;
|
|
12
12
|
const attentionRow = (text) => ` ${statusRow("warn", text)}`;
|
|
@@ -19,7 +19,7 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
19
19
|
// otherwise prints nothing until the end — silence here reads as a hang (v1.33.1)
|
|
20
20
|
console.error("probing installed agent CLIs — one short LLM call per configured model, may take a minute...");
|
|
21
21
|
const probeProgressTTY = process.stderr.isTTY === true;
|
|
22
|
-
const health = await probeAll(adapters);
|
|
22
|
+
const health = await probeAll(adapters, { cwd });
|
|
23
23
|
// MODEL-02: detect models where the adapter exposes a list surface, BEFORE writing doctor.json (write once, below).
|
|
24
24
|
// Fail OPEN — the inverse of gates' fail-closed: detection is advisory, so a broken list surface NEVER fails doctor.
|
|
25
25
|
for (const a of adapters) {
|
|
@@ -81,6 +81,11 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
81
81
|
const excluded = excludedChannels(cfg, adapters, health);
|
|
82
82
|
if (excluded.length)
|
|
83
83
|
rows.push(attentionRow(exclusionLine(excluded)));
|
|
84
|
+
rows.push(...denyPreferCollisions(cfg).map((c) => attentionRow(denyPreferCollisionLine(c))));
|
|
85
|
+
const aliasExcluded = modelAliasExclusions(cfg, adapters, health);
|
|
86
|
+
if (aliasExcluded.length)
|
|
87
|
+
rows.push(attentionRow(modelAliasLine(aliasExcluded)));
|
|
88
|
+
rows.push(...binaryShadowWarnings(adapters, health, cwd).map(attentionRow));
|
|
84
89
|
// HYG-07(a): doctor just probed fresh (probeAll above), so servability attribution is current by construction.
|
|
85
90
|
const servable = servableExclusions(cfg, adapters, health);
|
|
86
91
|
if (servable.length)
|
|
@@ -12,6 +12,8 @@ export type FleetOutput = {
|
|
|
12
12
|
export type FleetIO = {
|
|
13
13
|
input?: FleetInput;
|
|
14
14
|
output?: FleetOutput;
|
|
15
|
+
debug?: boolean;
|
|
16
|
+
reloadGuard?: (bytes: string) => string | null;
|
|
15
17
|
};
|
|
16
18
|
export declare function fleet(argv: string[], cwd?: string, adapters?: WorkerAdapter[], io?: FleetIO): Promise<string | {
|
|
17
19
|
out: string;
|