tickmarkr 2.6.2 → 2.6.4
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 +12 -2
- package/dist/adapters/codex.d.ts +2 -1
- package/dist/adapters/codex.js +26 -5
- package/dist/adapters/model-lints.d.ts +1 -0
- package/dist/adapters/model-lints.js +48 -1
- package/dist/adapters/model-windows.d.ts +2 -1
- package/dist/adapters/model-windows.js +18 -7
- package/dist/adapters/types.d.ts +6 -0
- package/dist/cli/commands/approve.d.ts +6 -3
- package/dist/cli/commands/approve.js +43 -6
- package/dist/cli/commands/doctor.d.ts +8 -2
- package/dist/cli/commands/doctor.js +6 -1
- package/dist/cli/commands/fleet.js +264 -59
- package/dist/cli/commands/init.js +5 -3
- package/dist/cli/commands/plan.js +13 -8
- package/dist/cli/commands/report.d.ts +40 -2
- package/dist/cli/commands/report.js +278 -11
- 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 +49 -4
- package/dist/cli/commands/verify.js +332 -121
- package/dist/compile/native.js +137 -0
- package/dist/config/config.d.ts +40 -9
- package/dist/config/config.js +133 -15
- package/dist/config/fleet-overlay.d.ts +13 -3
- package/dist/config/fleet-overlay.js +12 -8
- package/dist/drivers/herdr.d.ts +12 -0
- package/dist/drivers/herdr.js +51 -0
- package/dist/drivers/orca.d.ts +9 -1
- package/dist/drivers/orca.js +29 -7
- package/dist/drivers/types.d.ts +2 -0
- package/dist/drivers/types.js +2 -2
- package/dist/gates/acceptance.d.ts +7 -0
- package/dist/gates/acceptance.js +27 -5
- package/dist/gates/baseline.d.ts +20 -1
- package/dist/gates/baseline.js +100 -20
- package/dist/gates/cache.d.ts +8 -0
- package/dist/gates/cache.js +12 -2
- package/dist/gates/llm.d.ts +6 -0
- package/dist/gates/llm.js +27 -8
- package/dist/gates/review.d.ts +6 -1
- package/dist/gates/review.js +122 -32
- package/dist/gates/run-gates.d.ts +54 -3
- package/dist/gates/run-gates.js +331 -45
- package/dist/gates/test-manifest.d.ts +42 -0
- package/dist/gates/test-manifest.js +69 -10
- package/dist/route/router.d.ts +23 -1
- package/dist/route/router.js +54 -16
- package/dist/run/consult.d.ts +3 -1
- package/dist/run/consult.js +4 -2
- package/dist/run/daemon.d.ts +2 -1
- package/dist/run/daemon.js +349 -79
- package/dist/run/interactive-seed.d.ts +4 -0
- package/dist/run/interactive-seed.js +35 -9
- package/dist/run/journal.d.ts +123 -1
- package/dist/run/journal.js +480 -17
- package/dist/run/lease.d.ts +13 -0
- package/dist/run/lease.js +45 -0
- package/dist/run/protocol.d.ts +15 -0
- package/dist/run/protocol.js +11 -1
- package/dist/run/receipt-resolver.d.ts +22 -0
- package/dist/run/receipt-resolver.js +40 -1
- package/dist/run/repair-selection.d.ts +11 -1
- package/dist/run/repair-selection.js +17 -9
- package/dist/run/wall-budget.d.ts +48 -0
- package/dist/run/wall-budget.js +280 -0
- package/dist/tui/cockpit/live-store.d.ts +6 -0
- package/dist/tui/cockpit/live-store.js +36 -11
- package/dist/tui/cockpit/run-cockpit.js +2 -2
- package/dist/tui/cockpit/run-view.d.ts +2 -1
- package/dist/tui/cockpit/run-view.js +13 -9
- package/dist/tui/cockpit/setup-cockpit.d.ts +2 -0
- package/dist/tui/cockpit/setup-cockpit.js +4 -0
- package/package.json +2 -1
- package/schema/config.schema.json +8 -1
- package/skills/tickmarkr-auto/SKILL.md +2 -2
- package/skills/tickmarkr-loop/SKILL.md +17 -5
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +4 -1
|
@@ -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 { 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,28 +363,122 @@ 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] ?? []]));
|
|
366
|
+
// B2: the classifications and efforts of the last reviewed state — the preview callbacks carry mode,
|
|
367
|
+
// map and deny only (fleet-app's contract), so every preview after a review ranks the same fully
|
|
368
|
+
// staged candidate that review rendered, never the assembly-time tiers.
|
|
369
|
+
// ponytail: one staged after the last review reaches previews at the next w; thread classifications
|
|
370
|
+
// and efforts through fleet-app's preview callbacks if that lag ever matters.
|
|
371
|
+
let reviewedStage = { tiers: initial.tiers, efforts: initial.efforts };
|
|
372
|
+
// OBS-1182: the layers under the written overlay — B2: the defaults alone under the USER overlay —
|
|
373
|
+
// read raw, so clearing an effort masks an inherited one. OBS-1188: re-read at every review and
|
|
374
|
+
// save, never an assembly-time snapshot; an unreadable read keeps the last good one for the
|
|
375
|
+
// preview only — the save guard below refuses it.
|
|
376
|
+
let lowerOverrides = {};
|
|
349
377
|
const stagedEditable = (map, deny) => ({
|
|
350
378
|
...structuredClone(initial),
|
|
379
|
+
...structuredClone(reviewedStage),
|
|
351
380
|
...Object.fromEntries(DENY_SCOPES.map((scope) => [scope.key, deny[stagedDenyKeyOf(scope)]])),
|
|
352
381
|
// OBS-1046: the allow complement is staged beside the deny lists, never folded into them
|
|
353
382
|
allowOut: deny.allowOut ?? initial.allowOut,
|
|
354
383
|
map,
|
|
355
384
|
});
|
|
385
|
+
// B2: the writer diffs a CHANGED routing.map slot against what its destination declares there —
|
|
386
|
+
// the user layer (defaults + user, no repo) — never the effective slot a repo pin/pool/prefer
|
|
387
|
+
// shadows, which would leave the user's own hidden declaration beside the new one (a stale pin a
|
|
388
|
+
// second repository still routes to, or pin+pool bytes that refuse a valid edit).
|
|
389
|
+
// B2: a touched membership is the same — the writer rewrites the allow form from the whole staged
|
|
390
|
+
// fleet, so its flat deny lists and allow complement diff against the user layer's own; a user
|
|
391
|
+
// deny the repo's list hides is admitted or kept by the staged fleet, never left to re-exclude
|
|
392
|
+
// it in every other repository.
|
|
393
|
+
const membershipKeys = [...DENY_SCOPES.filter((scope) => scope.path.length === 3).map((scope) => scope.key), "allowOut"];
|
|
394
|
+
const sameSet = (a = [], b = []) => [...new Set(a)].sort().join() === [...new Set(b)].sort().join();
|
|
395
|
+
const destinationInitial = (base, edited, userText, target) => {
|
|
396
|
+
let userCfg;
|
|
397
|
+
try {
|
|
398
|
+
userCfg = loadConfigWithMode(cwd, { globalDir, userOverlayText: userText, repoOverlayText: "" }).cfg;
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
return base; // ponytail: user bytes that load only beside this repo — the reload guard refuses the write
|
|
402
|
+
}
|
|
403
|
+
const userLayer = userCfg.routing.map;
|
|
404
|
+
const editedMap = edited.map;
|
|
405
|
+
const map = { ...base.map };
|
|
406
|
+
for (const shape of new Set([...Object.keys(base.map), ...Object.keys(editedMap)])) {
|
|
407
|
+
if (JSON.stringify(base.map[shape]) === JSON.stringify(editedMap[shape]))
|
|
408
|
+
continue;
|
|
409
|
+
const { pin, pool, prefer } = userLayer[shape] ?? {};
|
|
410
|
+
// escalate is no slot key: it merges per key, so the effective value stays the baseline
|
|
411
|
+
const escalate = base.map[shape]?.escalate;
|
|
412
|
+
map[shape] = {
|
|
413
|
+
...(pin ? { pin } : {}), ...(pool ? { pool } : {}), ...(prefer ? { prefer } : {}),
|
|
414
|
+
...(escalate !== undefined ? { escalate } : {}),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
const membershipTouched = membershipKeys.some((key) => !sameSet(base[key], edited[key]));
|
|
418
|
+
const destination = membershipTouched ? fleetEditableFromConfig(userCfg, target) : undefined;
|
|
419
|
+
return {
|
|
420
|
+
...base,
|
|
421
|
+
map,
|
|
422
|
+
...(destination ? Object.fromEntries(membershipKeys.map((key) => [key, destination[key] ?? []])) : {}),
|
|
423
|
+
};
|
|
424
|
+
};
|
|
425
|
+
// B2: a changed slot staged with no declaration (auto, no prefer) where the defaults under the user
|
|
426
|
+
// overlay declare one: deleting the user's pin/pool would restore that default, so the slot is
|
|
427
|
+
// masked with prefer: [] — a declaration that replaces the lower slot atomically and ranks nothing
|
|
428
|
+
const maskClearedSlots = (base, staged) => {
|
|
429
|
+
const map = { ...staged.map };
|
|
430
|
+
for (const shape of new Set([...Object.keys(base.map), ...Object.keys(staged.map)])) {
|
|
431
|
+
const entry = staged.map[shape];
|
|
432
|
+
if (JSON.stringify(base.map[shape]) === JSON.stringify(entry))
|
|
433
|
+
continue;
|
|
434
|
+
if (Object.keys(mapSlot(entry)).length || !Object.keys(mapSlot(DEFAULT_CONFIG.routing.map[shape])).length)
|
|
435
|
+
continue;
|
|
436
|
+
map[shape] = { ...entry, prefer: [] };
|
|
437
|
+
}
|
|
438
|
+
return { ...staged, map };
|
|
439
|
+
};
|
|
440
|
+
// B2: the one user-layer write builder the previews and the review share, so a preview ranks the
|
|
441
|
+
// bytes y would land. A touched membership serializes against the universe the candidate itself
|
|
442
|
+
// serves (current layers plus staged classifications), rendered first with membership unchanged;
|
|
443
|
+
// a channel only that universe adds keeps the verdict the current layers' allow form gives it.
|
|
444
|
+
const userLayerWrite = (staged, userText, rest) => {
|
|
445
|
+
let edited = maskClearedSlots(initial, staged);
|
|
446
|
+
let target = universe;
|
|
447
|
+
if (membershipKeys.some((key) => !sameSet(initial[key], edited[key]))) {
|
|
448
|
+
const unchanged = { ...edited, ...Object.fromEntries(membershipKeys.map((key) => [key, initial[key]])) };
|
|
449
|
+
try {
|
|
450
|
+
const bytes = renderFleetOverlayWrite(userText, {
|
|
451
|
+
...rest, initial: destinationInitial(initial, unchanged, userText, universe), edited: unchanged, universe,
|
|
452
|
+
});
|
|
453
|
+
const candidate = loadConfigWithMode(cwd, { globalDir, userOverlayText: bytes }).cfg;
|
|
454
|
+
target = universeOf(candidateChannels(candidate));
|
|
455
|
+
const known = (entry) => universe.some((row) => entry === row.adapter || row.models.some((model) => universeEntryMatches(row, model, entry)));
|
|
456
|
+
const grown = (fleetEditableFromConfig(candidate, target).allowOut ?? []).filter((entry) => !known(entry));
|
|
457
|
+
if (grown.length)
|
|
458
|
+
edited = { ...edited, allowOut: [...new Set([...(edited.allowOut ?? []), ...grown])].sort() };
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
// the full render below (or the reload guard) names the refusal
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return { ...rest, initial: destinationInitial(initial, edited, userText, target), edited, universe: target };
|
|
465
|
+
};
|
|
356
466
|
const candidateMemo = new Map();
|
|
357
467
|
const previewCfg = (mode, map, deny) => {
|
|
358
|
-
|
|
468
|
+
// B2: the user and repo overlay bytes are part of the key — a layer edited mid-session (a repo
|
|
469
|
+
// deny added before a review) re-renders the preview instead of serving the pre-edit one
|
|
470
|
+
const userText = currentUserText();
|
|
471
|
+
const key = JSON.stringify([mode, map, deny, userText, currentRepoOverlayText(cwd)]);
|
|
359
472
|
const hit = candidateMemo.get(key);
|
|
360
473
|
if (hit)
|
|
361
474
|
return hit;
|
|
362
475
|
let preview;
|
|
363
476
|
try {
|
|
364
|
-
const bytes = renderFleetOverlayWrite(
|
|
365
|
-
|
|
366
|
-
edited: stagedEditable(map, deny),
|
|
367
|
-
universe,
|
|
477
|
+
const bytes = renderFleetOverlayWrite(userText, userLayerWrite(stagedEditable(map, deny), userText, {
|
|
478
|
+
lowerOverrides,
|
|
368
479
|
...(mode !== rm.mode.mode ? { mode } : {}),
|
|
369
|
-
});
|
|
370
|
-
preview = { ok: true,
|
|
480
|
+
}));
|
|
481
|
+
preview = { ok: true, ...loadConfigWithMode(cwd, { globalDir, userOverlayText: bytes }) };
|
|
371
482
|
}
|
|
372
483
|
catch (error) {
|
|
373
484
|
preview = { ok: false, error: error.message.replace(/\s+/g, " ").trim() };
|
|
@@ -382,9 +493,22 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
382
493
|
const preview = previewCfg(rm.mode.mode, editable.map, deny);
|
|
383
494
|
return preview.ok ? { ok: true, routing: preview.cfg.routing } : preview;
|
|
384
495
|
};
|
|
496
|
+
// B2: the candidate config's own discovery, blind to both membership scopes — classifications and
|
|
497
|
+
// channel metadata (tier, channel) a reloaded layer changed reach routing, ranking and the ledger,
|
|
498
|
+
// never the assembly-time channels. Keyed on the memoized candidate config object.
|
|
499
|
+
const candidateChannelMemo = new WeakMap();
|
|
500
|
+
const candidateChannels = (cfgPreview) => {
|
|
501
|
+
const hit = candidateChannelMemo.get(cfgPreview);
|
|
502
|
+
if (hit)
|
|
503
|
+
return hit;
|
|
504
|
+
const { deny: _deny, allow: _allow, ...blind } = cfgPreview.routing;
|
|
505
|
+
const discovered = discoverChannels({ ...cfgPreview, routing: blind }, adapters, health);
|
|
506
|
+
candidateChannelMemo.set(cfgPreview, discovered);
|
|
507
|
+
return discovered;
|
|
508
|
+
};
|
|
385
509
|
// route() never deny-filters its pool — that contract lives in discoverChannels — so every
|
|
386
510
|
// preview call rebuilds the pool the staged deny would discover
|
|
387
|
-
const previewPool = (cfgPreview) =>
|
|
511
|
+
const previewPool = (cfgPreview) => candidateChannels(cfgPreview).filter((c) => disallowedBy(c, cfgPreview.routing) === null);
|
|
388
512
|
const modeSpend = (mode, map, deny) => {
|
|
389
513
|
const tierCount = {};
|
|
390
514
|
let subs = 0;
|
|
@@ -403,7 +527,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
403
527
|
subs += 1;
|
|
404
528
|
else {
|
|
405
529
|
apiN += 1;
|
|
406
|
-
apiUsd +=
|
|
530
|
+
apiUsd += cfgPreview.pricing[assignment.tier] ?? 0;
|
|
407
531
|
}
|
|
408
532
|
}
|
|
409
533
|
catch {
|
|
@@ -421,11 +545,16 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
421
545
|
parts.push(`${unroutable} unroutable`);
|
|
422
546
|
return ` mix: ${mix} — ${parts.join(" · ")}`;
|
|
423
547
|
};
|
|
424
|
-
|
|
548
|
+
// B2: the floors the candidate user-layer bytes resolve to — the same config the routed mix ranks
|
|
549
|
+
// against, so a repo-declared mode that shadows the user's pick previews no floor change
|
|
550
|
+
const floorPreview = (mode, map, deny) => {
|
|
425
551
|
if (mode === rm.mode.mode)
|
|
426
552
|
return [];
|
|
553
|
+
const preview = previewCfg(mode, map, deny);
|
|
554
|
+
if (!preview.ok)
|
|
555
|
+
return []; // modeSpend names the refusal
|
|
427
556
|
const current = cfg.routing.floors;
|
|
428
|
-
const next =
|
|
557
|
+
const next = preview.cfg.routing.floors;
|
|
429
558
|
const changed = SHAPES.filter((shape) => current[shape] !== next[shape]);
|
|
430
559
|
return [
|
|
431
560
|
` floors vs ${rm.mode.mode}:`,
|
|
@@ -434,19 +563,23 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
434
563
|
: [" (no floor changes)"]),
|
|
435
564
|
];
|
|
436
565
|
};
|
|
437
|
-
|
|
438
|
-
|
|
566
|
+
// B2: `resolution` is the candidate user-layer bytes' own mode resolution — a repo-declared mode that
|
|
567
|
+
// shadows the user's pick keeps its provenance, never an operator pin the loader would not apply
|
|
568
|
+
// B2: `entry` is the slot the candidate's loader applies — a staged slot a repo declaration shadows
|
|
569
|
+
// is no operator pin here; the repo's winning declaration keeps its provenance
|
|
570
|
+
const whyDeclaration = (shape, resolution, entry, mapProducedValue) => {
|
|
571
|
+
const mapChanged = JSON.stringify(mapSlot(entry)) !== JSON.stringify(mapSlot(editable.map[shape]));
|
|
439
572
|
if (mapProducedValue) {
|
|
440
573
|
return mapChanged
|
|
441
574
|
? { operatorPinned: true }
|
|
442
575
|
: { declaredAt: `routing.map.${shape}` };
|
|
443
576
|
}
|
|
444
|
-
const floorSource =
|
|
577
|
+
const floorSource = resolution.provenance[shape];
|
|
445
578
|
if (floorSource === undefined)
|
|
446
579
|
return {};
|
|
447
580
|
if (floorSource === "config floors")
|
|
448
581
|
return { declaredAt: `routing.floors.${shape}` };
|
|
449
|
-
if (mode !== rm.mode.mode)
|
|
582
|
+
if (resolution.mode !== rm.mode.mode)
|
|
450
583
|
return { operatorPinned: true };
|
|
451
584
|
return rm.source === "default"
|
|
452
585
|
? { declaredAt: `routing.floors.${shape}` }
|
|
@@ -458,35 +591,42 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
458
591
|
return projectFleetWhy(SHAPES.map((shape) => ({
|
|
459
592
|
id: shape,
|
|
460
593
|
effective: previewUnavailable(preview.error),
|
|
461
|
-
...whyDeclaration(shape, mode, map, false),
|
|
594
|
+
...whyDeclaration(shape, modeCfgs[mode].mode, map[shape], false),
|
|
462
595
|
})), { repoRoot: cwd, globalDir });
|
|
463
596
|
}
|
|
464
597
|
const cfgPreview = preview.cfg;
|
|
465
598
|
const pool = previewPool(cfgPreview);
|
|
466
599
|
const values = SHAPES.map((shape) => {
|
|
600
|
+
// B2: annotations and provenance read the slot the loader applies; a staged slot it does not
|
|
601
|
+
// apply (a repo pin/pool/prefer above the user overlay) is named shadowed, never shown as routing
|
|
602
|
+
const entry = cfgPreview.routing.map[shape];
|
|
603
|
+
const staged = mapSlot(map[shape]);
|
|
604
|
+
const shadowed = JSON.stringify(staged) !== JSON.stringify(mapSlot(editable.map[shape]))
|
|
605
|
+
&& JSON.stringify(staged) !== JSON.stringify(mapSlot(entry));
|
|
606
|
+
const shadowNote = shadowed ? ` · staged ${slotLabel(staged)} shadowed — not applied in this repository` : "";
|
|
467
607
|
try {
|
|
468
608
|
const routed = route(previewTask(shape), cfgPreview, pool, profile, undefined, undefined, PREVIEW_EXPLORE);
|
|
469
609
|
const assignment = routed.assignment;
|
|
470
610
|
// OBS-531: a pooled shape used to render only the routed WINNER — indistinguishable from
|
|
471
611
|
// a pin. The declaration is the operator's decision; the winner is today's dice.
|
|
472
|
-
const declaredPool =
|
|
612
|
+
const declaredPool = entry?.pool;
|
|
473
613
|
const poolPrefix = declaredPool ? `pool(${declaredPool.mode}·${declaredPool.channels.length}) → ` : "";
|
|
474
|
-
const effective = `${poolPrefix}${assignment.adapter}:${assignment.model} (${assignment.channel}, ${assignment.tier}) ${costSignal(assignment,
|
|
475
|
-
const mapProducedValue =
|
|
614
|
+
const effective = `${poolPrefix}${assignment.adapter}:${assignment.model} (${assignment.channel}, ${assignment.tier}) ${costSignal(assignment, cfgPreview.pricing)}${shadowNote}`;
|
|
615
|
+
const mapProducedValue = entry?.pin !== undefined || entry?.pool !== undefined
|
|
476
616
|
|| routed.provenance.includes("via prefer");
|
|
477
617
|
return {
|
|
478
618
|
id: shape,
|
|
479
619
|
effective,
|
|
480
|
-
...whyDeclaration(shape, mode,
|
|
620
|
+
...whyDeclaration(shape, preview.mode, entry, mapProducedValue),
|
|
481
621
|
};
|
|
482
622
|
}
|
|
483
623
|
catch (error) {
|
|
484
|
-
const mapProducedValue =
|
|
485
|
-
|| (
|
|
624
|
+
const mapProducedValue = entry?.pin !== undefined || entry?.pool !== undefined
|
|
625
|
+
|| (entry?.prefer?.length ?? 0) > 0;
|
|
486
626
|
return {
|
|
487
627
|
id: shape,
|
|
488
|
-
effective: error.message
|
|
489
|
-
...whyDeclaration(shape, mode,
|
|
628
|
+
effective: `${error.message}${shadowNote}`,
|
|
629
|
+
...whyDeclaration(shape, preview.mode, entry, mapProducedValue),
|
|
490
630
|
setupCommand: "tickmarkr fleet",
|
|
491
631
|
};
|
|
492
632
|
}
|
|
@@ -506,7 +646,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
506
646
|
const excludedNoteFor = (cfgPreview) => {
|
|
507
647
|
let stagedOut = 0;
|
|
508
648
|
let denied = 0;
|
|
509
|
-
for (const channel of
|
|
649
|
+
for (const channel of candidateChannels(cfgPreview)) {
|
|
510
650
|
if (disallowedBy(channel, cfgPreview.routing) === null)
|
|
511
651
|
continue;
|
|
512
652
|
if (disallowedBy(channel, cfg.routing) === null)
|
|
@@ -532,9 +672,11 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
532
672
|
// silent omission is indistinguishable from a bug (OBS-530's lesson, extended past deny/allow).
|
|
533
673
|
// allow and the unauthed probe already carry a reason through excludedNoteFor above; the rest
|
|
534
674
|
// are shape-scoped (a pin/pool/floor/prefer excludes only for THIS shape) and belong here.
|
|
535
|
-
|
|
675
|
+
// B2: captions read the slot the candidate's loader applies — a staged slot a repo declaration
|
|
676
|
+
// shadows excludes nothing here, so it is never named as the reason
|
|
677
|
+
const shapeExclusionCaptions = (shape, cfgPreview) => {
|
|
536
678
|
const pool = previewPool(cfgPreview);
|
|
537
|
-
const entry = map[shape];
|
|
679
|
+
const entry = cfgPreview.routing.map[shape];
|
|
538
680
|
const captions = [];
|
|
539
681
|
// 2. pin or pool — a closed candidate set declared for this shape; a channel outside it is
|
|
540
682
|
// not manageable from the models view, it is a Shapes-view declaration.
|
|
@@ -592,10 +734,11 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
592
734
|
// all of them, never a first match), the unauthed probe, pin/pool/floor, the task hint and a
|
|
593
735
|
// deny∩prefer collision it is the target of — or that leaves the shape with no candidate at all.
|
|
594
736
|
const channelLedger = (shape, map, cfgPreview, offered) => {
|
|
595
|
-
|
|
737
|
+
// B2: reasons name the applied slot; the carried chain below stays the staged one Space edits
|
|
738
|
+
const entry = cfgPreview.routing.map[shape];
|
|
596
739
|
const floor = cfgPreview.routing.floors[shape];
|
|
597
740
|
const collisions = denyPreferCollisions(cfgPreview, [shape], health);
|
|
598
|
-
const discovered = new Set(
|
|
741
|
+
const discovered = new Set(candidateChannels(cfgPreview).map((c) => `${c.adapter}:${c.model}`));
|
|
599
742
|
// T2 review (D-225): discovery already dropped a channel whose model probe failed — it is
|
|
600
743
|
// rebuilt as a structured channel from its adapter's own declaration and walks the SAME
|
|
601
744
|
// reach + collector path as every other row, the auth reason appended LAST; never a bare string
|
|
@@ -604,17 +747,17 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
604
747
|
return identity ? { ...declared, identity } : declared;
|
|
605
748
|
};
|
|
606
749
|
const failedProbes = unauthedModels.filter(({ key }) => !discovered.has(key)).flatMap(({ key, adapter, reason }) => {
|
|
607
|
-
const declared = adapters.find((a) => a.id === adapter)?.channels(
|
|
750
|
+
const declared = adapters.find((a) => a.id === adapter)?.channels(cfgPreview).find((c) => `${c.adapter}:${c.model}` === key);
|
|
608
751
|
return declared ? [{ ...withIdentity(adapter, declared), authReason: `unauthed (${reason}) — re-probe with tickmarkr doctor` }] : [];
|
|
609
752
|
});
|
|
610
753
|
// T2 review round 6: an installed-but-unauthed CLI is dropped by discovery AND by the model
|
|
611
754
|
// probe list, so its configured channels are rebuilt from the adapter's own declaration too —
|
|
612
755
|
// each greys with its reach and covering entry, the CLI auth reason last; never one bare adapter line
|
|
613
|
-
const unauthedCliChannels = unauthedClis.flatMap((adapter) => (adapters.find((a) => a.id === adapter)?.channels(
|
|
756
|
+
const unauthedCliChannels = unauthedClis.flatMap((adapter) => (adapters.find((a) => a.id === adapter)?.channels(cfgPreview) ?? [])
|
|
614
757
|
.map((declared) => ({ ...withIdentity(adapter, declared), authReason: `${adapter} CLI unauthed — re-probe with tickmarkr doctor` })));
|
|
615
758
|
// D-225: the ledger is the EXCLUDED half of a closed partition — a channel the picker offers
|
|
616
759
|
// never greys beside itself; its pin/pool/floor/prefer captions stay in excludedNote
|
|
617
|
-
const ledgerChannels = [...
|
|
760
|
+
const ledgerChannels = [...candidateChannels(cfgPreview).map((c) => ({ ...c, authReason: undefined })), ...failedProbes, ...unauthedCliChannels]
|
|
618
761
|
.filter((c) => !offered.has(`${c.adapter}:${c.model}`));
|
|
619
762
|
const lines = ledgerChannels.flatMap((c) => {
|
|
620
763
|
const key = `${c.adapter}:${c.model}`;
|
|
@@ -650,7 +793,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
650
793
|
// OBS-1145 add.1: every carried pool member the picker does not offer leads the ledger in chain
|
|
651
794
|
// order — one no reason above names still gets its own row — so the picker renders its ordinal
|
|
652
795
|
// and Space can drop it; a chain member with no row is hidden AND irremovable
|
|
653
|
-
const carried =
|
|
796
|
+
const carried = map[shape]?.pool?.channels.filter((key) => !offered.has(key)) ?? [];
|
|
654
797
|
const named = new Set(lines.map((row) => row.key));
|
|
655
798
|
const unnamed = carried.filter((key) => !named.has(key)).map((key) => ({
|
|
656
799
|
key,
|
|
@@ -666,13 +809,13 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
666
809
|
const cfgPreview = preview.cfg;
|
|
667
810
|
const rows = shapeCandidates(previewTask(shape), cfgPreview, previewPool(cfgPreview), profile).map((candidate) => ({
|
|
668
811
|
id: `${candidate.assignment.adapter}:${candidate.assignment.model}`,
|
|
669
|
-
label: candidateRow(candidate,
|
|
812
|
+
label: candidateRow(candidate, cfgPreview.pricing),
|
|
670
813
|
pin: { via: candidate.assignment.adapter, model: candidate.assignment.model },
|
|
671
814
|
// OBS-1065: a picker lift reads eligibility from here, not the ledger strings
|
|
672
815
|
belowFloor: candidate.belowFloor,
|
|
673
816
|
}));
|
|
674
817
|
const offered = new Set(rows.map((row) => row.id));
|
|
675
|
-
const notes = [excludedNoteFor(cfgPreview), ...shapeExclusionCaptions(shape,
|
|
818
|
+
const notes = [excludedNoteFor(cfgPreview), ...shapeExclusionCaptions(shape, cfgPreview)].filter((note) => note !== undefined);
|
|
676
819
|
return { rows, excludedNote: notes.length ? notes.join("\n") : undefined, ledger: channelLedger(shape, map, cfgPreview, offered) };
|
|
677
820
|
};
|
|
678
821
|
const preferUniverse = [
|
|
@@ -700,10 +843,39 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
700
843
|
// discovered seats universe plus a keep-default row, and a write is staged only on a real change.
|
|
701
844
|
const initialJudge = `${cfg.judge.adapter}:${cfg.judge.model}`;
|
|
702
845
|
let pendingWrite = null;
|
|
703
|
-
|
|
704
|
-
//
|
|
705
|
-
|
|
846
|
+
const lowerLayers = () => lowerLayerModelOverrides({ globalDir, below: "user" });
|
|
847
|
+
// OBS-1188: the reviewed bytes are the only bytes that may land. Any byte of the user or repo overlay
|
|
848
|
+
// that moved since the review is a stale preview — a re-render alone would overwrite a same-key edit
|
|
849
|
+
// with the staged value and still match — then the lower layers are re-rendered as they are NOW.
|
|
850
|
+
// Null = still current.
|
|
851
|
+
const stalePreview = (pending, reviewed, prior = currentUserText()) => {
|
|
852
|
+
if (currentRepoOverlayText(cwd) !== pending.repo) {
|
|
853
|
+
return `stale preview — the repository overlay ${repoOverlayPath(cwd)} changed after the review diff was rendered; press w to review what would be written now`;
|
|
854
|
+
}
|
|
855
|
+
if (prior !== pending.user) {
|
|
856
|
+
return `stale preview — the user overlay ${userPath} changed after the review diff was rendered; press w to review what would be written now`;
|
|
857
|
+
}
|
|
858
|
+
const lower = lowerLayers();
|
|
859
|
+
if (!lower.ok)
|
|
860
|
+
return lower.error;
|
|
861
|
+
try {
|
|
862
|
+
if (renderFleetOverlayWrite(prior, { ...pending.write, lowerOverrides: lower.overrides }) === reviewed)
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
catch (error) {
|
|
866
|
+
return error.message;
|
|
867
|
+
}
|
|
868
|
+
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`;
|
|
869
|
+
};
|
|
870
|
+
// B2: a saved user key the repository overlay also decides does not apply in THIS repository —
|
|
871
|
+
// each is named with the value that still applies here, never reported as applied.
|
|
872
|
+
const repoShadowNotes = (before, after) => repoShadowedUserKeys(cwd, before, after, { globalDir }).map(({ path, value }) => {
|
|
873
|
+
const shown = value === undefined ? "absent" : typeof value === "string" ? value : JSON.stringify(value);
|
|
874
|
+
return `repo-shadowed: ${path} stays ${shown} in this repository — ${repoOverlayPath(cwd)} sets it above the user overlay`;
|
|
875
|
+
});
|
|
706
876
|
const reviewOverlay = (state) => {
|
|
877
|
+
// B2: a review re-reads every layer, so no preview rendered before it is served after it
|
|
878
|
+
candidateMemo.clear();
|
|
707
879
|
const staged = structuredClone(initial);
|
|
708
880
|
// OBS-994/FL-1, OBS-1099 add.1: every schema-enumerated deny scope rides the same review/write funnel
|
|
709
881
|
for (const scope of DENY_SCOPES)
|
|
@@ -724,10 +896,13 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
724
896
|
provenance: `${classification.note} — fleet ${today}`,
|
|
725
897
|
};
|
|
726
898
|
}
|
|
899
|
+
reviewedStage = { tiers: staged.tiers, efforts: staged.efforts };
|
|
727
900
|
// This callback is the sole candidate-overlay builder. The Ink component renders
|
|
728
901
|
// the diff and asks for confirmation, but owns neither filesystem access nor a writer.
|
|
729
|
-
|
|
730
|
-
const
|
|
902
|
+
// B2: defaults/user/repo are re-read here, at review — the destination is the user overlay.
|
|
903
|
+
const before = currentUserText();
|
|
904
|
+
const repo = currentRepoOverlayText(cwd);
|
|
905
|
+
const path = userPath;
|
|
731
906
|
const modeChanged = state.selectedMode !== rm.mode.mode;
|
|
732
907
|
const steeringChanged = ["review", "consult"].some((key) => JSON.stringify(state.steering[key]) !== JSON.stringify(initialSteering[key]));
|
|
733
908
|
const judgeSeat = state.judgeSeat;
|
|
@@ -737,42 +912,54 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
737
912
|
pendingWrite = null;
|
|
738
913
|
return { kind: "empty" };
|
|
739
914
|
}
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
915
|
+
const lower = lowerLayers();
|
|
916
|
+
if (lower.ok)
|
|
917
|
+
lowerOverrides = lower.overrides;
|
|
918
|
+
const write = userLayerWrite(staged, before, {
|
|
744
919
|
lowerOverrides,
|
|
745
920
|
...(modeChanged ? { mode: state.selectedMode } : {}),
|
|
746
921
|
...(judgeChanged && judgeSeat ? { judge: judgeSeat } : {}),
|
|
747
922
|
steering: { initial: initialSteering, edited: state.steering },
|
|
748
|
-
};
|
|
923
|
+
});
|
|
749
924
|
const after = renderFleetOverlayWrite(before, write);
|
|
750
925
|
if (before === after) {
|
|
751
926
|
pendingWrite = null;
|
|
752
927
|
return { kind: "empty" };
|
|
753
928
|
}
|
|
754
|
-
pendingWrite = write;
|
|
929
|
+
pendingWrite = { write, repo, user: before };
|
|
755
930
|
// OBS-1144: the review names every dead pool entry the written config carries, with its reason and
|
|
756
931
|
// what still routes — before y. Unloadable bytes carry no notes: the reload guard refuses them anyway.
|
|
757
932
|
let poolNotes = [];
|
|
758
933
|
try {
|
|
759
|
-
const written = loadConfigWithMode(cwd, { globalDir,
|
|
934
|
+
const written = loadConfigWithMode(cwd, { globalDir, userOverlayText: after }).cfg;
|
|
760
935
|
poolNotes = deadPoolEntries(written, undefined, health).map(deadPoolEntryLine);
|
|
761
936
|
}
|
|
762
937
|
catch {
|
|
763
938
|
poolNotes = [];
|
|
764
939
|
}
|
|
940
|
+
const notes = [...repoShadowNotes(before, after), ...(lower.ok ? [] : [`${lower.error} — y will be refused`]), ...poolNotes];
|
|
765
941
|
return {
|
|
766
942
|
kind: "diff",
|
|
767
943
|
before,
|
|
768
944
|
after,
|
|
769
945
|
diff: unifiedYamlDiff(before, after, path),
|
|
770
946
|
path,
|
|
771
|
-
...(
|
|
947
|
+
...(notes.length ? { notes } : {}),
|
|
772
948
|
};
|
|
773
949
|
};
|
|
774
|
-
|
|
775
|
-
|
|
950
|
+
// B2: every repository inherits the user overlay, so its bytes must load here AND on their own —
|
|
951
|
+
// a tiers entry whose vendor/channel only this repository declares would break every other one
|
|
952
|
+
const loaderGuard = io.reloadGuard
|
|
953
|
+
?? ((bytes) => {
|
|
954
|
+
const here = overlayBytesLoadError(cwd, bytes, { globalDir, layer: "user" });
|
|
955
|
+
if (here !== null)
|
|
956
|
+
return here;
|
|
957
|
+
const alone = overlayBytesLoadError(cwd, bytes, { globalDir, layer: "user", repoOverlayText: "" });
|
|
958
|
+
return alone === null ? null
|
|
959
|
+
: `the user overlay ${userPath} loads only beside ${repoOverlayPath(cwd)} — every repository inherits it, and without that overlay it fails: ${alone}`;
|
|
960
|
+
});
|
|
961
|
+
// OBS-1188: a stale preview or an unreadable lower layer answers on the same error channel as the loader
|
|
962
|
+
const reloadGuard = (bytes) => (pendingWrite && stalePreview(pendingWrite, bytes)) || loaderGuard(bytes);
|
|
776
963
|
const props = {
|
|
777
964
|
ageMs: doctorAgeMs(cwd),
|
|
778
965
|
adapters,
|
|
@@ -784,7 +971,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
784
971
|
initialMode: rm.mode.mode,
|
|
785
972
|
modeOptions: ROUTING_MODES.map((mode) => ({ id: mode, gloss: MODE_GLOSS[mode] })),
|
|
786
973
|
initialMap: editable.map,
|
|
787
|
-
modePreview: (mode, map, deny) => [modeSpend(mode, map, deny), ...floorPreview(mode)],
|
|
974
|
+
modePreview: (mode, map, deny) => [modeSpend(mode, map, deny), ...floorPreview(mode, map, deny)],
|
|
788
975
|
shapeRows: routedShapeRows,
|
|
789
976
|
candidatesForShape,
|
|
790
977
|
preferOptionsForShape: (_shape, current) => [
|
|
@@ -817,10 +1004,27 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
817
1004
|
return "fleet: discarded overlay changes";
|
|
818
1005
|
// The command remains the single config actuator. Every interactive edit reaches this
|
|
819
1006
|
// one write only after the component-rendered diff confirm and the production reload guard.
|
|
820
|
-
const
|
|
821
|
-
if (!
|
|
1007
|
+
const pending = pendingWrite;
|
|
1008
|
+
if (!pending)
|
|
822
1009
|
throw new Error("fleet write reached confirmation without a staged overlay mutation");
|
|
823
|
-
|
|
1010
|
+
const write = pending.write;
|
|
1011
|
+
// OBS-1188: re-checked against the bytes on disk at write time; a refusal throws before the temp file exists.
|
|
1012
|
+
// B2: the one destination is the user overlay, and bytes the loader refuses never land there either.
|
|
1013
|
+
try {
|
|
1014
|
+
writeFleetOverlay(userPath, (prior) => {
|
|
1015
|
+
const refusal = result.review.path === userPath
|
|
1016
|
+
? stalePreview(pending, result.review.after, prior) ?? loaderGuard(result.review.after)
|
|
1017
|
+
: `the reviewed destination ${result.review.path} is not the user overlay ${userPath}`;
|
|
1018
|
+
if (refusal !== null)
|
|
1019
|
+
throw new ConfigError(refusal);
|
|
1020
|
+
return result.review.after;
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
catch (error) {
|
|
1024
|
+
if (error instanceof ConfigError)
|
|
1025
|
+
return `fleet: nothing written — ${error.message}`;
|
|
1026
|
+
throw error;
|
|
1027
|
+
}
|
|
824
1028
|
// OBS-529: a freshly classified model has no probe verdict (doctor probes CONFIGURED models,
|
|
825
1029
|
// and it was not configured at probe time), so it stays unroutable — invisible in every
|
|
826
1030
|
// picker — until the next probe. Name the step, or the classify flow reads as broken.
|
|
@@ -835,7 +1039,8 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
835
1039
|
unprobed.push(`${adapter}:${model}`);
|
|
836
1040
|
}
|
|
837
1041
|
}
|
|
838
|
-
|
|
1042
|
+
// B2: a repo-shadowed save says what still applies here — never claimed applied
|
|
1043
|
+
const head = [`fleet: wrote ${userPath}`, ...repoShadowNotes(result.review.before, result.review.after).map((note) => `fleet: ${note}`)].join("\n");
|
|
839
1044
|
if (!unprobed.length)
|
|
840
1045
|
return head;
|
|
841
1046
|
const named = unprobed.slice(0, 3).join(", ") + (unprobed.length > 3 ? `, +${unprobed.length - 3} more` : "");
|
|
@@ -111,7 +111,7 @@ Before \`tickmarkr compile\` or \`tickmarkr run\`: run \`tickmarkr version\`, re
|
|
|
111
111
|
|
|
112
112
|
### Tip-verify-before-green
|
|
113
113
|
|
|
114
|
-
A run is green only when ALL of these hold: the run-end event exists in the journal, tip verify is not "failed",
|
|
114
|
+
A run is green only when ALL of these hold: the run-end event exists in the journal, tip verify is not "failed", the run-end summary's \`failed\`, \`human\`, \`blocked\` and \`pending\` buckets are every one of them empty, AND the owed checks in CURRENT \`tickmarkr status <runId>\` are outstanding empty AND known (\`outstanding 0\`; \`outstanding unknown\` is never empty). The shorter "run-end plus tip verify" form is NOT sufficient: a run ending \`done=[T1,T3,T4] human=[T2] tipVerify=passed\` is a closed PARTIAL run — three of four delivered, one parked — and calling it green is how a park becomes invisible. Empty execution buckets are not sufficient either (D-660): a run ending with every execution bucket empty and \`tipVerify=passed\` after an operator waived one review still owes that accepted-risk review check — status reads \`outstanding 1 (T7 review)\`, and \`tickmarkr run\` and \`resume\` exit 0 on execution alone — so it is execution complete, not green. Read the debt from CURRENT status, never from the run-end record: a \`tickmarkr verify --record <runId>\` discharge that lands after run-end moves current status to \`outstanding 0\` while the historical run-end row still carries 1, and only then is that run green; a legacy waiver without a recorded obligation reads \`outstanding unknown\` and is never green. Never report green to the operator, tab titles, or records until every clause holds; when one does not, name the bucket that is not empty, the outstanding checks, or the unknown debt.
|
|
115
115
|
|
|
116
116
|
### Verified handoffs
|
|
117
117
|
|
|
@@ -634,7 +634,8 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
|
|
|
634
634
|
const output = io.output ?? process.stdout;
|
|
635
635
|
const interactive = input.isTTY === true && output.isTTY === true && !(values.yes ?? false);
|
|
636
636
|
// Act 1 — preferences. Only when the repo config does not exist yet: init never rewrites an
|
|
637
|
-
// operator's config; re-tuning is `tickmarkr fleet` (act 3 below still runs
|
|
637
|
+
// operator's config; re-tuning is `tickmarkr fleet` (act 3 below still runs). Act 1 writes only
|
|
638
|
+
// project execution preferences; model choices are act 3's, saved to the USER overlay (B2).
|
|
638
639
|
if (!repoConfigExists) {
|
|
639
640
|
if (interactive) {
|
|
640
641
|
emitBanner();
|
|
@@ -671,7 +672,8 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
|
|
|
671
672
|
: await doctor([], cwd, undefined, { banner: false, compact: interactive });
|
|
672
673
|
// Act 3 — fleet. The compact discovery surface prints BETWEEN the acts so the operator reads
|
|
673
674
|
// what the fleet browser (and its presets overlay, raised on the first Shapes entry) ranks
|
|
674
|
-
// with; the final summary then carries a pointer, not a repeat.
|
|
675
|
+
// with; the final summary then carries a pointer, not a repeat. Its confirmation is the shared
|
|
676
|
+
// Fleet actuator's: the reviewed choices land in the user overlay (gdir), never the repo's.
|
|
675
677
|
if (interactive) {
|
|
676
678
|
output.write(`${doc}\n`);
|
|
677
679
|
doc = "discovery shown above — full matrix any time with `tickmarkr doctor`";
|