tickmarkr 2.5.3 → 2.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/registry.js +6 -1
- package/dist/adapters/types.d.ts +3 -0
- package/dist/cli/commands/approve.d.ts +1 -0
- package/dist/cli/commands/approve.js +54 -6
- package/dist/cli/commands/doctor.d.ts +4 -0
- package/dist/cli/commands/doctor.js +44 -29
- package/dist/cli/commands/fleet.js +195 -33
- package/dist/cli/commands/init.js +196 -6
- package/dist/cli/commands/plan.js +144 -3
- package/dist/cli/commands/resume.js +6 -2
- package/dist/cli/commands/run.js +21 -2
- package/dist/cli/commands/verify.js +1 -1
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +3 -1
- package/dist/compile/collateral.d.ts +2 -0
- package/dist/compile/collateral.js +50 -9
- package/dist/compile/ownership.d.ts +7 -0
- package/dist/compile/ownership.js +59 -22
- package/dist/config/config.d.ts +23 -8
- package/dist/config/config.js +42 -28
- package/dist/config/fleet-overlay.d.ts +3 -9
- package/dist/config/fleet-overlay.js +68 -11
- package/dist/config/fleet-why.d.ts +7 -0
- package/dist/config/fleet-why.js +5 -0
- package/dist/drivers/herdr.d.ts +5 -0
- package/dist/drivers/herdr.js +14 -3
- package/dist/drivers/index.d.ts +15 -1
- package/dist/drivers/index.js +38 -10
- package/dist/drivers/orca.d.ts +99 -10
- package/dist/drivers/orca.js +586 -97
- package/dist/drivers/types.d.ts +1 -0
- package/dist/gates/baseline.d.ts +3 -0
- package/dist/gates/baseline.js +2 -1
- package/dist/gates/cache.d.ts +100 -0
- package/dist/gates/cache.js +389 -0
- package/dist/gates/review.d.ts +35 -2
- package/dist/gates/review.js +64 -16
- package/dist/gates/run-gates.d.ts +5 -0
- package/dist/gates/run-gates.js +134 -18
- package/dist/gates/test-manifest.d.ts +99 -0
- package/dist/gates/test-manifest.js +389 -0
- package/dist/gates/test-reporter.d.ts +4 -0
- package/dist/gates/test-reporter.js +49 -0
- package/dist/route/preference.d.ts +22 -1
- package/dist/route/preference.js +123 -25
- package/dist/route/router.d.ts +13 -0
- package/dist/route/router.js +95 -17
- package/dist/run/daemon.d.ts +13 -0
- package/dist/run/daemon.js +398 -96
- package/dist/run/git.d.ts +35 -1
- package/dist/run/git.js +111 -10
- package/dist/run/journal.d.ts +14 -2
- package/dist/run/journal.js +78 -10
- package/dist/run/lease.d.ts +14 -0
- package/dist/run/lease.js +87 -0
- package/dist/run/merge.d.ts +2 -0
- package/dist/run/merge.js +91 -3
- package/dist/run/operator-state.d.ts +11 -0
- package/dist/run/operator-state.js +17 -3
- package/dist/tui/cockpit/board.d.ts +96 -0
- package/dist/tui/cockpit/board.js +346 -0
- package/dist/tui/cockpit/decision-actions.js +2 -0
- package/dist/tui/cockpit/layout.d.ts +5 -1
- package/dist/tui/cockpit/layout.js +8 -3
- package/dist/tui/cockpit/live-runtime.js +83 -31
- package/dist/tui/cockpit/run-view.d.ts +7 -5
- package/dist/tui/cockpit/run-view.js +12 -11
- package/dist/tui/ink/fleet-app.d.ts +41 -27
- package/dist/tui/ink/fleet-app.js +204 -31
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +180 -99
|
@@ -578,7 +578,12 @@ export function discoverChannels(cfg, adapters, health, role = "worker") {
|
|
|
578
578
|
// channel — routing an unknown or 403 into dispatch is fail-closed. Operators with pre-v1.21 doctor.json
|
|
579
579
|
// can opt into the prior unknown-is-routable behavior with routing.allowUnverifiedModels.
|
|
580
580
|
// OBS-117: configured aliases absent from the adapter CLI's own model list are not dispatchable.
|
|
581
|
-
return a.channels(cfg)
|
|
581
|
+
return a.channels(cfg)
|
|
582
|
+
.filter((c) => !invalid.has(c.model) && modelAuthed(h, c.model, cfg.routing.allowUnverifiedModels) && (!s || s.includes(c.model)))
|
|
583
|
+
.map((c) => {
|
|
584
|
+
const identity = h?.modelAuth?.[c.model]?.identity ?? h?.modelIdentities?.[c.model];
|
|
585
|
+
return identity ? { ...c, identity } : c;
|
|
586
|
+
});
|
|
582
587
|
});
|
|
583
588
|
if (!cfg.routing.allow && !cfg.routing.deny)
|
|
584
589
|
return base;
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface BillingChannel {
|
|
|
22
22
|
model: string;
|
|
23
23
|
channel: "sub" | "api";
|
|
24
24
|
tier: Tier;
|
|
25
|
+
identity?: string;
|
|
25
26
|
}
|
|
26
27
|
export declare const MODEL_PROBE_ERRORS: readonly ["EMFILE", "EAGAIN", "ENFILE", "ENOMEM", "ENOSPC"];
|
|
27
28
|
export type ModelProbeError = typeof MODEL_PROBE_ERRORS[number];
|
|
@@ -30,6 +31,7 @@ export interface ModelAuth {
|
|
|
30
31
|
reason?: string;
|
|
31
32
|
probeError?: ModelProbeError;
|
|
32
33
|
probedAt: string;
|
|
34
|
+
identity?: string;
|
|
33
35
|
}
|
|
34
36
|
export interface AuthHealth {
|
|
35
37
|
installed: boolean;
|
|
@@ -40,6 +42,7 @@ export interface AuthHealth {
|
|
|
40
42
|
modelsDetectedAt?: string;
|
|
41
43
|
servable?: string[];
|
|
42
44
|
modelAuth?: Record<string, ModelAuth>;
|
|
45
|
+
modelIdentities?: Record<string, string>;
|
|
43
46
|
}
|
|
44
47
|
export declare function modelAuthed(health: AuthHealth | undefined, model: string, allowUnverifiedModels?: boolean): boolean;
|
|
45
48
|
export interface Invocation {
|
|
@@ -21,6 +21,7 @@ export interface NewestPark {
|
|
|
21
21
|
/** The daemon-recorded kind (task-human data.kind), never inferred from prose. */
|
|
22
22
|
kind: string | undefined;
|
|
23
23
|
reason: string | undefined;
|
|
24
|
+
approveCommand?: string;
|
|
24
25
|
/** The newest failed gate before the park — the gate a waive would satisfy. */
|
|
25
26
|
failedGate: string | undefined;
|
|
26
27
|
/** A pre-dispatch human gate whose reason marks it permanent by design (see isTombstonePark). */
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { graphDefinitionHash, loadGraph, saveGraph } from "../../graph/graph.js";
|
|
1
2
|
import { userInfo } from "node:os";
|
|
3
|
+
import { separabilityErrors } from "../../compile/collateral.js";
|
|
2
4
|
import { GATE_NAMES } from "../../graph/schema.js";
|
|
3
|
-
import { ATTEMPT_CAP_RELEASE, GATE_SATISFIED_RELEASE, Journal, RECHECK_RELEASE, REVIEW_UPHELD_RELEASE } from "../../run/journal.js";
|
|
5
|
+
import { applyScopeAmendments, engagementComparable, ATTEMPT_CAP_RELEASE, GATE_SATISFIED_RELEASE, Journal, RECHECK_RELEASE, REVIEW_UPHELD_RELEASE } from "../../run/journal.js";
|
|
4
6
|
export const APPROVAL_DISPOSITIONS = ["dispatch", "waive-gate", "re-dispatch", "fund-fixed-attempt", "fresh-budget"];
|
|
5
7
|
/**
|
|
6
8
|
* What each disposition's release actually buys — the clause every operator-facing sentence about
|
|
@@ -52,6 +54,7 @@ sourceIndexes) {
|
|
|
52
54
|
const reason = typeof e.data.reason === "string" ? e.data.reason : undefined;
|
|
53
55
|
return {
|
|
54
56
|
index: i, line: (sourceIndexes?.[i] ?? i) + 1, ts: typeof e.ts === "string" ? e.ts : undefined, kind, reason,
|
|
57
|
+
approveCommand: typeof e.data.approveCommand === "string" ? e.data.approveCommand : undefined,
|
|
55
58
|
failedGate: failedGateForNewestPark(events, taskId, i), tombstone: isTombstonePark(kind, reason),
|
|
56
59
|
};
|
|
57
60
|
}
|
|
@@ -73,7 +76,7 @@ export function readJournalEvents(journal) {
|
|
|
73
76
|
* surface may read it from, so what a menu offers and what the command accepts cannot drift.
|
|
74
77
|
*/
|
|
75
78
|
export function permittedDecisionVerbs(park) {
|
|
76
|
-
if (!park || park.tombstone)
|
|
79
|
+
if (!park || park.tombstone || park.kind === "scope-request")
|
|
77
80
|
return [];
|
|
78
81
|
if (park.kind === "gate-fail") {
|
|
79
82
|
if (park.failedGate === undefined)
|
|
@@ -123,7 +126,7 @@ export function approvalEnactment(token, run) {
|
|
|
123
126
|
}
|
|
124
127
|
/** The production command registered in COMMANDS; its returned bytes are what the CLI prints. */
|
|
125
128
|
export async function approve(argv, cwd = process.cwd()) {
|
|
126
|
-
const { runId, taskId, by, reason, waive, uphold, recheck, reviewRoundCeiling } = parseArgs(argv);
|
|
129
|
+
const { runId, taskId, by, reason, waive, uphold, recheck, reviewRoundCeiling, files } = parseArgs(argv);
|
|
127
130
|
const decisions = [waive, uphold, recheck].filter(Boolean).length;
|
|
128
131
|
if (decisions > 1)
|
|
129
132
|
throw new Error("--waive, --uphold and --recheck are different decisions — pass one");
|
|
@@ -148,6 +151,41 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
148
151
|
const gateFailPark = park?.kind === "gate-fail";
|
|
149
152
|
const infraPark = park?.kind === "infra";
|
|
150
153
|
const failedGate = gateFailPark ? park?.failedGate : undefined;
|
|
154
|
+
if ((park?.kind === "scope-request" && !decisions) || files !== undefined) {
|
|
155
|
+
if (park?.kind !== "scope-request")
|
|
156
|
+
throw new Error("--files requires a scope-request park");
|
|
157
|
+
if (!files?.length)
|
|
158
|
+
throw new Error(`scope-request for ${taskId} requires --files <glob,…>`);
|
|
159
|
+
if (decisions)
|
|
160
|
+
throw new Error("--files cannot be combined with --waive, --uphold or --recheck");
|
|
161
|
+
const graph = applyScopeAmendments(loadGraph(cwd), journal);
|
|
162
|
+
const from = graphDefinitionHash(graph);
|
|
163
|
+
if (!engagementComparable(journal.read(), from).comparable
|
|
164
|
+
|| (lastHuman?.data.graphDefinitionHash !== undefined && lastHuman.data.graphDefinitionHash !== from)) {
|
|
165
|
+
throw new Error(`refusing stale scope-request approval for ${taskId}: graph revision changed`);
|
|
166
|
+
}
|
|
167
|
+
const task = graph.tasks.find((t) => t.id === taskId);
|
|
168
|
+
if (!task)
|
|
169
|
+
throw new Error(`unknown task ${taskId}`);
|
|
170
|
+
const amendedFiles = [...new Set([...task.files, ...files])];
|
|
171
|
+
if (amendedFiles.length === task.files.length)
|
|
172
|
+
throw new Error("--files must extend the parked files[] revision");
|
|
173
|
+
const amended = { ...graph, tasks: graph.tasks.map((t) => t.id === taskId ? { ...t, files: amendedFiles } : t) };
|
|
174
|
+
const conflicts = separabilityErrors(amended.tasks);
|
|
175
|
+
if (conflicts.length)
|
|
176
|
+
throw new Error(`refusing scope-request approval for ${taskId}: ${conflicts.join("\n")}`);
|
|
177
|
+
journal.append("task-approved", taskId, {
|
|
178
|
+
by, ...(reason ? { reason } : {}), via: "cli", release: "scope-request",
|
|
179
|
+
amendment: { from, to: graphDefinitionHash(amended), beforeFiles: task.files, files: amendedFiles, parkLine: park.line },
|
|
180
|
+
});
|
|
181
|
+
// Do not write graph.json from this process while the daemon owns it: its sweep materializes
|
|
182
|
+
// the amendment without replacing a sibling's running state with this command's snapshot.
|
|
183
|
+
const projected = applyScopeAmendments(graph, journal);
|
|
184
|
+
const owner = approvalRunOwner(cwd, runId);
|
|
185
|
+
if (!owner.live && !owner.blockingRunId)
|
|
186
|
+
saveGraph(cwd, projected);
|
|
187
|
+
return disposition(cwd, runId, "dispatch", `approved files[] for ${taskId} in ${runId} — by ${by}`, serialization.contended);
|
|
188
|
+
}
|
|
151
189
|
if (gateFailPark && !failedGate) {
|
|
152
190
|
throw new Error(`task ${taskId} is parked on gate-fail but has no failed gate result on the newest park — refusing to infer one`);
|
|
153
191
|
}
|
|
@@ -247,11 +285,12 @@ function disposition(cwd, runId, token, message, contended) {
|
|
|
247
285
|
};
|
|
248
286
|
return `${out}\nTICKMARKR_APPROVAL ${JSON.stringify(record)}`;
|
|
249
287
|
}
|
|
250
|
-
const USAGE = "usage: tickmarkr approve <run-id> <task-id> [--waive|--uphold|--recheck] [--review-rounds <positive-integer>] [--by <name>] [--reason <text>]";
|
|
288
|
+
const USAGE = "usage: tickmarkr approve <run-id> <task-id> [--files <glob,…>] [--waive|--uphold|--recheck] [--review-rounds <positive-integer>] [--by <name>] [--reason <text>]";
|
|
251
289
|
// hand-parsed argv — no CLI framework (house style). Positionals are runId then taskId; decision,
|
|
252
290
|
// ceiling, actor and reason are flags. Throws usage on missing positionals (mirrors resume.ts/unlock.ts).
|
|
253
291
|
function parseArgs(argv) {
|
|
254
292
|
const positionals = [];
|
|
293
|
+
let files;
|
|
255
294
|
let by;
|
|
256
295
|
let reason;
|
|
257
296
|
let waive = false;
|
|
@@ -260,7 +299,16 @@ function parseArgs(argv) {
|
|
|
260
299
|
let reviewRoundCeiling;
|
|
261
300
|
for (let i = 0; i < argv.length; i++) {
|
|
262
301
|
const a = argv[i];
|
|
263
|
-
if (a === "--
|
|
302
|
+
if (a === "--files") {
|
|
303
|
+
const value = argv[++i];
|
|
304
|
+
if (!value || value.startsWith("--"))
|
|
305
|
+
throw new Error("--files requires <glob,…>");
|
|
306
|
+
files = value.split(",").map((file) => file.trim());
|
|
307
|
+
if (files.some((file) => !file || file.startsWith("/") || file.startsWith("!") || file.split("/").includes(".."))) {
|
|
308
|
+
throw new Error("--files requires non-empty repository-relative globs");
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
else if (a === "--by") {
|
|
264
312
|
by = argv[++i];
|
|
265
313
|
if (!by)
|
|
266
314
|
throw new Error(USAGE);
|
|
@@ -296,7 +344,7 @@ function parseArgs(argv) {
|
|
|
296
344
|
if (!runId || !taskId) {
|
|
297
345
|
throw new Error(USAGE);
|
|
298
346
|
}
|
|
299
|
-
return { runId, taskId, by: by ?? userInfo().username, reason, waive, uphold, recheck, reviewRoundCeiling };
|
|
347
|
+
return { runId, taskId, by: by ?? userInfo().username, reason, waive, uphold, recheck, reviewRoundCeiling, files };
|
|
300
348
|
}
|
|
301
349
|
function failedGateForNewestPark(events, taskId, lastHumanIndex) {
|
|
302
350
|
for (let i = lastHumanIndex - 1; i >= 0; i -= 1) {
|
|
@@ -128,6 +128,7 @@ export declare function doctorProbePreflight(cwd?: string, cfg?: {
|
|
|
128
128
|
}>;
|
|
129
129
|
floors: Record<string, "cheap" | "mid" | "frontier">;
|
|
130
130
|
learned: "on" | "off";
|
|
131
|
+
escalateTier: "on" | "off";
|
|
131
132
|
allowUnverifiedModels: boolean;
|
|
132
133
|
mode?: "partner-led" | "risk-based" | "staff-led" | undefined;
|
|
133
134
|
learnedTuning?: {
|
|
@@ -185,6 +186,7 @@ export declare function doctorProbePreflight(cwd?: string, cfg?: {
|
|
|
185
186
|
complexityThreshold: number;
|
|
186
187
|
timeoutMs: number;
|
|
187
188
|
required: boolean;
|
|
189
|
+
floor: "cheap" | "mid" | "frontier" | "worker";
|
|
188
190
|
prefer?: string[] | undefined;
|
|
189
191
|
policy?: "full" | "judge-only" | undefined;
|
|
190
192
|
criticalPaths?: string[] | undefined;
|
|
@@ -245,6 +247,7 @@ export declare function cachedDoctorDiagnostics(cwd?: string, adapters?: WorkerA
|
|
|
245
247
|
}>;
|
|
246
248
|
floors: Record<string, "cheap" | "mid" | "frontier">;
|
|
247
249
|
learned: "on" | "off";
|
|
250
|
+
escalateTier: "on" | "off";
|
|
248
251
|
allowUnverifiedModels: boolean;
|
|
249
252
|
mode?: "partner-led" | "risk-based" | "staff-led" | undefined;
|
|
250
253
|
learnedTuning?: {
|
|
@@ -302,6 +305,7 @@ export declare function cachedDoctorDiagnostics(cwd?: string, adapters?: WorkerA
|
|
|
302
305
|
complexityThreshold: number;
|
|
303
306
|
timeoutMs: number;
|
|
304
307
|
required: boolean;
|
|
308
|
+
floor: "cheap" | "mid" | "frontier" | "worker";
|
|
305
309
|
prefer?: string[] | undefined;
|
|
306
310
|
policy?: "full" | "judge-only" | undefined;
|
|
307
311
|
criticalPaths?: string[] | undefined;
|
|
@@ -613,6 +613,46 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
613
613
|
if (probeProgressTTY && probeOk + probeBad > 0) {
|
|
614
614
|
process.stderr.write(`\r\x1b[2K probed ${probeOk + probeBad} channels · ${probeOk} ok${probeBad ? ` · ${probeBad} failed` : ""}\n`);
|
|
615
615
|
}
|
|
616
|
+
const identityResolver = opts.resolveClaudeAliasIdentity
|
|
617
|
+
?? (adapters.some((a) => a.id === claudeCode.id) ? resolveClaudeAliasIdentity : undefined);
|
|
618
|
+
const aliasDriftFindings = [];
|
|
619
|
+
// Single pre-write snapshot of the resolver's outcome per alias (including undefined/failure).
|
|
620
|
+
// Catalog rendering below reads only this map — never calls the resolver again — so a transient
|
|
621
|
+
// resolver that succeeds on a later call can never produce a result that stays display-only
|
|
622
|
+
// while doctor.json and discovery keep the earlier undefined/failed outcome.
|
|
623
|
+
const resolvedIdentitySnapshot = {};
|
|
624
|
+
if (identityResolver && health["claude-code"]?.installed) {
|
|
625
|
+
const configured = cfg.tiers["claude-code"]?.models ?? {};
|
|
626
|
+
for (const [alias, stampedIdentity] of Object.entries(CLAUDE_ALIAS_IDENTITY_STAMPS)) {
|
|
627
|
+
if (!(alias in configured))
|
|
628
|
+
continue;
|
|
629
|
+
let resolvedIdentity;
|
|
630
|
+
try {
|
|
631
|
+
resolvedIdentity = identityResolver(cwd, alias);
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
resolvedIdentitySnapshot[alias] = undefined;
|
|
635
|
+
continue; // advisory source failure is unknown, never a doctor failure
|
|
636
|
+
}
|
|
637
|
+
resolvedIdentitySnapshot[alias] = resolvedIdentity;
|
|
638
|
+
if (resolvedIdentity) {
|
|
639
|
+
if (!health["claude-code"].modelAuth) {
|
|
640
|
+
health["claude-code"].modelAuth = {};
|
|
641
|
+
}
|
|
642
|
+
const existing = health["claude-code"].modelAuth[alias] ?? {
|
|
643
|
+
authed: true,
|
|
644
|
+
probedAt: new Date().toISOString(),
|
|
645
|
+
};
|
|
646
|
+
health["claude-code"].modelAuth[alias] = {
|
|
647
|
+
...existing,
|
|
648
|
+
identity: resolvedIdentity,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
if (resolvedIdentity && resolvedIdentity !== stampedIdentity) {
|
|
652
|
+
aliasDriftFindings.push(`resolved-identity drift: claude-code:${alias} resolved to ${resolvedIdentity}, stamped identity ${stampedIdentity} — reclassify per benchmark policy (advisory — routing unchanged)`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
616
656
|
writeDoctor(cwd, health);
|
|
617
657
|
const rows = adapters.map((a) => {
|
|
618
658
|
const h = health[a.id];
|
|
@@ -752,37 +792,12 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
752
792
|
// v1.65 T3: hardcoded-flag drift — advisory warn rows only. Runs AFTER writeDoctor so the verdicts
|
|
753
793
|
// can never leak into doctor.json, and discoverChannels/routing never read them.
|
|
754
794
|
lintRows.push(...flagDriftWarnings(adapters, health).map(attentionRow));
|
|
755
|
-
// OBS-145: resolved-identity drift is
|
|
756
|
-
|
|
757
|
-
// never enters health/doctor.json, config, channel discovery, learned profiles, or route().
|
|
758
|
-
const identityResolver = opts.resolveClaudeAliasIdentity
|
|
759
|
-
?? (adapters.includes(claudeCode) ? resolveClaudeAliasIdentity : undefined);
|
|
760
|
-
if (identityResolver && health["claude-code"]?.installed) {
|
|
761
|
-
const configured = cfg.tiers["claude-code"]?.models ?? {};
|
|
762
|
-
for (const [alias, stampedIdentity] of Object.entries(CLAUDE_ALIAS_IDENTITY_STAMPS)) {
|
|
763
|
-
if (!(alias in configured))
|
|
764
|
-
continue;
|
|
765
|
-
let resolvedIdentity;
|
|
766
|
-
try {
|
|
767
|
-
resolvedIdentity = identityResolver(cwd, alias);
|
|
768
|
-
}
|
|
769
|
-
catch {
|
|
770
|
-
continue; // advisory source failure is unknown, never a doctor failure
|
|
771
|
-
}
|
|
772
|
-
if (resolvedIdentity && resolvedIdentity !== stampedIdentity) {
|
|
773
|
-
lintRows.push(attentionRow(`resolved-identity drift: claude-code:${alias} resolved to ${resolvedIdentity}, stamped identity ${stampedIdentity} — reclassify per benchmark policy (advisory — routing unchanged)`));
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
795
|
+
// OBS-145: resolved-identity drift is advisory display warning.
|
|
796
|
+
lintRows.push(...aliasDriftFindings.map(attentionRow));
|
|
777
797
|
const resolvedCatalogModel = (adapter, model) => {
|
|
778
|
-
if (adapter !== "claude-code" || !
|
|
798
|
+
if (adapter !== "claude-code" || !(model in CLAUDE_ALIAS_IDENTITY_STAMPS))
|
|
779
799
|
return undefined;
|
|
780
|
-
|
|
781
|
-
return identityResolver(cwd, model);
|
|
782
|
-
}
|
|
783
|
-
catch {
|
|
784
|
-
return undefined;
|
|
785
|
-
}
|
|
800
|
+
return resolvedIdentitySnapshot[model];
|
|
786
801
|
};
|
|
787
802
|
// MODEL-05/06: print-only drift fragment; advisory, whole-line-commented additions, tickmarkr NEVER applies it.
|
|
788
803
|
// TTY gets a one-line summary + the fragment as a file (the full dump drowned everything else,
|
|
@@ -5,13 +5,13 @@ import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse, modelAuthE
|
|
|
5
5
|
import { catalogModelAdvisory, catalogTierRanking, declaredModelWindow, fleetUnclassifiedModels } from "../../adapters/model-lints.js";
|
|
6
6
|
import { CATALOG_REFRESH_TIMEOUT_MS, formatCatalogRefreshLegs, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
|
|
7
7
|
import { CLAUDE_ALIAS_IDENTITY_STAMPS, readClaudeAliasIdentity } from "../../adapters/claude-code.js";
|
|
8
|
-
import { fleetEditableFromConfig, fleetEditableEquals, formatFleetPrint, globalConfigDir, overlayBytesLoadError, renderFleetOverlayWrite, repoOverlayPath, ROUTING_MODES, unifiedYamlDiff, } from "../../config/config.js";
|
|
9
|
-
import { projectFleetWhy, renderFleetWhy } from "../../config/fleet-why.js";
|
|
8
|
+
import { fleetEditableFromConfig, fleetEditableEquals, formatFleetPrint, globalConfigDir, loadConfigWithMode, overlayBytesLoadError, readOverlayFile, renderFleetOverlayWrite, repoOverlayPath, ROUTING_MODES, TIER_RANK, unifiedYamlDiff, } from "../../config/config.js";
|
|
9
|
+
import { exclusionReason, projectFleetWhy, renderFleetWhy } from "../../config/fleet-why.js";
|
|
10
10
|
import { SHAPES, TIERS } from "../../graph/schema.js";
|
|
11
11
|
import { doctor } from "./doctor.js";
|
|
12
12
|
import { candidateRow, costSignal, shapeCandidates } from "./fleet-picker.js";
|
|
13
13
|
import { route } from "../../route/router.js";
|
|
14
|
-
import { disallowedBy } from "../../route/preference.js";
|
|
14
|
+
import { denyPreferCollisionLine, denyPreferCollisions, disallowedBy, entryMatchesChannel, exclusionCollector } from "../../route/preference.js";
|
|
15
15
|
import { resolveRunMode } from "../../run/daemon.js";
|
|
16
16
|
import { loadRoutingProfile } from "../../run/journal.js";
|
|
17
17
|
const initialFetch = globalThis.fetch;
|
|
@@ -202,10 +202,17 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
202
202
|
const previewChannels = discoverChannels({ ...cfg, routing: routingScopeBlind }, adapters, health);
|
|
203
203
|
const universe = adapters
|
|
204
204
|
.filter((adapter) => health[adapter.id]?.installed)
|
|
205
|
-
.map((adapter) =>
|
|
206
|
-
adapter
|
|
207
|
-
|
|
208
|
-
|
|
205
|
+
.map((adapter) => {
|
|
206
|
+
const served = previewChannels.filter((c) => c.adapter === adapter.id);
|
|
207
|
+
// LEG2-T3 round 2 finding 3: the recorded identity per channel, so the editable membership
|
|
208
|
+
// matches allow/deny entries with the same authority the collector and the browser use
|
|
209
|
+
const identities = Object.fromEntries(served.flatMap((c) => (c.identity ? [[c.model, c.identity]] : [])));
|
|
210
|
+
return {
|
|
211
|
+
adapter: adapter.id,
|
|
212
|
+
models: [...new Set(served.map((c) => c.model))],
|
|
213
|
+
...(Object.keys(identities).length ? { identities } : {}),
|
|
214
|
+
};
|
|
215
|
+
});
|
|
209
216
|
const initial = fleetEditableFromConfig(cfg, universe);
|
|
210
217
|
const editable = structuredClone(initial);
|
|
211
218
|
// OBS-508: the same catalog evidence doctor's drift overlay prints now rides each unclassified
|
|
@@ -250,9 +257,14 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
250
257
|
|| a.model.localeCompare(b.model));
|
|
251
258
|
// OBS-508 follow-through: the browser renders the metadata the assembler always had — catalog
|
|
252
259
|
// ctx/price evidence and doctor's model-probe wall clock — as columns instead of dropping them.
|
|
260
|
+
// OBS-972/FL-1: the same alias→identity doctor recorded (modelAuth.identity, falling back to
|
|
261
|
+
// the coarser per-adapter modelIdentities map registry.ts/preference.ts already read for
|
|
262
|
+
// routing exclusion) — a floating alias's resolved concrete model id.
|
|
263
|
+
const resolvedModelIdentity = (adapter, model) => health[adapter]?.modelAuth?.[model]?.identity ?? health[adapter]?.modelIdentities?.[model];
|
|
253
264
|
const rowEvidence = (adapter, model, evidence) => {
|
|
254
265
|
const probed = health[adapter]?.modelAuth?.[model];
|
|
255
266
|
const contextWindow = evidence?.contextWindow ?? declaredModelWindow(cfg, adapter, model);
|
|
267
|
+
const identity = resolvedModelIdentity(adapter, model);
|
|
256
268
|
const out = {
|
|
257
269
|
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
258
270
|
...(evidence?.outputWindow !== undefined ? { outputWindow: evidence.outputWindow } : {}),
|
|
@@ -262,6 +274,8 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
262
274
|
// OBS-519: doctor already recorded the failed verdict — a rate-limited/unauthed model must
|
|
263
275
|
// not render identically to a healthy row on the surface that edits its fleet membership.
|
|
264
276
|
...(probed?.authed === false ? { unauthed: probed.reason ?? "probe failed" } : {}),
|
|
277
|
+
// the browser decides deny coverage itself, over the STAGED policy (LEG2-T3 finding 1)
|
|
278
|
+
...(identity !== undefined ? { identity } : {}),
|
|
265
279
|
};
|
|
266
280
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
267
281
|
};
|
|
@@ -306,24 +320,42 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
306
320
|
]));
|
|
307
321
|
const channels = discoverChannels(cfg, adapters, health);
|
|
308
322
|
const profile = loadRoutingProfile(cwd, cfg, { preview: true });
|
|
323
|
+
const stagedEditable = (map, deny) => ({
|
|
324
|
+
...structuredClone(initial),
|
|
325
|
+
denyAdapters: deny.adapters,
|
|
326
|
+
denyModels: deny.models,
|
|
327
|
+
denyWorkersAdapters: deny.workersAdapters,
|
|
328
|
+
denyWorkersModels: deny.workersModels,
|
|
329
|
+
map,
|
|
330
|
+
});
|
|
331
|
+
const candidateMemo = new Map();
|
|
309
332
|
const previewCfg = (mode, map, deny) => {
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
333
|
+
const key = JSON.stringify([mode, map, deny]);
|
|
334
|
+
const hit = candidateMemo.get(key);
|
|
335
|
+
if (hit)
|
|
336
|
+
return hit;
|
|
337
|
+
let preview;
|
|
338
|
+
try {
|
|
339
|
+
const bytes = renderFleetOverlayWrite(currentRepoOverlayText(cwd), {
|
|
340
|
+
initial,
|
|
341
|
+
edited: stagedEditable(map, deny),
|
|
342
|
+
universe,
|
|
343
|
+
...(mode !== rm.mode.mode ? { mode } : {}),
|
|
344
|
+
});
|
|
345
|
+
preview = { ok: true, cfg: loadConfigWithMode(cwd, { globalDir, repoOverlayText: bytes }).cfg };
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
preview = { ok: false, error: error.message.replace(/\s+/g, " ").trim() };
|
|
349
|
+
}
|
|
350
|
+
if (candidateMemo.size > 32)
|
|
351
|
+
candidateMemo.clear();
|
|
352
|
+
candidateMemo.set(key, preview);
|
|
353
|
+
return preview;
|
|
354
|
+
};
|
|
355
|
+
const previewUnavailable = (error) => `preview unavailable (${error}) — the staged overlay does not load, so w would be refused`;
|
|
356
|
+
const stagedRouting = (deny) => {
|
|
357
|
+
const preview = previewCfg(rm.mode.mode, editable.map, deny);
|
|
358
|
+
return preview.ok ? { ok: true, routing: preview.cfg.routing } : preview;
|
|
327
359
|
};
|
|
328
360
|
// route() never deny-filters its pool — that contract lives in discoverChannels — so every
|
|
329
361
|
// preview call rebuilds the pool the staged deny would discover
|
|
@@ -333,7 +365,10 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
333
365
|
let subs = 0;
|
|
334
366
|
let apiN = 0;
|
|
335
367
|
let apiUsd = 0;
|
|
336
|
-
const
|
|
368
|
+
const preview = previewCfg(mode, map, deny);
|
|
369
|
+
if (!preview.ok)
|
|
370
|
+
return ` mix: ${previewUnavailable(preview.error)}`;
|
|
371
|
+
const cfgPreview = preview.cfg;
|
|
337
372
|
const pool = previewPool(cfgPreview);
|
|
338
373
|
for (const shape of SHAPES) {
|
|
339
374
|
try {
|
|
@@ -393,7 +428,15 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
393
428
|
: { declaredAt: "routing.mode" };
|
|
394
429
|
};
|
|
395
430
|
const projectedShapeRows = (mode, map, deny) => {
|
|
396
|
-
const
|
|
431
|
+
const preview = previewCfg(mode, map, deny);
|
|
432
|
+
if (!preview.ok) {
|
|
433
|
+
return projectFleetWhy(SHAPES.map((shape) => ({
|
|
434
|
+
id: shape,
|
|
435
|
+
effective: previewUnavailable(preview.error),
|
|
436
|
+
...whyDeclaration(shape, mode, map, false),
|
|
437
|
+
})), { repoRoot: cwd, globalDir });
|
|
438
|
+
}
|
|
439
|
+
const cfgPreview = preview.cfg;
|
|
397
440
|
const pool = previewPool(cfgPreview);
|
|
398
441
|
const values = SHAPES.map((shape) => {
|
|
399
442
|
try {
|
|
@@ -433,13 +476,13 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
433
476
|
const unauthedClis = adapters
|
|
434
477
|
.filter((adapter) => health[adapter.id]?.installed && health[adapter.id]?.authed === false)
|
|
435
478
|
.map((adapter) => adapter.id);
|
|
436
|
-
const
|
|
437
|
-
const
|
|
479
|
+
const unauthedModels = modelAuthExclusions(cfg, adapters, health);
|
|
480
|
+
const unauthedModelCount = unauthedModels.length;
|
|
481
|
+
const excludedNoteFor = (cfgPreview) => {
|
|
438
482
|
let stagedOut = 0;
|
|
439
483
|
let denied = 0;
|
|
440
|
-
const stagedRouting = previewCfg(rm.mode.mode, {}, deny).routing;
|
|
441
484
|
for (const channel of previewChannels) {
|
|
442
|
-
if (disallowedBy(channel,
|
|
485
|
+
if (disallowedBy(channel, cfgPreview.routing) === null)
|
|
443
486
|
continue;
|
|
444
487
|
if (disallowedBy(channel, cfg.routing) === null)
|
|
445
488
|
stagedOut += 1;
|
|
@@ -459,14 +502,122 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
459
502
|
parts.push(`${unclassifiedRows.length} unclassified (never routed)`);
|
|
460
503
|
return parts.length ? `not offered: ${parts.join(" · ")} — the models view explains each row` : undefined;
|
|
461
504
|
};
|
|
505
|
+
// OBS-994/FL-1: the nine mechanisms outside the deny scopes that can keep a channel off a
|
|
506
|
+
// shape's candidate list each get a reason or an explicit "not manageable here" caption — a
|
|
507
|
+
// silent omission is indistinguishable from a bug (OBS-530's lesson, extended past deny/allow).
|
|
508
|
+
// allow and the unauthed probe already carry a reason through excludedNoteFor above; the rest
|
|
509
|
+
// are shape-scoped (a pin/pool/floor/prefer excludes only for THIS shape) and belong here.
|
|
510
|
+
const shapeExclusionCaptions = (shape, map, cfgPreview) => {
|
|
511
|
+
const pool = previewPool(cfgPreview);
|
|
512
|
+
const entry = map[shape];
|
|
513
|
+
const captions = [];
|
|
514
|
+
// 2. pin or pool — a closed candidate set declared for this shape; a channel outside it is
|
|
515
|
+
// not manageable from the models view, it is a Shapes-view declaration.
|
|
516
|
+
if (entry?.pin) {
|
|
517
|
+
captions.push(`pin: routing.map.${shape}.pin fixes this shape to ${entry.pin.via}:${entry.pin.model} — not manageable here, edit it in Shapes`);
|
|
518
|
+
}
|
|
519
|
+
else if (entry?.pool) {
|
|
520
|
+
const declared = new Set(entry.pool.channels);
|
|
521
|
+
const outside = pool.filter((c) => !declared.has(`${c.adapter}:${c.model}`));
|
|
522
|
+
if (outside.length) {
|
|
523
|
+
captions.push(`pool: ${outside.length} channel(s) outside routing.map.${shape}.pool's declared set — not manageable here, edit it in Shapes`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// 3. floor — channels below this shape's minimum tier.
|
|
527
|
+
const floor = cfgPreview.routing.floors[shape];
|
|
528
|
+
if (floor) {
|
|
529
|
+
const belowFloor = pool.filter((c) => TIER_RANK[c.tier] < TIER_RANK[floor]);
|
|
530
|
+
if (belowFloor.length) {
|
|
531
|
+
captions.push(`floor: ${belowFloor.length} channel(s) below routing.floors.${shape} (${floor}) — not manageable here, edit the floor in Shapes`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
// 5. tombstones — the repo overlay's OWN null over this shape's map entry, masking a lower
|
|
535
|
+
// (global/default) layer's declaration rather than expressing a value of its own.
|
|
536
|
+
const repoRaw = readOverlayFile(repoOverlayPath(cwd));
|
|
537
|
+
const rawEntry = repoRaw.routing?.map?.[shape];
|
|
538
|
+
if (rawEntry === null) {
|
|
539
|
+
captions.push(`tombstone: routing.map.${shape}: null masks a lower layer's declaration — not manageable here, edit the repo overlay directly`);
|
|
540
|
+
}
|
|
541
|
+
else if (rawEntry && "pool" in rawEntry && rawEntry.pool === null) {
|
|
542
|
+
captions.push(`tombstone: routing.map.${shape}.pool: null masks a lower layer's pool — not manageable here, edit the repo overlay directly`);
|
|
543
|
+
}
|
|
544
|
+
else if (rawEntry && "pin" in rawEntry && rawEntry.pin === null) {
|
|
545
|
+
captions.push(`tombstone: routing.map.${shape}.pin: null masks a lower layer's pin — not manageable here, edit the repo overlay directly`);
|
|
546
|
+
}
|
|
547
|
+
// 6. explore/learned knobs — global routing settings, never a per-channel fleet toggle.
|
|
548
|
+
if (cfgPreview.routing.explore?.mode === "off") {
|
|
549
|
+
captions.push("explore: routing.explore.mode is off — a global knob, not manageable here");
|
|
550
|
+
}
|
|
551
|
+
if (cfgPreview.routing.learned === "off") {
|
|
552
|
+
captions.push("learned: routing.learned is off — a global knob, not manageable here");
|
|
553
|
+
}
|
|
554
|
+
// 7. task hint — a shape's prefer bias is authored in Shapes/spec, not per-channel in fleet.
|
|
555
|
+
if (entry?.prefer?.length) {
|
|
556
|
+
captions.push(`task hint: routing.map.${shape}.prefer ranks ${entry.prefer.join(", ")} — not manageable here, edit it in Shapes`);
|
|
557
|
+
}
|
|
558
|
+
// 8. deny∩prefer collision — a standing lint on this shape's declaration, never a fleet toggle.
|
|
559
|
+
for (const collision of denyPreferCollisions(cfgPreview, [shape])) {
|
|
560
|
+
captions.push(denyPreferCollisionLine(collision));
|
|
561
|
+
}
|
|
562
|
+
// 9. first-match provenance — the per-channel ledger below lists EVERY collector scope.
|
|
563
|
+
return captions;
|
|
564
|
+
};
|
|
565
|
+
// LEG2-T3 finding 1: each channel a mechanism touches on this shape gets its own ledger row
|
|
566
|
+
// naming every reason — collector scopes with their config paths (allow, flat and workers deny,
|
|
567
|
+
// all of them, never a first match), the unauthed probe, pin/pool/floor, the task hint and a
|
|
568
|
+
// deny∩prefer collision it is the target of — or that leaves the shape with no candidate at all.
|
|
569
|
+
const channelLedger = (shape, map, cfgPreview, offered) => {
|
|
570
|
+
const entry = map[shape];
|
|
571
|
+
const floor = cfgPreview.routing.floors[shape];
|
|
572
|
+
const collisions = denyPreferCollisions(cfgPreview, [shape]);
|
|
573
|
+
const discovered = new Set(previewChannels.map((c) => `${c.adapter}:${c.model}`));
|
|
574
|
+
// discovery already dropped a channel whose model probe failed — it still gets its own row
|
|
575
|
+
const unauthedRows = [
|
|
576
|
+
...unauthedClis.map((adapter) => `${adapter} — CLI unauthed — re-probe with tickmarkr doctor`),
|
|
577
|
+
...unauthedModels.filter(({ key }) => !discovered.has(key))
|
|
578
|
+
.map(({ key, reason }) => `${key.replace(":", "/")} — unauthed (${reason}) — re-probe with tickmarkr doctor`),
|
|
579
|
+
];
|
|
580
|
+
return [...unauthedRows, ...previewChannels.flatMap((c) => {
|
|
581
|
+
const key = `${c.adapter}:${c.model}`;
|
|
582
|
+
const reasons = exclusionCollector(c, cfgPreview.routing, "worker").map(exclusionReason);
|
|
583
|
+
if (health[c.adapter]?.authed === false)
|
|
584
|
+
reasons.push(`${c.adapter} CLI unauthed — re-probe with tickmarkr doctor`);
|
|
585
|
+
else if (health[c.adapter]?.modelAuth?.[c.model]?.authed === false)
|
|
586
|
+
reasons.push("unauthed — re-probe with tickmarkr doctor");
|
|
587
|
+
if (entry?.pin && key !== `${entry.pin.via}:${entry.pin.model}`) {
|
|
588
|
+
reasons.push(`not routing.map.${shape}.pin (${entry.pin.via}:${entry.pin.model}) — not manageable here`);
|
|
589
|
+
}
|
|
590
|
+
if (entry?.pool && !entry.pool.channels.includes(key)) {
|
|
591
|
+
reasons.push(`outside routing.map.${shape}.pool — not manageable here`);
|
|
592
|
+
}
|
|
593
|
+
if (floor && TIER_RANK[c.tier] < TIER_RANK[floor]) {
|
|
594
|
+
reasons.push(`below routing.floors.${shape} (${floor}) — not manageable here`);
|
|
595
|
+
}
|
|
596
|
+
if (entry?.prefer?.some((p) => entryMatchesChannel(p, c))) {
|
|
597
|
+
reasons.push(`task hint: routing.map.${shape}.prefer names it — not manageable here`);
|
|
598
|
+
}
|
|
599
|
+
for (const collision of collisions) {
|
|
600
|
+
const named = collision.detail.split(" > ").some((p) => entryMatchesChannel(p, c));
|
|
601
|
+
if (named || offered.size === 0)
|
|
602
|
+
reasons.push(denyPreferCollisionLine(collision));
|
|
603
|
+
}
|
|
604
|
+
// labelled like the models view's row (adapter/model), so a ledger row never reads as a candidate
|
|
605
|
+
return reasons.length ? [`${c.adapter}/${c.model} — ${reasons.join("; ")}`] : [];
|
|
606
|
+
})];
|
|
607
|
+
};
|
|
462
608
|
const candidatesForShape = (shape, mode, map, deny) => {
|
|
463
|
-
const
|
|
609
|
+
const preview = previewCfg(mode, map, deny);
|
|
610
|
+
if (!preview.ok)
|
|
611
|
+
return { rows: [], excludedNote: previewUnavailable(preview.error) };
|
|
612
|
+
const cfgPreview = preview.cfg;
|
|
464
613
|
const rows = shapeCandidates(previewTask(shape), cfgPreview, previewPool(cfgPreview), profile).map((candidate) => ({
|
|
465
614
|
id: `${candidate.assignment.adapter}:${candidate.assignment.model}`,
|
|
466
615
|
label: candidateRow(candidate, cfg.pricing),
|
|
467
616
|
pin: { via: candidate.assignment.adapter, model: candidate.assignment.model },
|
|
468
617
|
}));
|
|
469
|
-
|
|
618
|
+
const offered = new Set(rows.map((row) => row.id));
|
|
619
|
+
const notes = [excludedNoteFor(cfgPreview), ...shapeExclusionCaptions(shape, map, cfgPreview)].filter((note) => note !== undefined);
|
|
620
|
+
return { rows, excludedNote: notes.length ? notes.join("\n") : undefined, ledger: channelLedger(shape, map, cfgPreview, offered) };
|
|
470
621
|
};
|
|
471
622
|
const preferUniverse = [
|
|
472
623
|
...new Set(channels.flatMap((channel) => [
|
|
@@ -493,6 +644,9 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
493
644
|
const staged = structuredClone(initial);
|
|
494
645
|
staged.denyAdapters = state.denyAdapters;
|
|
495
646
|
staged.denyModels = state.denyModels;
|
|
647
|
+
// OBS-994/FL-1: the workers-only deny scope rides the same review/write funnel as the flat one.
|
|
648
|
+
staged.denyWorkersAdapters = state.denyWorkersAdapters;
|
|
649
|
+
staged.denyWorkersModels = state.denyWorkersModels;
|
|
496
650
|
staged.map = state.map;
|
|
497
651
|
const today = new Date().toISOString().slice(0, 10);
|
|
498
652
|
for (const classification of state.classifications) {
|
|
@@ -545,6 +699,8 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
545
699
|
health,
|
|
546
700
|
initialDenyAdapters: editable.denyAdapters,
|
|
547
701
|
initialDenyModels: editable.denyModels,
|
|
702
|
+
initialDenyWorkersAdapters: editable.denyWorkersAdapters,
|
|
703
|
+
initialDenyWorkersModels: editable.denyWorkersModels,
|
|
548
704
|
modelGroups,
|
|
549
705
|
initialMode: rm.mode.mode,
|
|
550
706
|
modeOptions: ROUTING_MODES.map((mode) => ({ id: mode, gloss: MODE_GLOSS[mode] })),
|
|
@@ -560,6 +716,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
560
716
|
steeringOptionsFor,
|
|
561
717
|
reviewOverlay,
|
|
562
718
|
reloadGuard,
|
|
719
|
+
stagedRouting,
|
|
563
720
|
entry: opts.entry,
|
|
564
721
|
initialJudge,
|
|
565
722
|
judgeSeats: seats,
|
|
@@ -607,6 +764,11 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
607
764
|
return {
|
|
608
765
|
props,
|
|
609
766
|
commit,
|
|
610
|
-
renderWhy: () => renderFleetWhy(projectedShapeRows(rm.mode.mode, editable.map, {
|
|
767
|
+
renderWhy: () => renderFleetWhy(projectedShapeRows(rm.mode.mode, editable.map, {
|
|
768
|
+
adapters: editable.denyAdapters,
|
|
769
|
+
models: editable.denyModels,
|
|
770
|
+
workersAdapters: editable.denyWorkersAdapters ?? [],
|
|
771
|
+
workersModels: editable.denyWorkersModels ?? [],
|
|
772
|
+
})),
|
|
611
773
|
};
|
|
612
774
|
}
|