tickmarkr 2.6.3 → 2.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/dist/adapters/catalog-remote.d.ts +23 -0
- package/dist/adapters/catalog-remote.js +38 -0
- package/dist/adapters/claude-code.d.ts +1 -1
- package/dist/adapters/claude-code.js +4 -1
- package/dist/adapters/codex-commit-check.d.ts +25 -0
- package/dist/adapters/codex-commit-check.js +108 -0
- package/dist/adapters/codex.d.ts +2 -1
- package/dist/adapters/codex.js +26 -5
- package/dist/adapters/model-discovery-check.d.ts +33 -0
- package/dist/adapters/model-discovery-check.js +123 -0
- package/dist/adapters/model-lints.d.ts +2 -1
- package/dist/adapters/model-lints.js +52 -1
- package/dist/adapters/model-windows.d.ts +2 -1
- package/dist/adapters/model-windows.js +18 -7
- package/dist/adapters/prompt.js +3 -2
- package/dist/adapters/types.d.ts +13 -0
- package/dist/cli/commands/approve.js +37 -6
- package/dist/cli/commands/doctor.d.ts +4 -0
- package/dist/cli/commands/doctor.js +20 -3
- package/dist/cli/commands/fleet.d.ts +10 -1
- package/dist/cli/commands/fleet.js +279 -95
- package/dist/cli/commands/init.js +10 -4
- package/dist/cli/commands/plan.js +65 -4
- package/dist/cli/commands/report.d.ts +39 -2
- package/dist/cli/commands/report.js +227 -10
- package/dist/cli/commands/resume.js +2 -4
- package/dist/cli/commands/run.d.ts +11 -0
- package/dist/cli/commands/run.js +33 -5
- package/dist/cli/commands/status.js +30 -3
- package/dist/cli/commands/verify.d.ts +1 -1
- package/dist/cli/commands/verify.js +356 -123
- package/dist/compile/native.js +130 -0
- package/dist/config/config.d.ts +25 -7
- package/dist/config/config.js +92 -11
- package/dist/config/fleet-overlay.d.ts +2 -1
- package/dist/config/fleet-overlay.js +77 -4
- package/dist/drivers/orca.d.ts +19 -1
- package/dist/drivers/orca.js +161 -31
- package/dist/gates/cache.d.ts +4 -0
- package/dist/gates/cache.js +10 -2
- package/dist/gates/llm.d.ts +4 -1
- package/dist/gates/llm.js +6 -1
- package/dist/gates/review.js +8 -3
- package/dist/gates/run-gates.d.ts +3 -2
- package/dist/gates/run-gates.js +91 -50
- package/dist/route/router.d.ts +11 -0
- package/dist/route/router.js +29 -8
- package/dist/run/daemon.js +158 -22
- package/dist/run/git.js +9 -0
- package/dist/run/journal.d.ts +101 -1
- package/dist/run/journal.js +352 -2
- package/dist/run/launchservices-check.d.ts +56 -0
- package/dist/run/launchservices-check.js +117 -0
- package/dist/run/operator-state.d.ts +22 -2
- package/dist/run/operator-state.js +20 -3
- package/dist/run/title-environment.d.ts +45 -0
- package/dist/run/title-environment.js +127 -0
- package/dist/run/title-preload.cjs +38 -0
- package/dist/run/title-preload.d.cts +1 -0
- package/dist/tui/cockpit/board.js +12 -6
- package/dist/tui/cockpit/keys.d.ts +8 -0
- package/dist/tui/cockpit/keys.js +4 -1
- package/dist/tui/cockpit/layout.d.ts +2 -2
- package/dist/tui/cockpit/layout.js +2 -2
- package/dist/tui/cockpit/live-runtime.js +46 -20
- package/dist/tui/cockpit/live-store.d.ts +30 -1
- package/dist/tui/cockpit/live-store.js +134 -18
- package/dist/tui/cockpit/log-view.d.ts +28 -0
- package/dist/tui/cockpit/log-view.js +60 -0
- package/dist/tui/cockpit/run-view.js +4 -0
- package/dist/tui/cockpit/shell.d.ts +2 -1
- package/dist/tui/cockpit/shell.js +6 -4
- package/dist/tui/ink/fleet-app.d.ts +12 -6
- package/dist/tui/ink/fleet-app.js +17 -11
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +33 -3
- package/skills/tickmarkr-loop/SKILL.md +55 -5
- package/skills/tickmarkr-overseer/SKILL.md +83 -8
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname } from "node:path";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse, modelAuthExclusions, PREFERENCE_ROLES } from "../../adapters/registry.js";
|
|
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 { ConfigError, fleetEditableFromConfig, fleetEditableEquals, formatFleetPrint, globalConfigDir, DENY_SCOPES, initialDenyPropOf, loadConfigWithMode, lowerLayerModelOverrides, overlayBytesLoadError, readOverlayFile, stagedDenyKeyOf, renderFleetOverlayWrite, repoOverlayPath, ROUTING_MODES, TIER_RANK, unifiedYamlDiff, } from "../../config/config.js";
|
|
8
|
+
import { ConfigError, DEFAULT_CONFIG, fleetEditableFromConfig, fleetEditableEquals, formatFleetPrint, globalConfigDir, DENY_SCOPES, initialDenyPropOf, loadConfigWithMode, lowerLayerModelOverrides, overlayBytesLoadError, readOverlayFile, stagedDenyKeyOf, renderFleetOverlayWrite, repoOverlayPath, repoShadowedUserKeys, ROUTING_MODES, TIER_RANK, unifiedYamlDiff, universeEntryMatches, } from "../../config/config.js";
|
|
9
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";
|
|
@@ -22,6 +22,16 @@ const QUIT = "fleet: quit without writing";
|
|
|
22
22
|
// (rankCandidates routes noExplore so repeated calls agree) — a due probe must never make a
|
|
23
23
|
// step-4/5 row disagree with the picker's rank-1 for the same shape and channel set.
|
|
24
24
|
const PREVIEW_EXPLORE = { noExplore: true };
|
|
25
|
+
// B2: a routing.map entry's declaration slot (pin/pool/prefer) — an empty prefer declares nothing
|
|
26
|
+
const mapSlot = (entry) => ({
|
|
27
|
+
...(entry?.pin ? { pin: entry.pin } : {}),
|
|
28
|
+
...(entry?.pool ? { pool: entry.pool } : {}),
|
|
29
|
+
...(entry?.prefer?.length ? { prefer: entry.prefer } : {}),
|
|
30
|
+
});
|
|
31
|
+
const slotLabel = (slot) => slot.pin ? `pin ${slot.pin.via}:${slot.pin.model}`
|
|
32
|
+
: slot.pool ? `pool(${slot.pool.mode}·${slot.pool.channels.length})`
|
|
33
|
+
: slot.prefer ? `prefer [${slot.prefer.join(", ")}]`
|
|
34
|
+
: "cleared slot";
|
|
25
35
|
function previewTask(shape) {
|
|
26
36
|
return {
|
|
27
37
|
id: "fleet-preview",
|
|
@@ -208,6 +218,10 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
208
218
|
* overlay write. */
|
|
209
219
|
export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
210
220
|
const globalDir = opts.globalDir;
|
|
221
|
+
// B2: the editor's choices are the operator's MACHINE choices — every save lands in the USER
|
|
222
|
+
// overlay, which every repository on this machine inherits. The repo overlay is read, never written.
|
|
223
|
+
const userPath = join(globalDir, "config.yaml");
|
|
224
|
+
const currentUserText = () => (existsSync(userPath) ? readFileSync(userPath, "utf8") : "");
|
|
211
225
|
const { reuse, health: cached } = initDoctorReuse(cwd, false);
|
|
212
226
|
if (!reuse || !cached) {
|
|
213
227
|
return {
|
|
@@ -225,10 +239,12 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
225
239
|
// writer emits the minimal allow form from it. The same pool later ranks every preview.
|
|
226
240
|
const { deny: _diskDeny, allow: _diskAllow, ...routingScopeBlind } = cfg.routing;
|
|
227
241
|
const previewChannels = discoverChannels({ ...cfg, routing: routingScopeBlind }, adapters, health);
|
|
228
|
-
|
|
242
|
+
// B2: the universe any scope-blind discovery serves — the assembly's, or a candidate config's at a
|
|
243
|
+
// membership write (current layers plus staged classifications)
|
|
244
|
+
const universeOf = (discovered) => adapters
|
|
229
245
|
.filter((adapter) => health[adapter.id]?.installed)
|
|
230
246
|
.map((adapter) => {
|
|
231
|
-
const served =
|
|
247
|
+
const served = discovered.filter((c) => c.adapter === adapter.id);
|
|
232
248
|
// LEG2-T3 round 2 finding 3: the recorded identity per channel, so the editable membership
|
|
233
249
|
// matches allow/deny entries with the same authority the collector and the browser use
|
|
234
250
|
const identities = Object.fromEntries(served.flatMap((c) => (c.identity ? [[c.model, c.identity]] : [])));
|
|
@@ -238,6 +254,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
238
254
|
...(Object.keys(identities).length ? { identities } : {}),
|
|
239
255
|
};
|
|
240
256
|
});
|
|
257
|
+
const universe = universeOf(previewChannels);
|
|
241
258
|
const initial = fleetEditableFromConfig(cfg, universe);
|
|
242
259
|
const editable = structuredClone(initial);
|
|
243
260
|
// OBS-508: the same catalog evidence doctor's drift overlay prints now rides each unclassified
|
|
@@ -346,30 +363,143 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
346
363
|
const channels = discoverChannels(cfg, adapters, health);
|
|
347
364
|
const profile = loadRoutingProfile(cwd, cfg, { preview: true });
|
|
348
365
|
const stagedDenyOf = (editable) => Object.fromEntries(DENY_SCOPES.map((scope) => [stagedDenyKeyOf(scope), editable[scope.key] ?? []]));
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
366
|
+
// T8: staged classifications and efforts as the editable tiers/efforts a review writes — one fold for
|
|
367
|
+
// the review and every preview, so the mode/shape/picker callbacks rank exactly what w would write.
|
|
368
|
+
const stagedMetadataOf = (stage) => {
|
|
369
|
+
const tiers = structuredClone(initial.tiers);
|
|
370
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
371
|
+
for (const classification of stage.classifications) {
|
|
372
|
+
tiers[classification.adapter] ??= {};
|
|
373
|
+
tiers[classification.adapter][classification.model] = {
|
|
374
|
+
tier: classification.tier,
|
|
375
|
+
provenance: `${classification.note} — fleet ${today}`,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
// OBS-1182: staged efforts replace the loaded ones; none staged anywhere ⇒ no key, as loaded
|
|
379
|
+
const efforts = Object.fromEntries(Object.entries(stage.efforts ?? {}).filter(([, models]) => Object.keys(models).length));
|
|
380
|
+
return { tiers, ...(Object.keys(efforts).length ? { efforts } : {}) };
|
|
381
|
+
};
|
|
382
|
+
// B2: the classifications and efforts of the last reviewed state — what a preview called without a
|
|
383
|
+
// stage (the browser's staged routing, --why) ranks; the editor passes its current stage (T8).
|
|
384
|
+
let reviewedStage = { tiers: initial.tiers, efforts: initial.efforts };
|
|
385
|
+
// OBS-1182: the layers under the written overlay — B2: the defaults alone under the USER overlay —
|
|
386
|
+
// read raw, so clearing an effort masks an inherited one. OBS-1188: re-read at every review and
|
|
387
|
+
// save, never an assembly-time snapshot; an unreadable read keeps the last good one for the
|
|
388
|
+
// preview only — the save guard below refuses it.
|
|
389
|
+
let lowerOverrides = {};
|
|
390
|
+
const stagedEditable = (map, deny, metadata) => ({
|
|
391
|
+
...structuredClone(initial),
|
|
392
|
+
efforts: undefined, // metadata carries efforts only when some are staged — as a review writes them
|
|
393
|
+
...structuredClone(metadata),
|
|
353
394
|
...Object.fromEntries(DENY_SCOPES.map((scope) => [scope.key, deny[stagedDenyKeyOf(scope)]])),
|
|
354
395
|
// OBS-1046: the allow complement is staged beside the deny lists, never folded into them
|
|
355
396
|
allowOut: deny.allowOut ?? initial.allowOut,
|
|
356
397
|
map,
|
|
357
398
|
});
|
|
399
|
+
// B2: the writer diffs a CHANGED routing.map slot against what its destination declares there —
|
|
400
|
+
// the user layer (defaults + user, no repo) — never the effective slot a repo pin/pool/prefer
|
|
401
|
+
// shadows, which would leave the user's own hidden declaration beside the new one (a stale pin a
|
|
402
|
+
// second repository still routes to, or pin+pool bytes that refuse a valid edit).
|
|
403
|
+
// B2: a touched membership is the same — the writer rewrites the allow form from the whole staged
|
|
404
|
+
// fleet, so its flat deny lists and allow complement diff against the user layer's own; a user
|
|
405
|
+
// deny the repo's list hides is admitted or kept by the staged fleet, never left to re-exclude
|
|
406
|
+
// it in every other repository.
|
|
407
|
+
const membershipKeys = [...DENY_SCOPES.filter((scope) => scope.path.length === 3).map((scope) => scope.key), "allowOut"];
|
|
408
|
+
const sameSet = (a = [], b = []) => [...new Set(a)].sort().join() === [...new Set(b)].sort().join();
|
|
409
|
+
const destinationInitial = (base, edited, userText, target) => {
|
|
410
|
+
let userCfg;
|
|
411
|
+
try {
|
|
412
|
+
userCfg = loadConfigWithMode(cwd, { globalDir, userOverlayText: userText, repoOverlayText: "" }).cfg;
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
return base; // ponytail: user bytes that load only beside this repo — the reload guard refuses the write
|
|
416
|
+
}
|
|
417
|
+
const userLayer = userCfg.routing.map;
|
|
418
|
+
const editedMap = edited.map;
|
|
419
|
+
const map = { ...base.map };
|
|
420
|
+
for (const shape of new Set([...Object.keys(base.map), ...Object.keys(editedMap)])) {
|
|
421
|
+
if (JSON.stringify(base.map[shape]) === JSON.stringify(editedMap[shape]))
|
|
422
|
+
continue;
|
|
423
|
+
const { pin, pool, prefer } = userLayer[shape] ?? {};
|
|
424
|
+
// escalate is no slot key: it merges per key, so the effective value stays the baseline
|
|
425
|
+
const escalate = base.map[shape]?.escalate;
|
|
426
|
+
map[shape] = {
|
|
427
|
+
...(pin ? { pin } : {}), ...(pool ? { pool } : {}), ...(prefer ? { prefer } : {}),
|
|
428
|
+
...(escalate !== undefined ? { escalate } : {}),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
const membershipTouched = membershipKeys.some((key) => !sameSet(base[key], edited[key]));
|
|
432
|
+
const destination = membershipTouched ? fleetEditableFromConfig(userCfg, target) : undefined;
|
|
433
|
+
return {
|
|
434
|
+
...base,
|
|
435
|
+
map,
|
|
436
|
+
...(destination ? Object.fromEntries(membershipKeys.map((key) => [key, destination[key] ?? []])) : {}),
|
|
437
|
+
};
|
|
438
|
+
};
|
|
439
|
+
// B2: a changed slot staged with no declaration (auto, no prefer) where the defaults under the user
|
|
440
|
+
// overlay declare one: deleting the user's pin/pool would restore that default, so the slot is
|
|
441
|
+
// masked with prefer: [] — a declaration that replaces the lower slot atomically and ranks nothing.
|
|
442
|
+
// T8: a default PIN is masked by the writer's raw pin: null tombstone instead (lowerMap below), so
|
|
443
|
+
// Auto saves no prefer declaration the operator never made.
|
|
444
|
+
const maskClearedSlots = (base, staged) => {
|
|
445
|
+
const map = { ...staged.map };
|
|
446
|
+
for (const shape of new Set([...Object.keys(base.map), ...Object.keys(staged.map)])) {
|
|
447
|
+
const entry = staged.map[shape];
|
|
448
|
+
const lower = mapSlot(DEFAULT_CONFIG.routing.map[shape]);
|
|
449
|
+
if (JSON.stringify(base.map[shape]) === JSON.stringify(entry))
|
|
450
|
+
continue;
|
|
451
|
+
if (Object.keys(mapSlot(entry)).length || !Object.keys(lower).length || lower.pin)
|
|
452
|
+
continue;
|
|
453
|
+
map[shape] = { ...entry, prefer: [] };
|
|
454
|
+
}
|
|
455
|
+
return { ...staged, map };
|
|
456
|
+
};
|
|
457
|
+
// B2: the one user-layer write builder the previews and the review share, so a preview ranks the
|
|
458
|
+
// bytes y would land. A touched membership serializes against the universe the candidate itself
|
|
459
|
+
// serves (current layers plus staged classifications), rendered first with membership unchanged;
|
|
460
|
+
// a channel only that universe adds keeps the verdict the current layers' allow form gives it.
|
|
461
|
+
const userLayerWrite = (staged, userText, rest) => {
|
|
462
|
+
let edited = maskClearedSlots(initial, staged);
|
|
463
|
+
let target = universe;
|
|
464
|
+
if (membershipKeys.some((key) => !sameSet(initial[key], edited[key]))) {
|
|
465
|
+
const unchanged = { ...edited, ...Object.fromEntries(membershipKeys.map((key) => [key, initial[key]])) };
|
|
466
|
+
try {
|
|
467
|
+
const bytes = renderFleetOverlayWrite(userText, {
|
|
468
|
+
...rest, initial: destinationInitial(initial, unchanged, userText, universe), edited: unchanged, universe,
|
|
469
|
+
lowerMap: DEFAULT_CONFIG.routing.map,
|
|
470
|
+
});
|
|
471
|
+
const candidate = loadConfigWithMode(cwd, { globalDir, userOverlayText: bytes }).cfg;
|
|
472
|
+
target = universeOf(candidateChannels(candidate));
|
|
473
|
+
const known = (entry) => universe.some((row) => entry === row.adapter || row.models.some((model) => universeEntryMatches(row, model, entry)));
|
|
474
|
+
const grown = (fleetEditableFromConfig(candidate, target).allowOut ?? []).filter((entry) => !known(entry));
|
|
475
|
+
if (grown.length)
|
|
476
|
+
edited = { ...edited, allowOut: [...new Set([...(edited.allowOut ?? []), ...grown])].sort() };
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
// the full render below (or the reload guard) names the refusal
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// T8: the user overlay's lower layer is the defaults alone (B2) — a pin cleared over one is tombstoned
|
|
483
|
+
return { ...rest, initial: destinationInitial(initial, edited, userText, target), edited, universe: target, lowerMap: DEFAULT_CONFIG.routing.map };
|
|
484
|
+
};
|
|
358
485
|
const candidateMemo = new Map();
|
|
359
|
-
const previewCfg = (mode, map, deny) => {
|
|
360
|
-
|
|
486
|
+
const previewCfg = (mode, map, deny, stage) => {
|
|
487
|
+
// B2: the user and repo overlay bytes are part of the key — a layer edited mid-session (a repo
|
|
488
|
+
// deny added before a review) re-renders the preview instead of serving the pre-edit one.
|
|
489
|
+
// T8: so are the staged tiers/efforts — a metadata edit never serves the preview cached before it
|
|
490
|
+
const metadata = stage ? stagedMetadataOf(stage) : reviewedStage;
|
|
491
|
+
const userText = currentUserText();
|
|
492
|
+
const key = JSON.stringify([mode, map, deny, metadata, userText, currentRepoOverlayText(cwd)]);
|
|
361
493
|
const hit = candidateMemo.get(key);
|
|
362
494
|
if (hit)
|
|
363
495
|
return hit;
|
|
364
496
|
let preview;
|
|
365
497
|
try {
|
|
366
|
-
const bytes = renderFleetOverlayWrite(
|
|
367
|
-
|
|
368
|
-
edited: stagedEditable(map, deny),
|
|
369
|
-
universe,
|
|
498
|
+
const bytes = renderFleetOverlayWrite(userText, userLayerWrite(stagedEditable(map, deny, metadata), userText, {
|
|
499
|
+
lowerOverrides,
|
|
370
500
|
...(mode !== rm.mode.mode ? { mode } : {}),
|
|
371
|
-
});
|
|
372
|
-
preview = { ok: true,
|
|
501
|
+
}));
|
|
502
|
+
preview = { ok: true, ...loadConfigWithMode(cwd, { globalDir, userOverlayText: bytes }) };
|
|
373
503
|
}
|
|
374
504
|
catch (error) {
|
|
375
505
|
preview = { ok: false, error: error.message.replace(/\s+/g, " ").trim() };
|
|
@@ -384,15 +514,28 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
384
514
|
const preview = previewCfg(rm.mode.mode, editable.map, deny);
|
|
385
515
|
return preview.ok ? { ok: true, routing: preview.cfg.routing } : preview;
|
|
386
516
|
};
|
|
517
|
+
// B2: the candidate config's own discovery, blind to both membership scopes — classifications and
|
|
518
|
+
// channel metadata (tier, channel) a reloaded layer changed reach routing, ranking and the ledger,
|
|
519
|
+
// never the assembly-time channels. Keyed on the memoized candidate config object.
|
|
520
|
+
const candidateChannelMemo = new WeakMap();
|
|
521
|
+
const candidateChannels = (cfgPreview) => {
|
|
522
|
+
const hit = candidateChannelMemo.get(cfgPreview);
|
|
523
|
+
if (hit)
|
|
524
|
+
return hit;
|
|
525
|
+
const { deny: _deny, allow: _allow, ...blind } = cfgPreview.routing;
|
|
526
|
+
const discovered = discoverChannels({ ...cfgPreview, routing: blind }, adapters, health);
|
|
527
|
+
candidateChannelMemo.set(cfgPreview, discovered);
|
|
528
|
+
return discovered;
|
|
529
|
+
};
|
|
387
530
|
// route() never deny-filters its pool — that contract lives in discoverChannels — so every
|
|
388
531
|
// preview call rebuilds the pool the staged deny would discover
|
|
389
|
-
const previewPool = (cfgPreview) =>
|
|
390
|
-
const modeSpend = (mode, map, deny) => {
|
|
532
|
+
const previewPool = (cfgPreview) => candidateChannels(cfgPreview).filter((c) => disallowedBy(c, cfgPreview.routing) === null);
|
|
533
|
+
const modeSpend = (mode, map, deny, stage) => {
|
|
391
534
|
const tierCount = {};
|
|
392
535
|
let subs = 0;
|
|
393
536
|
let apiN = 0;
|
|
394
537
|
let apiUsd = 0;
|
|
395
|
-
const preview = previewCfg(mode, map, deny);
|
|
538
|
+
const preview = previewCfg(mode, map, deny, stage);
|
|
396
539
|
if (!preview.ok)
|
|
397
540
|
return ` mix: ${previewUnavailable(preview.error)}`;
|
|
398
541
|
const cfgPreview = preview.cfg;
|
|
@@ -405,7 +548,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
405
548
|
subs += 1;
|
|
406
549
|
else {
|
|
407
550
|
apiN += 1;
|
|
408
|
-
apiUsd +=
|
|
551
|
+
apiUsd += cfgPreview.pricing[assignment.tier] ?? 0;
|
|
409
552
|
}
|
|
410
553
|
}
|
|
411
554
|
catch {
|
|
@@ -423,11 +566,16 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
423
566
|
parts.push(`${unroutable} unroutable`);
|
|
424
567
|
return ` mix: ${mix} — ${parts.join(" · ")}`;
|
|
425
568
|
};
|
|
426
|
-
|
|
569
|
+
// B2: the floors the candidate user-layer bytes resolve to — the same config the routed mix ranks
|
|
570
|
+
// against, so a repo-declared mode that shadows the user's pick previews no floor change
|
|
571
|
+
const floorPreview = (mode, map, deny, stage) => {
|
|
427
572
|
if (mode === rm.mode.mode)
|
|
428
573
|
return [];
|
|
574
|
+
const preview = previewCfg(mode, map, deny, stage);
|
|
575
|
+
if (!preview.ok)
|
|
576
|
+
return []; // modeSpend names the refusal
|
|
429
577
|
const current = cfg.routing.floors;
|
|
430
|
-
const next =
|
|
578
|
+
const next = preview.cfg.routing.floors;
|
|
431
579
|
const changed = SHAPES.filter((shape) => current[shape] !== next[shape]);
|
|
432
580
|
return [
|
|
433
581
|
` floors vs ${rm.mode.mode}:`,
|
|
@@ -436,66 +584,79 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
436
584
|
: [" (no floor changes)"]),
|
|
437
585
|
];
|
|
438
586
|
};
|
|
439
|
-
|
|
440
|
-
|
|
587
|
+
// B2: `resolution` is the candidate user-layer bytes' own mode resolution — a repo-declared mode that
|
|
588
|
+
// shadows the user's pick keeps its provenance, never an operator pin the loader would not apply
|
|
589
|
+
// B2: `entry` is the slot the candidate's loader applies — a staged slot a repo declaration shadows
|
|
590
|
+
// is no operator pin here; the repo's winning declaration keeps its provenance
|
|
591
|
+
const whyDeclaration = (shape, resolution, entry, mapProducedValue) => {
|
|
592
|
+
const mapChanged = JSON.stringify(mapSlot(entry)) !== JSON.stringify(mapSlot(editable.map[shape]));
|
|
441
593
|
if (mapProducedValue) {
|
|
442
594
|
return mapChanged
|
|
443
595
|
? { operatorPinned: true }
|
|
444
596
|
: { declaredAt: `routing.map.${shape}` };
|
|
445
597
|
}
|
|
446
|
-
const floorSource =
|
|
598
|
+
const floorSource = resolution.provenance[shape];
|
|
447
599
|
if (floorSource === undefined)
|
|
448
600
|
return {};
|
|
449
601
|
if (floorSource === "config floors")
|
|
450
602
|
return { declaredAt: `routing.floors.${shape}` };
|
|
451
|
-
if (mode !== rm.mode.mode)
|
|
603
|
+
if (resolution.mode !== rm.mode.mode)
|
|
452
604
|
return { operatorPinned: true };
|
|
453
605
|
return rm.source === "default"
|
|
454
606
|
? { declaredAt: `routing.floors.${shape}` }
|
|
455
607
|
: { declaredAt: "routing.mode" };
|
|
456
608
|
};
|
|
457
|
-
const projectedShapeRows = (mode, map, deny) => {
|
|
458
|
-
const preview = previewCfg(mode, map, deny);
|
|
609
|
+
const projectedShapeRows = (mode, map, deny, stage) => {
|
|
610
|
+
const preview = previewCfg(mode, map, deny, stage);
|
|
459
611
|
if (!preview.ok) {
|
|
460
612
|
return projectFleetWhy(SHAPES.map((shape) => ({
|
|
461
613
|
id: shape,
|
|
462
614
|
effective: previewUnavailable(preview.error),
|
|
463
|
-
...whyDeclaration(shape, mode, map, false),
|
|
615
|
+
...whyDeclaration(shape, modeCfgs[mode].mode, map[shape], false),
|
|
464
616
|
})), { repoRoot: cwd, globalDir });
|
|
465
617
|
}
|
|
466
618
|
const cfgPreview = preview.cfg;
|
|
467
619
|
const pool = previewPool(cfgPreview);
|
|
468
620
|
const values = SHAPES.map((shape) => {
|
|
621
|
+
// B2: annotations and provenance read the slot the loader applies; a staged slot it does not
|
|
622
|
+
// apply (a repo pin/pool/prefer above the user overlay) is named shadowed, never shown as routing
|
|
623
|
+
const entry = cfgPreview.routing.map[shape];
|
|
624
|
+
const staged = mapSlot(map[shape]);
|
|
625
|
+
const shadowed = JSON.stringify(staged) !== JSON.stringify(mapSlot(editable.map[shape]))
|
|
626
|
+
&& JSON.stringify(staged) !== JSON.stringify(mapSlot(entry));
|
|
627
|
+
const shadowNote = shadowed ? ` · staged ${slotLabel(staged)} shadowed — not applied in this repository` : "";
|
|
469
628
|
try {
|
|
470
629
|
const routed = route(previewTask(shape), cfgPreview, pool, profile, undefined, undefined, PREVIEW_EXPLORE);
|
|
471
630
|
const assignment = routed.assignment;
|
|
472
631
|
// OBS-531: a pooled shape used to render only the routed WINNER — indistinguishable from
|
|
473
632
|
// a pin. The declaration is the operator's decision; the winner is today's dice.
|
|
474
|
-
const declaredPool =
|
|
633
|
+
const declaredPool = entry?.pool;
|
|
475
634
|
const poolPrefix = declaredPool ? `pool(${declaredPool.mode}·${declaredPool.channels.length}) → ` : "";
|
|
476
|
-
|
|
477
|
-
const
|
|
635
|
+
// T8: the launch effort the routed channel carries — a staged effort shows here before w
|
|
636
|
+
const effort = assignment.effort ? `, effort ${assignment.effort}` : "";
|
|
637
|
+
const effective = `${poolPrefix}${assignment.adapter}:${assignment.model} (${assignment.channel}, ${assignment.tier}${effort}) ${costSignal(assignment, cfgPreview.pricing)}${shadowNote}`;
|
|
638
|
+
const mapProducedValue = entry?.pin !== undefined || entry?.pool !== undefined
|
|
478
639
|
|| routed.provenance.includes("via prefer");
|
|
479
640
|
return {
|
|
480
641
|
id: shape,
|
|
481
642
|
effective,
|
|
482
|
-
...whyDeclaration(shape, mode,
|
|
643
|
+
...whyDeclaration(shape, preview.mode, entry, mapProducedValue),
|
|
483
644
|
};
|
|
484
645
|
}
|
|
485
646
|
catch (error) {
|
|
486
|
-
const mapProducedValue =
|
|
487
|
-
|| (
|
|
647
|
+
const mapProducedValue = entry?.pin !== undefined || entry?.pool !== undefined
|
|
648
|
+
|| (entry?.prefer?.length ?? 0) > 0;
|
|
488
649
|
return {
|
|
489
650
|
id: shape,
|
|
490
|
-
effective: error.message
|
|
491
|
-
...whyDeclaration(shape, mode,
|
|
651
|
+
effective: `${error.message}${shadowNote}`,
|
|
652
|
+
...whyDeclaration(shape, preview.mode, entry, mapProducedValue),
|
|
492
653
|
setupCommand: "tickmarkr fleet",
|
|
493
654
|
};
|
|
494
655
|
}
|
|
495
656
|
});
|
|
496
657
|
return projectFleetWhy(values, { repoRoot: cwd, globalDir });
|
|
497
658
|
};
|
|
498
|
-
const routedShapeRows = (mode, map, deny) => projectedShapeRows(mode, map, deny).map(({ id, label }) => ({ id, label }));
|
|
659
|
+
const routedShapeRows = (mode, map, deny, stage) => projectedShapeRows(mode, map, deny, stage).map(({ id, label }) => ({ id, label }));
|
|
499
660
|
// OBS-530: the picker silently omitted every excluded channel — the operator asked "why is X
|
|
500
661
|
// missing" three times in one session and the answer lived only in config archaeology. One dim
|
|
501
662
|
// line now names each bucket. Static buckets (unauthed, unclassified) computed once; the
|
|
@@ -508,7 +669,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
508
669
|
const excludedNoteFor = (cfgPreview) => {
|
|
509
670
|
let stagedOut = 0;
|
|
510
671
|
let denied = 0;
|
|
511
|
-
for (const channel of
|
|
672
|
+
for (const channel of candidateChannels(cfgPreview)) {
|
|
512
673
|
if (disallowedBy(channel, cfgPreview.routing) === null)
|
|
513
674
|
continue;
|
|
514
675
|
if (disallowedBy(channel, cfg.routing) === null)
|
|
@@ -534,9 +695,11 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
534
695
|
// silent omission is indistinguishable from a bug (OBS-530's lesson, extended past deny/allow).
|
|
535
696
|
// allow and the unauthed probe already carry a reason through excludedNoteFor above; the rest
|
|
536
697
|
// are shape-scoped (a pin/pool/floor/prefer excludes only for THIS shape) and belong here.
|
|
537
|
-
|
|
698
|
+
// B2: captions read the slot the candidate's loader applies — a staged slot a repo declaration
|
|
699
|
+
// shadows excludes nothing here, so it is never named as the reason
|
|
700
|
+
const shapeExclusionCaptions = (shape, cfgPreview) => {
|
|
538
701
|
const pool = previewPool(cfgPreview);
|
|
539
|
-
const entry = map[shape];
|
|
702
|
+
const entry = cfgPreview.routing.map[shape];
|
|
540
703
|
const captions = [];
|
|
541
704
|
// 2. pin or pool — a closed candidate set declared for this shape; a channel outside it is
|
|
542
705
|
// not manageable from the models view, it is a Shapes-view declaration.
|
|
@@ -594,10 +757,11 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
594
757
|
// all of them, never a first match), the unauthed probe, pin/pool/floor, the task hint and a
|
|
595
758
|
// deny∩prefer collision it is the target of — or that leaves the shape with no candidate at all.
|
|
596
759
|
const channelLedger = (shape, map, cfgPreview, offered) => {
|
|
597
|
-
|
|
760
|
+
// B2: reasons name the applied slot; the carried chain below stays the staged one Space edits
|
|
761
|
+
const entry = cfgPreview.routing.map[shape];
|
|
598
762
|
const floor = cfgPreview.routing.floors[shape];
|
|
599
763
|
const collisions = denyPreferCollisions(cfgPreview, [shape], health);
|
|
600
|
-
const discovered = new Set(
|
|
764
|
+
const discovered = new Set(candidateChannels(cfgPreview).map((c) => `${c.adapter}:${c.model}`));
|
|
601
765
|
// T2 review (D-225): discovery already dropped a channel whose model probe failed — it is
|
|
602
766
|
// rebuilt as a structured channel from its adapter's own declaration and walks the SAME
|
|
603
767
|
// reach + collector path as every other row, the auth reason appended LAST; never a bare string
|
|
@@ -606,17 +770,17 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
606
770
|
return identity ? { ...declared, identity } : declared;
|
|
607
771
|
};
|
|
608
772
|
const failedProbes = unauthedModels.filter(({ key }) => !discovered.has(key)).flatMap(({ key, adapter, reason }) => {
|
|
609
|
-
const declared = adapters.find((a) => a.id === adapter)?.channels(
|
|
773
|
+
const declared = adapters.find((a) => a.id === adapter)?.channels(cfgPreview).find((c) => `${c.adapter}:${c.model}` === key);
|
|
610
774
|
return declared ? [{ ...withIdentity(adapter, declared), authReason: `unauthed (${reason}) — re-probe with tickmarkr doctor` }] : [];
|
|
611
775
|
});
|
|
612
776
|
// T2 review round 6: an installed-but-unauthed CLI is dropped by discovery AND by the model
|
|
613
777
|
// probe list, so its configured channels are rebuilt from the adapter's own declaration too —
|
|
614
778
|
// each greys with its reach and covering entry, the CLI auth reason last; never one bare adapter line
|
|
615
|
-
const unauthedCliChannels = unauthedClis.flatMap((adapter) => (adapters.find((a) => a.id === adapter)?.channels(
|
|
779
|
+
const unauthedCliChannels = unauthedClis.flatMap((adapter) => (adapters.find((a) => a.id === adapter)?.channels(cfgPreview) ?? [])
|
|
616
780
|
.map((declared) => ({ ...withIdentity(adapter, declared), authReason: `${adapter} CLI unauthed — re-probe with tickmarkr doctor` })));
|
|
617
781
|
// D-225: the ledger is the EXCLUDED half of a closed partition — a channel the picker offers
|
|
618
782
|
// never greys beside itself; its pin/pool/floor/prefer captions stay in excludedNote
|
|
619
|
-
const ledgerChannels = [...
|
|
783
|
+
const ledgerChannels = [...candidateChannels(cfgPreview).map((c) => ({ ...c, authReason: undefined })), ...failedProbes, ...unauthedCliChannels]
|
|
620
784
|
.filter((c) => !offered.has(`${c.adapter}:${c.model}`));
|
|
621
785
|
const lines = ledgerChannels.flatMap((c) => {
|
|
622
786
|
const key = `${c.adapter}:${c.model}`;
|
|
@@ -652,7 +816,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
652
816
|
// OBS-1145 add.1: every carried pool member the picker does not offer leads the ledger in chain
|
|
653
817
|
// order — one no reason above names still gets its own row — so the picker renders its ordinal
|
|
654
818
|
// and Space can drop it; a chain member with no row is hidden AND irremovable
|
|
655
|
-
const carried =
|
|
819
|
+
const carried = map[shape]?.pool?.channels.filter((key) => !offered.has(key)) ?? [];
|
|
656
820
|
const named = new Set(lines.map((row) => row.key));
|
|
657
821
|
const unnamed = carried.filter((key) => !named.has(key)).map((key) => ({
|
|
658
822
|
key,
|
|
@@ -661,20 +825,20 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
661
825
|
const order = (key) => (carried.includes(key) ? carried.indexOf(key) : carried.length);
|
|
662
826
|
return [...lines, ...unnamed].sort((a, b) => order(a.key) - order(b.key)).map((row) => row.line);
|
|
663
827
|
};
|
|
664
|
-
const candidatesForShape = (shape, mode, map, deny) => {
|
|
665
|
-
const preview = previewCfg(mode, map, deny);
|
|
828
|
+
const candidatesForShape = (shape, mode, map, deny, stage) => {
|
|
829
|
+
const preview = previewCfg(mode, map, deny, stage);
|
|
666
830
|
if (!preview.ok)
|
|
667
831
|
return { rows: [], excludedNote: previewUnavailable(preview.error) };
|
|
668
832
|
const cfgPreview = preview.cfg;
|
|
669
833
|
const rows = shapeCandidates(previewTask(shape), cfgPreview, previewPool(cfgPreview), profile).map((candidate) => ({
|
|
670
834
|
id: `${candidate.assignment.adapter}:${candidate.assignment.model}`,
|
|
671
|
-
label: candidateRow(candidate,
|
|
835
|
+
label: candidateRow(candidate, cfgPreview.pricing),
|
|
672
836
|
pin: { via: candidate.assignment.adapter, model: candidate.assignment.model },
|
|
673
837
|
// OBS-1065: a picker lift reads eligibility from here, not the ledger strings
|
|
674
838
|
belowFloor: candidate.belowFloor,
|
|
675
839
|
}));
|
|
676
840
|
const offered = new Set(rows.map((row) => row.id));
|
|
677
|
-
const notes = [excludedNoteFor(cfgPreview), ...shapeExclusionCaptions(shape,
|
|
841
|
+
const notes = [excludedNoteFor(cfgPreview), ...shapeExclusionCaptions(shape, cfgPreview)].filter((note) => note !== undefined);
|
|
678
842
|
return { rows, excludedNote: notes.length ? notes.join("\n") : undefined, ledger: channelLedger(shape, map, cfgPreview, offered) };
|
|
679
843
|
};
|
|
680
844
|
const preferUniverse = [
|
|
@@ -702,51 +866,58 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
702
866
|
// discovered seats universe plus a keep-default row, and a write is staged only on a real change.
|
|
703
867
|
const initialJudge = `${cfg.judge.adapter}:${cfg.judge.model}`;
|
|
704
868
|
let pendingWrite = null;
|
|
705
|
-
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
//
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
869
|
+
const lowerLayers = () => lowerLayerModelOverrides({ globalDir, below: "user" });
|
|
870
|
+
// OBS-1188: the reviewed bytes are the only bytes that may land. Any byte of the user or repo overlay
|
|
871
|
+
// that moved since the review is a stale preview — a re-render alone would overwrite a same-key edit
|
|
872
|
+
// with the staged value and still match — then the lower layers are re-rendered as they are NOW.
|
|
873
|
+
// Null = still current.
|
|
874
|
+
const stalePreview = (pending, reviewed, prior = currentUserText()) => {
|
|
875
|
+
if (currentRepoOverlayText(cwd) !== pending.repo) {
|
|
876
|
+
return `stale preview — the repository overlay ${repoOverlayPath(cwd)} changed after the review diff was rendered; press w to review what would be written now`;
|
|
877
|
+
}
|
|
878
|
+
if (prior !== pending.user) {
|
|
879
|
+
return `stale preview — the user overlay ${userPath} changed after the review diff was rendered; press w to review what would be written now`;
|
|
880
|
+
}
|
|
881
|
+
const lower = lowerLayers();
|
|
714
882
|
if (!lower.ok)
|
|
715
883
|
return lower.error;
|
|
716
884
|
try {
|
|
717
|
-
if (renderFleetOverlayWrite(prior, { ...write, lowerOverrides: lower.overrides }) === reviewed)
|
|
885
|
+
if (renderFleetOverlayWrite(prior, { ...pending.write, lowerOverrides: lower.overrides }) === reviewed)
|
|
718
886
|
return null;
|
|
719
887
|
}
|
|
720
888
|
catch (error) {
|
|
721
889
|
return error.message;
|
|
722
890
|
}
|
|
723
|
-
return
|
|
891
|
+
return `stale preview — the user overlay ${userPath} or a lower config layer changed after the review diff was rendered; press w to review what would be written now`;
|
|
724
892
|
};
|
|
893
|
+
// B2: a saved user key the repository overlay also decides does not apply in THIS repository —
|
|
894
|
+
// each is named with the value that still applies here, never reported as applied.
|
|
895
|
+
const repoShadowNotes = (before, after) => repoShadowedUserKeys(cwd, before, after, { globalDir }).map(({ path, value }) => {
|
|
896
|
+
const shown = value === undefined ? "absent" : typeof value === "string" ? value : JSON.stringify(value);
|
|
897
|
+
return `repo-shadowed: ${path} stays ${shown} in this repository — ${repoOverlayPath(cwd)} sets it above the user overlay`;
|
|
898
|
+
});
|
|
725
899
|
const reviewOverlay = (state) => {
|
|
900
|
+
// B2: a review re-reads every layer, so no preview rendered before it is served after it
|
|
901
|
+
candidateMemo.clear();
|
|
726
902
|
const staged = structuredClone(initial);
|
|
727
903
|
// OBS-994/FL-1, OBS-1099 add.1: every schema-enumerated deny scope rides the same review/write funnel
|
|
728
904
|
for (const scope of DENY_SCOPES)
|
|
729
905
|
staged[scope.key] = state[scope.key];
|
|
730
906
|
staged.allowOut = state.allowOut ?? initial.allowOut;
|
|
731
907
|
staged.map = state.map;
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
if (
|
|
735
|
-
staged.efforts = efforts;
|
|
908
|
+
const metadata = stagedMetadataOf(state);
|
|
909
|
+
staged.tiers = metadata.tiers;
|
|
910
|
+
if (metadata.efforts)
|
|
911
|
+
staged.efforts = metadata.efforts;
|
|
736
912
|
else
|
|
737
913
|
delete staged.efforts;
|
|
738
|
-
|
|
739
|
-
for (const classification of state.classifications) {
|
|
740
|
-
staged.tiers[classification.adapter] ??= {};
|
|
741
|
-
staged.tiers[classification.adapter][classification.model] = {
|
|
742
|
-
tier: classification.tier,
|
|
743
|
-
provenance: `${classification.note} — fleet ${today}`,
|
|
744
|
-
};
|
|
745
|
-
}
|
|
914
|
+
reviewedStage = metadata;
|
|
746
915
|
// This callback is the sole candidate-overlay builder. The Ink component renders
|
|
747
916
|
// the diff and asks for confirmation, but owns neither filesystem access nor a writer.
|
|
748
|
-
|
|
749
|
-
const
|
|
917
|
+
// B2: defaults/user/repo are re-read here, at review — the destination is the user overlay.
|
|
918
|
+
const before = currentUserText();
|
|
919
|
+
const repo = currentRepoOverlayText(cwd);
|
|
920
|
+
const path = userPath;
|
|
750
921
|
const modeChanged = state.selectedMode !== rm.mode.mode;
|
|
751
922
|
const steeringChanged = ["review", "consult"].some((key) => JSON.stringify(state.steering[key]) !== JSON.stringify(initialSteering[key]));
|
|
752
923
|
const judgeSeat = state.judgeSeat;
|
|
@@ -756,35 +927,32 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
756
927
|
pendingWrite = null;
|
|
757
928
|
return { kind: "empty" };
|
|
758
929
|
}
|
|
759
|
-
const lower =
|
|
930
|
+
const lower = lowerLayers();
|
|
760
931
|
if (lower.ok)
|
|
761
932
|
lowerOverrides = lower.overrides;
|
|
762
|
-
const write = {
|
|
763
|
-
initial,
|
|
764
|
-
edited: staged,
|
|
765
|
-
universe,
|
|
933
|
+
const write = userLayerWrite(staged, before, {
|
|
766
934
|
lowerOverrides,
|
|
767
935
|
...(modeChanged ? { mode: state.selectedMode } : {}),
|
|
768
936
|
...(judgeChanged && judgeSeat ? { judge: judgeSeat } : {}),
|
|
769
937
|
steering: { initial: initialSteering, edited: state.steering },
|
|
770
|
-
};
|
|
938
|
+
});
|
|
771
939
|
const after = renderFleetOverlayWrite(before, write);
|
|
772
940
|
if (before === after) {
|
|
773
941
|
pendingWrite = null;
|
|
774
942
|
return { kind: "empty" };
|
|
775
943
|
}
|
|
776
|
-
pendingWrite = write;
|
|
944
|
+
pendingWrite = { write, repo, user: before };
|
|
777
945
|
// OBS-1144: the review names every dead pool entry the written config carries, with its reason and
|
|
778
946
|
// what still routes — before y. Unloadable bytes carry no notes: the reload guard refuses them anyway.
|
|
779
947
|
let poolNotes = [];
|
|
780
948
|
try {
|
|
781
|
-
const written = loadConfigWithMode(cwd, { globalDir,
|
|
949
|
+
const written = loadConfigWithMode(cwd, { globalDir, userOverlayText: after }).cfg;
|
|
782
950
|
poolNotes = deadPoolEntries(written, undefined, health).map(deadPoolEntryLine);
|
|
783
951
|
}
|
|
784
952
|
catch {
|
|
785
953
|
poolNotes = [];
|
|
786
954
|
}
|
|
787
|
-
const notes = [...(lower.ok ? [] : [`${lower.error} — y will be refused`]), ...poolNotes];
|
|
955
|
+
const notes = [...repoShadowNotes(before, after), ...(lower.ok ? [] : [`${lower.error} — y will be refused`]), ...poolNotes];
|
|
788
956
|
return {
|
|
789
957
|
kind: "diff",
|
|
790
958
|
before,
|
|
@@ -794,8 +962,17 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
794
962
|
...(notes.length ? { notes } : {}),
|
|
795
963
|
};
|
|
796
964
|
};
|
|
965
|
+
// B2: every repository inherits the user overlay, so its bytes must load here AND on their own —
|
|
966
|
+
// a tiers entry whose vendor/channel only this repository declares would break every other one
|
|
797
967
|
const loaderGuard = io.reloadGuard
|
|
798
|
-
?? ((bytes) =>
|
|
968
|
+
?? ((bytes) => {
|
|
969
|
+
const here = overlayBytesLoadError(cwd, bytes, { globalDir, layer: "user" });
|
|
970
|
+
if (here !== null)
|
|
971
|
+
return here;
|
|
972
|
+
const alone = overlayBytesLoadError(cwd, bytes, { globalDir, layer: "user", repoOverlayText: "" });
|
|
973
|
+
return alone === null ? null
|
|
974
|
+
: `the user overlay ${userPath} loads only beside ${repoOverlayPath(cwd)} — every repository inherits it, and without that overlay it fails: ${alone}`;
|
|
975
|
+
});
|
|
799
976
|
// OBS-1188: a stale preview or an unreadable lower layer answers on the same error channel as the loader
|
|
800
977
|
const reloadGuard = (bytes) => (pendingWrite && stalePreview(pendingWrite, bytes)) || loaderGuard(bytes);
|
|
801
978
|
const props = {
|
|
@@ -809,7 +986,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
809
986
|
initialMode: rm.mode.mode,
|
|
810
987
|
modeOptions: ROUTING_MODES.map((mode) => ({ id: mode, gloss: MODE_GLOSS[mode] })),
|
|
811
988
|
initialMap: editable.map,
|
|
812
|
-
modePreview: (mode, map, deny) => [modeSpend(mode, map, deny), ...floorPreview(mode)],
|
|
989
|
+
modePreview: (mode, map, deny, stage) => [modeSpend(mode, map, deny, stage), ...floorPreview(mode, map, deny, stage)],
|
|
813
990
|
shapeRows: routedShapeRows,
|
|
814
991
|
candidatesForShape,
|
|
815
992
|
preferOptionsForShape: (_shape, current) => [
|
|
@@ -842,15 +1019,19 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
842
1019
|
return "fleet: discarded overlay changes";
|
|
843
1020
|
// The command remains the single config actuator. Every interactive edit reaches this
|
|
844
1021
|
// one write only after the component-rendered diff confirm and the production reload guard.
|
|
845
|
-
const
|
|
846
|
-
if (!
|
|
1022
|
+
const pending = pendingWrite;
|
|
1023
|
+
if (!pending)
|
|
847
1024
|
throw new Error("fleet write reached confirmation without a staged overlay mutation");
|
|
848
|
-
|
|
1025
|
+
const write = pending.write;
|
|
1026
|
+
// OBS-1188: re-checked against the bytes on disk at write time; a refusal throws before the temp file exists.
|
|
1027
|
+
// B2: the one destination is the user overlay, and bytes the loader refuses never land there either.
|
|
849
1028
|
try {
|
|
850
|
-
writeFleetOverlay(
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
|
|
1029
|
+
writeFleetOverlay(userPath, (prior) => {
|
|
1030
|
+
const refusal = result.review.path === userPath
|
|
1031
|
+
? stalePreview(pending, result.review.after, prior) ?? loaderGuard(result.review.after)
|
|
1032
|
+
: `the reviewed destination ${result.review.path} is not the user overlay ${userPath}`;
|
|
1033
|
+
if (refusal !== null)
|
|
1034
|
+
throw new ConfigError(refusal);
|
|
854
1035
|
return result.review.after;
|
|
855
1036
|
});
|
|
856
1037
|
}
|
|
@@ -873,7 +1054,8 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
873
1054
|
unprobed.push(`${adapter}:${model}`);
|
|
874
1055
|
}
|
|
875
1056
|
}
|
|
876
|
-
|
|
1057
|
+
// B2: a repo-shadowed save says what still applies here — never claimed applied
|
|
1058
|
+
const head = [`fleet: wrote ${userPath}`, ...repoShadowNotes(result.review.before, result.review.after).map((note) => `fleet: ${note}`)].join("\n");
|
|
877
1059
|
if (!unprobed.length)
|
|
878
1060
|
return head;
|
|
879
1061
|
const named = unprobed.slice(0, 3).join(", ") + (unprobed.length > 3 ? `, +${unprobed.length - 3} more` : "");
|
|
@@ -883,5 +1065,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
883
1065
|
props,
|
|
884
1066
|
commit,
|
|
885
1067
|
renderWhy: () => renderFleetWhy(projectedShapeRows(rm.mode.mode, editable.map, stagedDenyOf(editable))),
|
|
1068
|
+
// T8: the memoized candidate every preview callback ranks — read-only, so its identity is checkable
|
|
1069
|
+
previewConfig: previewCfg,
|
|
886
1070
|
};
|
|
887
1071
|
}
|