tickmarkr 2.5.4 → 2.5.6
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 +3 -1
- package/dist/adapters/registry.js +6 -1
- package/dist/adapters/types.d.ts +3 -0
- package/dist/cli/commands/approve.d.ts +1 -0
- package/dist/cli/commands/approve.js +118 -9
- package/dist/cli/commands/doctor.d.ts +10 -0
- package/dist/cli/commands/doctor.js +44 -29
- package/dist/cli/commands/fleet.js +199 -33
- package/dist/cli/commands/init.js +196 -6
- package/dist/cli/commands/plan.js +20 -4
- package/dist/cli/commands/resume.js +4 -2
- package/dist/cli/commands/run.js +11 -2
- package/dist/cli/commands/verify.js +102 -81
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +3 -1
- package/dist/config/config.d.ts +25 -8
- package/dist/config/config.js +41 -29
- package/dist/config/fleet-overlay.d.ts +3 -9
- package/dist/config/fleet-overlay.js +114 -19
- package/dist/config/fleet-why.d.ts +7 -0
- package/dist/config/fleet-why.js +5 -0
- package/dist/drivers/index.d.ts +15 -1
- package/dist/drivers/index.js +39 -10
- package/dist/drivers/orca.d.ts +119 -10
- package/dist/drivers/orca.js +781 -110
- package/dist/gates/baseline.d.ts +17 -3
- package/dist/gates/baseline.js +63 -15
- package/dist/gates/cache.d.ts +101 -0
- package/dist/gates/cache.js +401 -0
- package/dist/gates/llm.d.ts +3 -0
- package/dist/gates/llm.js +11 -0
- package/dist/gates/review.d.ts +28 -3
- package/dist/gates/review.js +106 -14
- package/dist/gates/run-gates.d.ts +9 -1
- package/dist/gates/run-gates.js +407 -102
- package/dist/gates/test-manifest.d.ts +128 -0
- package/dist/gates/test-manifest.js +463 -0
- package/dist/gates/test-reporter.d.ts +4 -0
- package/dist/gates/test-reporter.js +57 -0
- package/dist/graph/graph.d.ts +2 -0
- package/dist/graph/graph.js +45 -1
- package/dist/route/preference.d.ts +22 -1
- package/dist/route/preference.js +123 -25
- package/dist/route/router.js +31 -6
- package/dist/run/consult.js +5 -4
- package/dist/run/daemon.d.ts +15 -0
- package/dist/run/daemon.js +638 -167
- package/dist/run/execution-budget.d.ts +25 -0
- package/dist/run/execution-budget.js +142 -0
- package/dist/run/git.d.ts +50 -1
- package/dist/run/git.js +131 -12
- package/dist/run/journal.d.ts +16 -2
- package/dist/run/journal.js +115 -25
- package/dist/run/lease.d.ts +58 -0
- package/dist/run/lease.js +310 -0
- package/dist/run/merge.d.ts +2 -0
- package/dist/run/merge.js +91 -3
- package/dist/run/operator-state.d.ts +11 -0
- package/dist/run/operator-state.js +17 -3
- package/dist/run/recovery.d.ts +8 -0
- package/dist/run/recovery.js +25 -0
- package/dist/run/repair-selection.d.ts +12 -0
- package/dist/run/repair-selection.js +56 -0
- package/dist/run/stall.d.ts +6 -1
- package/dist/run/stall.js +60 -3
- package/dist/tui/cockpit/board.d.ts +96 -0
- package/dist/tui/cockpit/board.js +346 -0
- package/dist/tui/cockpit/decision-actions.js +2 -0
- package/dist/tui/cockpit/layout.d.ts +5 -1
- package/dist/tui/cockpit/layout.js +8 -3
- package/dist/tui/cockpit/live-runtime.js +71 -21
- package/dist/tui/cockpit/run-view.d.ts +7 -5
- package/dist/tui/cockpit/run-view.js +12 -11
- package/dist/tui/ink/fleet-app.d.ts +49 -27
- package/dist/tui/ink/fleet-app.js +229 -38
- package/package.json +2 -2
- package/skills/tickmarkr-overseer/SKILL.md +173 -113
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +29 -2
|
@@ -5,13 +5,13 @@ import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse, modelAuthE
|
|
|
5
5
|
import { catalogModelAdvisory, catalogTierRanking, declaredModelWindow, fleetUnclassifiedModels } from "../../adapters/model-lints.js";
|
|
6
6
|
import { CATALOG_REFRESH_TIMEOUT_MS, formatCatalogRefreshLegs, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
|
|
7
7
|
import { CLAUDE_ALIAS_IDENTITY_STAMPS, readClaudeAliasIdentity } from "../../adapters/claude-code.js";
|
|
8
|
-
import { fleetEditableFromConfig, fleetEditableEquals, formatFleetPrint, globalConfigDir, overlayBytesLoadError, renderFleetOverlayWrite, repoOverlayPath, ROUTING_MODES, unifiedYamlDiff, } from "../../config/config.js";
|
|
9
|
-
import { projectFleetWhy, renderFleetWhy } from "../../config/fleet-why.js";
|
|
8
|
+
import { fleetEditableFromConfig, fleetEditableEquals, formatFleetPrint, globalConfigDir, loadConfigWithMode, overlayBytesLoadError, readOverlayFile, renderFleetOverlayWrite, repoOverlayPath, ROUTING_MODES, TIER_RANK, unifiedYamlDiff, } from "../../config/config.js";
|
|
9
|
+
import { exclusionReason, projectFleetWhy, renderFleetWhy } from "../../config/fleet-why.js";
|
|
10
10
|
import { SHAPES, TIERS } from "../../graph/schema.js";
|
|
11
11
|
import { doctor } from "./doctor.js";
|
|
12
12
|
import { candidateRow, costSignal, shapeCandidates } from "./fleet-picker.js";
|
|
13
13
|
import { route } from "../../route/router.js";
|
|
14
|
-
import { disallowedBy } from "../../route/preference.js";
|
|
14
|
+
import { denyPreferCollisionLine, denyPreferCollisions, disallowedBy, entryMatchesChannel, exclusionCollector } from "../../route/preference.js";
|
|
15
15
|
import { resolveRunMode } from "../../run/daemon.js";
|
|
16
16
|
import { loadRoutingProfile } from "../../run/journal.js";
|
|
17
17
|
const initialFetch = globalThis.fetch;
|
|
@@ -202,10 +202,17 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
202
202
|
const previewChannels = discoverChannels({ ...cfg, routing: routingScopeBlind }, adapters, health);
|
|
203
203
|
const universe = adapters
|
|
204
204
|
.filter((adapter) => health[adapter.id]?.installed)
|
|
205
|
-
.map((adapter) =>
|
|
206
|
-
adapter
|
|
207
|
-
|
|
208
|
-
|
|
205
|
+
.map((adapter) => {
|
|
206
|
+
const served = previewChannels.filter((c) => c.adapter === adapter.id);
|
|
207
|
+
// LEG2-T3 round 2 finding 3: the recorded identity per channel, so the editable membership
|
|
208
|
+
// matches allow/deny entries with the same authority the collector and the browser use
|
|
209
|
+
const identities = Object.fromEntries(served.flatMap((c) => (c.identity ? [[c.model, c.identity]] : [])));
|
|
210
|
+
return {
|
|
211
|
+
adapter: adapter.id,
|
|
212
|
+
models: [...new Set(served.map((c) => c.model))],
|
|
213
|
+
...(Object.keys(identities).length ? { identities } : {}),
|
|
214
|
+
};
|
|
215
|
+
});
|
|
209
216
|
const initial = fleetEditableFromConfig(cfg, universe);
|
|
210
217
|
const editable = structuredClone(initial);
|
|
211
218
|
// OBS-508: the same catalog evidence doctor's drift overlay prints now rides each unclassified
|
|
@@ -250,9 +257,14 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
250
257
|
|| a.model.localeCompare(b.model));
|
|
251
258
|
// OBS-508 follow-through: the browser renders the metadata the assembler always had — catalog
|
|
252
259
|
// ctx/price evidence and doctor's model-probe wall clock — as columns instead of dropping them.
|
|
260
|
+
// OBS-972/FL-1: the same alias→identity doctor recorded (modelAuth.identity, falling back to
|
|
261
|
+
// the coarser per-adapter modelIdentities map registry.ts/preference.ts already read for
|
|
262
|
+
// routing exclusion) — a floating alias's resolved concrete model id.
|
|
263
|
+
const resolvedModelIdentity = (adapter, model) => health[adapter]?.modelAuth?.[model]?.identity ?? health[adapter]?.modelIdentities?.[model];
|
|
253
264
|
const rowEvidence = (adapter, model, evidence) => {
|
|
254
265
|
const probed = health[adapter]?.modelAuth?.[model];
|
|
255
266
|
const contextWindow = evidence?.contextWindow ?? declaredModelWindow(cfg, adapter, model);
|
|
267
|
+
const identity = resolvedModelIdentity(adapter, model);
|
|
256
268
|
const out = {
|
|
257
269
|
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
258
270
|
...(evidence?.outputWindow !== undefined ? { outputWindow: evidence.outputWindow } : {}),
|
|
@@ -262,6 +274,8 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
262
274
|
// OBS-519: doctor already recorded the failed verdict — a rate-limited/unauthed model must
|
|
263
275
|
// not render identically to a healthy row on the surface that edits its fleet membership.
|
|
264
276
|
...(probed?.authed === false ? { unauthed: probed.reason ?? "probe failed" } : {}),
|
|
277
|
+
// the browser decides deny coverage itself, over the STAGED policy (LEG2-T3 finding 1)
|
|
278
|
+
...(identity !== undefined ? { identity } : {}),
|
|
265
279
|
};
|
|
266
280
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
267
281
|
};
|
|
@@ -306,24 +320,44 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
306
320
|
]));
|
|
307
321
|
const channels = discoverChannels(cfg, adapters, health);
|
|
308
322
|
const profile = loadRoutingProfile(cwd, cfg, { preview: true });
|
|
323
|
+
const stagedEditable = (map, deny) => ({
|
|
324
|
+
...structuredClone(initial),
|
|
325
|
+
denyAdapters: deny.adapters,
|
|
326
|
+
denyModels: deny.models,
|
|
327
|
+
// OBS-1046: the allow complement is staged beside the deny lists, never folded into them
|
|
328
|
+
allowOut: deny.allowOut ?? initial.allowOut,
|
|
329
|
+
denyWorkersAdapters: deny.workersAdapters,
|
|
330
|
+
denyWorkersModels: deny.workersModels,
|
|
331
|
+
map,
|
|
332
|
+
});
|
|
333
|
+
const candidateMemo = new Map();
|
|
309
334
|
const previewCfg = (mode, map, deny) => {
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
335
|
+
const key = JSON.stringify([mode, map, deny]);
|
|
336
|
+
const hit = candidateMemo.get(key);
|
|
337
|
+
if (hit)
|
|
338
|
+
return hit;
|
|
339
|
+
let preview;
|
|
340
|
+
try {
|
|
341
|
+
const bytes = renderFleetOverlayWrite(currentRepoOverlayText(cwd), {
|
|
342
|
+
initial,
|
|
343
|
+
edited: stagedEditable(map, deny),
|
|
344
|
+
universe,
|
|
345
|
+
...(mode !== rm.mode.mode ? { mode } : {}),
|
|
346
|
+
});
|
|
347
|
+
preview = { ok: true, cfg: loadConfigWithMode(cwd, { globalDir, repoOverlayText: bytes }).cfg };
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
preview = { ok: false, error: error.message.replace(/\s+/g, " ").trim() };
|
|
351
|
+
}
|
|
352
|
+
if (candidateMemo.size > 32)
|
|
353
|
+
candidateMemo.clear();
|
|
354
|
+
candidateMemo.set(key, preview);
|
|
355
|
+
return preview;
|
|
356
|
+
};
|
|
357
|
+
const previewUnavailable = (error) => `preview unavailable (${error}) — the staged overlay does not load, so w would be refused`;
|
|
358
|
+
const stagedRouting = (deny) => {
|
|
359
|
+
const preview = previewCfg(rm.mode.mode, editable.map, deny);
|
|
360
|
+
return preview.ok ? { ok: true, routing: preview.cfg.routing } : preview;
|
|
327
361
|
};
|
|
328
362
|
// route() never deny-filters its pool — that contract lives in discoverChannels — so every
|
|
329
363
|
// preview call rebuilds the pool the staged deny would discover
|
|
@@ -333,7 +367,10 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
333
367
|
let subs = 0;
|
|
334
368
|
let apiN = 0;
|
|
335
369
|
let apiUsd = 0;
|
|
336
|
-
const
|
|
370
|
+
const preview = previewCfg(mode, map, deny);
|
|
371
|
+
if (!preview.ok)
|
|
372
|
+
return ` mix: ${previewUnavailable(preview.error)}`;
|
|
373
|
+
const cfgPreview = preview.cfg;
|
|
337
374
|
const pool = previewPool(cfgPreview);
|
|
338
375
|
for (const shape of SHAPES) {
|
|
339
376
|
try {
|
|
@@ -393,7 +430,15 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
393
430
|
: { declaredAt: "routing.mode" };
|
|
394
431
|
};
|
|
395
432
|
const projectedShapeRows = (mode, map, deny) => {
|
|
396
|
-
const
|
|
433
|
+
const preview = previewCfg(mode, map, deny);
|
|
434
|
+
if (!preview.ok) {
|
|
435
|
+
return projectFleetWhy(SHAPES.map((shape) => ({
|
|
436
|
+
id: shape,
|
|
437
|
+
effective: previewUnavailable(preview.error),
|
|
438
|
+
...whyDeclaration(shape, mode, map, false),
|
|
439
|
+
})), { repoRoot: cwd, globalDir });
|
|
440
|
+
}
|
|
441
|
+
const cfgPreview = preview.cfg;
|
|
397
442
|
const pool = previewPool(cfgPreview);
|
|
398
443
|
const values = SHAPES.map((shape) => {
|
|
399
444
|
try {
|
|
@@ -433,13 +478,13 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
433
478
|
const unauthedClis = adapters
|
|
434
479
|
.filter((adapter) => health[adapter.id]?.installed && health[adapter.id]?.authed === false)
|
|
435
480
|
.map((adapter) => adapter.id);
|
|
436
|
-
const
|
|
437
|
-
const
|
|
481
|
+
const unauthedModels = modelAuthExclusions(cfg, adapters, health);
|
|
482
|
+
const unauthedModelCount = unauthedModels.length;
|
|
483
|
+
const excludedNoteFor = (cfgPreview) => {
|
|
438
484
|
let stagedOut = 0;
|
|
439
485
|
let denied = 0;
|
|
440
|
-
const stagedRouting = previewCfg(rm.mode.mode, {}, deny).routing;
|
|
441
486
|
for (const channel of previewChannels) {
|
|
442
|
-
if (disallowedBy(channel,
|
|
487
|
+
if (disallowedBy(channel, cfgPreview.routing) === null)
|
|
443
488
|
continue;
|
|
444
489
|
if (disallowedBy(channel, cfg.routing) === null)
|
|
445
490
|
stagedOut += 1;
|
|
@@ -459,14 +504,122 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
459
504
|
parts.push(`${unclassifiedRows.length} unclassified (never routed)`);
|
|
460
505
|
return parts.length ? `not offered: ${parts.join(" · ")} — the models view explains each row` : undefined;
|
|
461
506
|
};
|
|
507
|
+
// OBS-994/FL-1: the nine mechanisms outside the deny scopes that can keep a channel off a
|
|
508
|
+
// shape's candidate list each get a reason or an explicit "not manageable here" caption — a
|
|
509
|
+
// silent omission is indistinguishable from a bug (OBS-530's lesson, extended past deny/allow).
|
|
510
|
+
// allow and the unauthed probe already carry a reason through excludedNoteFor above; the rest
|
|
511
|
+
// are shape-scoped (a pin/pool/floor/prefer excludes only for THIS shape) and belong here.
|
|
512
|
+
const shapeExclusionCaptions = (shape, map, cfgPreview) => {
|
|
513
|
+
const pool = previewPool(cfgPreview);
|
|
514
|
+
const entry = map[shape];
|
|
515
|
+
const captions = [];
|
|
516
|
+
// 2. pin or pool — a closed candidate set declared for this shape; a channel outside it is
|
|
517
|
+
// not manageable from the models view, it is a Shapes-view declaration.
|
|
518
|
+
if (entry?.pin) {
|
|
519
|
+
captions.push(`pin: routing.map.${shape}.pin fixes this shape to ${entry.pin.via}:${entry.pin.model} — not manageable here, edit it in Shapes`);
|
|
520
|
+
}
|
|
521
|
+
else if (entry?.pool) {
|
|
522
|
+
const declared = new Set(entry.pool.channels);
|
|
523
|
+
const outside = pool.filter((c) => !declared.has(`${c.adapter}:${c.model}`));
|
|
524
|
+
if (outside.length) {
|
|
525
|
+
captions.push(`pool: ${outside.length} channel(s) outside routing.map.${shape}.pool's declared set — not manageable here, edit it in Shapes`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// 3. floor — channels below this shape's minimum tier.
|
|
529
|
+
const floor = cfgPreview.routing.floors[shape];
|
|
530
|
+
if (floor) {
|
|
531
|
+
const belowFloor = pool.filter((c) => TIER_RANK[c.tier] < TIER_RANK[floor]);
|
|
532
|
+
if (belowFloor.length) {
|
|
533
|
+
captions.push(`floor: ${belowFloor.length} channel(s) below routing.floors.${shape} (${floor}) — not manageable here, edit the floor in Shapes`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// 5. tombstones — the repo overlay's OWN null over this shape's map entry, masking a lower
|
|
537
|
+
// (global/default) layer's declaration rather than expressing a value of its own.
|
|
538
|
+
const repoRaw = readOverlayFile(repoOverlayPath(cwd));
|
|
539
|
+
const rawEntry = repoRaw.routing?.map?.[shape];
|
|
540
|
+
if (rawEntry === null) {
|
|
541
|
+
captions.push(`tombstone: routing.map.${shape}: null masks a lower layer's declaration — not manageable here, edit the repo overlay directly`);
|
|
542
|
+
}
|
|
543
|
+
else if (rawEntry && "pool" in rawEntry && rawEntry.pool === null) {
|
|
544
|
+
captions.push(`tombstone: routing.map.${shape}.pool: null masks a lower layer's pool — not manageable here, edit the repo overlay directly`);
|
|
545
|
+
}
|
|
546
|
+
else if (rawEntry && "pin" in rawEntry && rawEntry.pin === null) {
|
|
547
|
+
captions.push(`tombstone: routing.map.${shape}.pin: null masks a lower layer's pin — not manageable here, edit the repo overlay directly`);
|
|
548
|
+
}
|
|
549
|
+
// 6. explore/learned knobs — global routing settings, never a per-channel fleet toggle.
|
|
550
|
+
if (cfgPreview.routing.explore?.mode === "off") {
|
|
551
|
+
captions.push("explore: routing.explore.mode is off — a global knob, not manageable here");
|
|
552
|
+
}
|
|
553
|
+
if (cfgPreview.routing.learned === "off") {
|
|
554
|
+
captions.push("learned: routing.learned is off — a global knob, not manageable here");
|
|
555
|
+
}
|
|
556
|
+
// 7. task hint — a shape's prefer bias is authored in Shapes/spec, not per-channel in fleet.
|
|
557
|
+
if (entry?.prefer?.length) {
|
|
558
|
+
captions.push(`task hint: routing.map.${shape}.prefer ranks ${entry.prefer.join(", ")} — not manageable here, edit it in Shapes`);
|
|
559
|
+
}
|
|
560
|
+
// 8. deny∩prefer collision — a standing lint on this shape's declaration, never a fleet toggle.
|
|
561
|
+
for (const collision of denyPreferCollisions(cfgPreview, [shape])) {
|
|
562
|
+
captions.push(denyPreferCollisionLine(collision));
|
|
563
|
+
}
|
|
564
|
+
// 9. first-match provenance — the per-channel ledger below lists EVERY collector scope.
|
|
565
|
+
return captions;
|
|
566
|
+
};
|
|
567
|
+
// LEG2-T3 finding 1: each channel a mechanism touches on this shape gets its own ledger row
|
|
568
|
+
// naming every reason — collector scopes with their config paths (allow, flat and workers deny,
|
|
569
|
+
// all of them, never a first match), the unauthed probe, pin/pool/floor, the task hint and a
|
|
570
|
+
// deny∩prefer collision it is the target of — or that leaves the shape with no candidate at all.
|
|
571
|
+
const channelLedger = (shape, map, cfgPreview, offered) => {
|
|
572
|
+
const entry = map[shape];
|
|
573
|
+
const floor = cfgPreview.routing.floors[shape];
|
|
574
|
+
const collisions = denyPreferCollisions(cfgPreview, [shape]);
|
|
575
|
+
const discovered = new Set(previewChannels.map((c) => `${c.adapter}:${c.model}`));
|
|
576
|
+
// discovery already dropped a channel whose model probe failed — it still gets its own row
|
|
577
|
+
const unauthedRows = [
|
|
578
|
+
...unauthedClis.map((adapter) => `${adapter} — CLI unauthed — re-probe with tickmarkr doctor`),
|
|
579
|
+
...unauthedModels.filter(({ key }) => !discovered.has(key))
|
|
580
|
+
.map(({ key, reason }) => `${key.replace(":", "/")} — unauthed (${reason}) — re-probe with tickmarkr doctor`),
|
|
581
|
+
];
|
|
582
|
+
return [...unauthedRows, ...previewChannels.flatMap((c) => {
|
|
583
|
+
const key = `${c.adapter}:${c.model}`;
|
|
584
|
+
const reasons = exclusionCollector(c, cfgPreview.routing, "worker").map(exclusionReason);
|
|
585
|
+
if (health[c.adapter]?.authed === false)
|
|
586
|
+
reasons.push(`${c.adapter} CLI unauthed — re-probe with tickmarkr doctor`);
|
|
587
|
+
else if (health[c.adapter]?.modelAuth?.[c.model]?.authed === false)
|
|
588
|
+
reasons.push("unauthed — re-probe with tickmarkr doctor");
|
|
589
|
+
if (entry?.pin && key !== `${entry.pin.via}:${entry.pin.model}`) {
|
|
590
|
+
reasons.push(`not routing.map.${shape}.pin (${entry.pin.via}:${entry.pin.model}) — not manageable here`);
|
|
591
|
+
}
|
|
592
|
+
if (entry?.pool && !entry.pool.channels.includes(key)) {
|
|
593
|
+
reasons.push(`outside routing.map.${shape}.pool — not manageable here`);
|
|
594
|
+
}
|
|
595
|
+
if (floor && TIER_RANK[c.tier] < TIER_RANK[floor]) {
|
|
596
|
+
reasons.push(`below routing.floors.${shape} (${floor}) — not manageable here`);
|
|
597
|
+
}
|
|
598
|
+
if (entry?.prefer?.some((p) => entryMatchesChannel(p, c))) {
|
|
599
|
+
reasons.push(`task hint: routing.map.${shape}.prefer names it — not manageable here`);
|
|
600
|
+
}
|
|
601
|
+
for (const collision of collisions) {
|
|
602
|
+
const named = collision.detail.split(" > ").some((p) => entryMatchesChannel(p, c));
|
|
603
|
+
if (named || offered.size === 0)
|
|
604
|
+
reasons.push(denyPreferCollisionLine(collision));
|
|
605
|
+
}
|
|
606
|
+
// labelled like the models view's row (adapter/model), so a ledger row never reads as a candidate
|
|
607
|
+
return reasons.length ? [`${c.adapter}/${c.model} — ${reasons.join("; ")}`] : [];
|
|
608
|
+
})];
|
|
609
|
+
};
|
|
462
610
|
const candidatesForShape = (shape, mode, map, deny) => {
|
|
463
|
-
const
|
|
611
|
+
const preview = previewCfg(mode, map, deny);
|
|
612
|
+
if (!preview.ok)
|
|
613
|
+
return { rows: [], excludedNote: previewUnavailable(preview.error) };
|
|
614
|
+
const cfgPreview = preview.cfg;
|
|
464
615
|
const rows = shapeCandidates(previewTask(shape), cfgPreview, previewPool(cfgPreview), profile).map((candidate) => ({
|
|
465
616
|
id: `${candidate.assignment.adapter}:${candidate.assignment.model}`,
|
|
466
617
|
label: candidateRow(candidate, cfg.pricing),
|
|
467
618
|
pin: { via: candidate.assignment.adapter, model: candidate.assignment.model },
|
|
468
619
|
}));
|
|
469
|
-
|
|
620
|
+
const offered = new Set(rows.map((row) => row.id));
|
|
621
|
+
const notes = [excludedNoteFor(cfgPreview), ...shapeExclusionCaptions(shape, map, cfgPreview)].filter((note) => note !== undefined);
|
|
622
|
+
return { rows, excludedNote: notes.length ? notes.join("\n") : undefined, ledger: channelLedger(shape, map, cfgPreview, offered) };
|
|
470
623
|
};
|
|
471
624
|
const preferUniverse = [
|
|
472
625
|
...new Set(channels.flatMap((channel) => [
|
|
@@ -493,6 +646,10 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
493
646
|
const staged = structuredClone(initial);
|
|
494
647
|
staged.denyAdapters = state.denyAdapters;
|
|
495
648
|
staged.denyModels = state.denyModels;
|
|
649
|
+
staged.allowOut = state.allowOut ?? initial.allowOut;
|
|
650
|
+
// OBS-994/FL-1: the workers-only deny scope rides the same review/write funnel as the flat one.
|
|
651
|
+
staged.denyWorkersAdapters = state.denyWorkersAdapters;
|
|
652
|
+
staged.denyWorkersModels = state.denyWorkersModels;
|
|
496
653
|
staged.map = state.map;
|
|
497
654
|
const today = new Date().toISOString().slice(0, 10);
|
|
498
655
|
for (const classification of state.classifications) {
|
|
@@ -545,6 +702,9 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
545
702
|
health,
|
|
546
703
|
initialDenyAdapters: editable.denyAdapters,
|
|
547
704
|
initialDenyModels: editable.denyModels,
|
|
705
|
+
initialAllowOut: editable.allowOut,
|
|
706
|
+
initialDenyWorkersAdapters: editable.denyWorkersAdapters,
|
|
707
|
+
initialDenyWorkersModels: editable.denyWorkersModels,
|
|
548
708
|
modelGroups,
|
|
549
709
|
initialMode: rm.mode.mode,
|
|
550
710
|
modeOptions: ROUTING_MODES.map((mode) => ({ id: mode, gloss: MODE_GLOSS[mode] })),
|
|
@@ -560,6 +720,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
560
720
|
steeringOptionsFor,
|
|
561
721
|
reviewOverlay,
|
|
562
722
|
reloadGuard,
|
|
723
|
+
stagedRouting,
|
|
563
724
|
entry: opts.entry,
|
|
564
725
|
initialJudge,
|
|
565
726
|
judgeSeats: seats,
|
|
@@ -607,6 +768,11 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
607
768
|
return {
|
|
608
769
|
props,
|
|
609
770
|
commit,
|
|
610
|
-
renderWhy: () => renderFleetWhy(projectedShapeRows(rm.mode.mode, editable.map, {
|
|
771
|
+
renderWhy: () => renderFleetWhy(projectedShapeRows(rm.mode.mode, editable.map, {
|
|
772
|
+
adapters: editable.denyAdapters,
|
|
773
|
+
models: editable.denyModels,
|
|
774
|
+
workersAdapters: editable.denyWorkersAdapters ?? [],
|
|
775
|
+
workersModels: editable.denyWorkersModels ?? [],
|
|
776
|
+
})),
|
|
611
777
|
};
|
|
612
778
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { appendFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join, relative } from "node:path";
|
|
2
|
+
import { appendFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
@@ -9,10 +9,11 @@ import { allAdapters, formatDoctorAgeForInit, formatDoctorReport, initDoctorReus
|
|
|
9
9
|
import { configTemplate, DEFAULT_CONFIG, globalConfigDir, loadConfig } from "../../config/config.js";
|
|
10
10
|
import { LEGACY_PREFIX, specTemplate } from "../../compile/native.js";
|
|
11
11
|
import { BANNER, kvRow, legend, rule, statusRow, title } from "../../brand.js";
|
|
12
|
-
import { tickmarkrDir } from "../../graph/graph.js";
|
|
12
|
+
import { stateDirName, tickmarkrDir } from "../../graph/graph.js";
|
|
13
13
|
import { orcaHostDetected } from "../../drivers/index.js";
|
|
14
|
+
import { WORKTREES_DIR } from "../../run/git.js";
|
|
14
15
|
import { Journal } from "../../run/journal.js";
|
|
15
|
-
import { runLockRunId, runStatusLine } from "../../run/lock.js";
|
|
16
|
+
import { runLockOwner, runLockRunId, runStatusLine } from "../../run/lock.js";
|
|
16
17
|
import { doctor } from "./doctor.js";
|
|
17
18
|
import { assembleFleetEditor } from "./fleet.js";
|
|
18
19
|
const SCAFFOLD_SPEC = "tickmarkr.spec.md";
|
|
@@ -340,6 +341,191 @@ async function runInitWizard(cwd, input, output) {
|
|
|
340
341
|
return null;
|
|
341
342
|
return { overlay: result.overlay, installSkills: result.installSkills, installDocs: result.installDocs };
|
|
342
343
|
}
|
|
344
|
+
function realpathSafe(p) {
|
|
345
|
+
try {
|
|
346
|
+
return realpathSync(p);
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
const parent = dirname(p);
|
|
350
|
+
if (parent === p)
|
|
351
|
+
return p;
|
|
352
|
+
return join(realpathSafe(parent), basename(p));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function isPathInside(parentDir, candidatePath) {
|
|
356
|
+
const resolvedParent = realpathSafe(resolve(parentDir));
|
|
357
|
+
const resolvedCandidate = realpathSafe(resolve(candidatePath));
|
|
358
|
+
const rel = relative(resolvedParent, resolvedCandidate);
|
|
359
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* The ONLY way the --remove path runs git: GIT_DIR, GIT_WORK_TREE and GIT_COMMON_DIR are stripped so the repository acted on
|
|
363
|
+
* is always cwd's own, and LC_ALL=C keeps stderr parseable. Throws execFileSync's error (status, stderr) unchanged.
|
|
364
|
+
*/
|
|
365
|
+
function removeGit(cwd, args) {
|
|
366
|
+
const { GIT_DIR: _dir, GIT_WORK_TREE: _workTree, GIT_COMMON_DIR: _commonDir, ...env } = process.env;
|
|
367
|
+
return execFileSync("git", args, { cwd, env: { ...env, LC_ALL: "C" }, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
368
|
+
}
|
|
369
|
+
function isLinkedWorktree(cwd) {
|
|
370
|
+
try {
|
|
371
|
+
const gitDir = removeGit(cwd, ["rev-parse", "--git-dir"]).trim();
|
|
372
|
+
const gitCommonDir = removeGit(cwd, ["rev-parse", "--git-common-dir"]).trim();
|
|
373
|
+
return resolve(cwd, gitDir) !== resolve(cwd, gitCommonDir);
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function checkLiveLock(cwd) {
|
|
380
|
+
const localOwner = runLockOwner(cwd);
|
|
381
|
+
if (localOwner?.live)
|
|
382
|
+
return localOwner;
|
|
383
|
+
try {
|
|
384
|
+
const gitDir = removeGit(cwd, ["rev-parse", "--git-dir"]).trim();
|
|
385
|
+
const gitCommonDir = removeGit(cwd, ["rev-parse", "--git-common-dir"]).trim();
|
|
386
|
+
if (resolve(cwd, gitDir) !== resolve(cwd, gitCommonDir)) {
|
|
387
|
+
const mainRepo = dirname(resolve(cwd, gitCommonDir));
|
|
388
|
+
const mainOwner = runLockOwner(mainRepo);
|
|
389
|
+
if (mainOwner?.live)
|
|
390
|
+
return mainOwner;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
// not a git repo
|
|
395
|
+
}
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Only git's discovery answer — exit 128, stderr `fatal: not a git repository (or any of the parent directories)` — means false.
|
|
400
|
+
* Every other failure (explicit bad GIT_DIR, invalid gitfile, permission, missing binary) refuses: unregistration could not be verified.
|
|
401
|
+
*/
|
|
402
|
+
function isGitRepo(cwd) {
|
|
403
|
+
try {
|
|
404
|
+
removeGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
catch (err) {
|
|
408
|
+
const e = err;
|
|
409
|
+
const stderr = String(e.stderr ?? "").trim();
|
|
410
|
+
if (e.status === 128 && stderr.startsWith("fatal: not a git repository (or any of the parent directories)"))
|
|
411
|
+
return false;
|
|
412
|
+
throw new Error(`cannot remove: git repository probe failed: ${stderr || e.message || "unknown error"}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/** Runs git and throws `message` plus git's stderr on failure — a remove never proceeds past a failed step. */
|
|
416
|
+
function git(cwd, args, message) {
|
|
417
|
+
try {
|
|
418
|
+
return removeGit(cwd, args);
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
const stderr = String(err.stderr ?? "").trim();
|
|
422
|
+
throw new Error(stderr ? `${message}: ${stderr}` : message);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function registeredWorktrees(cwd, worktreesDir) {
|
|
426
|
+
const out = git(cwd, ["worktree", "list", "--porcelain"], "cannot remove: git worktree list failed");
|
|
427
|
+
return out
|
|
428
|
+
.split("\n")
|
|
429
|
+
.filter((line) => line.startsWith("worktree "))
|
|
430
|
+
.map((line) => line.slice("worktree ".length).trim())
|
|
431
|
+
.filter((wt) => isPathInside(worktreesDir, wt));
|
|
432
|
+
}
|
|
433
|
+
function handleRemove(cwd) {
|
|
434
|
+
const owner = checkLiveLock(cwd);
|
|
435
|
+
if (owner?.live) {
|
|
436
|
+
const holder = typeof owner.pid === "number" ? `live pid ${owner.pid}` : (owner.pid ?? "unreadable payload");
|
|
437
|
+
throw new Error(`cannot remove: graph.lock held by ${holder}`);
|
|
438
|
+
}
|
|
439
|
+
if (isLinkedWorktree(cwd)) {
|
|
440
|
+
throw new Error("cannot remove from inside a linked worktree");
|
|
441
|
+
}
|
|
442
|
+
const notes = [];
|
|
443
|
+
const stateDir = join(cwd, stateDirName(cwd));
|
|
444
|
+
const worktreesDir = join(stateDir, WORKTREES_DIR);
|
|
445
|
+
// 1. Unregister every git worktree under stateDir/WORKTREES_DIR, and prove it, before deleting anything
|
|
446
|
+
if (isGitRepo(cwd)) {
|
|
447
|
+
for (const wt of registeredWorktrees(cwd, worktreesDir)) {
|
|
448
|
+
// double --force: git refuses a locked worktree (present or already deleted) with a single one
|
|
449
|
+
git(cwd, ["worktree", "remove", "--force", "--force", wt], `cannot remove: failed to unregister worktree ${wt}`);
|
|
450
|
+
}
|
|
451
|
+
git(cwd, ["worktree", "prune"], "cannot remove: git worktree prune failed");
|
|
452
|
+
const left = registeredWorktrees(cwd, worktreesDir);
|
|
453
|
+
if (left.length > 0) {
|
|
454
|
+
throw new Error(`cannot remove: worktrees still registered: ${left.join(", ")}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// 2. Delete .tickmarkr state directory
|
|
458
|
+
if (existsSync(stateDir)) {
|
|
459
|
+
rmSync(stateDir, { recursive: true, force: true });
|
|
460
|
+
notes.push(`deleted state directory ${stateDir}`);
|
|
461
|
+
}
|
|
462
|
+
// 3. Delete shipped skill directories at every host location
|
|
463
|
+
const targetSkillDirs = [
|
|
464
|
+
join(cwd, ".agents", "skills"),
|
|
465
|
+
join(cwd, ".claude", "skills"),
|
|
466
|
+
];
|
|
467
|
+
for (const dir of targetSkillDirs) {
|
|
468
|
+
for (const skill of AGENT_SKILLS) {
|
|
469
|
+
const skillPath = join(dir, skill);
|
|
470
|
+
if (existsSync(skillPath)) {
|
|
471
|
+
rmSync(skillPath, { recursive: true, force: true });
|
|
472
|
+
notes.push(`deleted skill ${skillPath}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// 4. Delete marker-bounded guidance block in AGENTS.md and CLAUDE.md leaving every byte outside identical
|
|
477
|
+
const marked = new RegExp(`<!-- (?:tickmarkr|${LEGACY_PREFIX}):agent-docs begin -->[\\s\\S]*?`
|
|
478
|
+
+ `<!-- (?:tickmarkr|${LEGACY_PREFIX}):agent-docs end -->`);
|
|
479
|
+
for (const doc of ["AGENTS.md", "CLAUDE.md"]) {
|
|
480
|
+
const docPath = join(cwd, doc);
|
|
481
|
+
if (existsSync(docPath)) {
|
|
482
|
+
const current = readFileSync(docPath, "utf8");
|
|
483
|
+
const m = current.match(marked);
|
|
484
|
+
if (m && m.index !== undefined) {
|
|
485
|
+
const before = current.slice(0, m.index);
|
|
486
|
+
const after = current.slice(m.index + m[0].length);
|
|
487
|
+
writeFileSync(docPath, before + after);
|
|
488
|
+
notes.push(`removed guidance block from ${docPath}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// 5. Delete scaffold spec only while it still equals the shipped template
|
|
493
|
+
const specPath = join(cwd, SCAFFOLD_SPEC);
|
|
494
|
+
if (existsSync(specPath)) {
|
|
495
|
+
const currentSpec = readFileSync(specPath, "utf8");
|
|
496
|
+
if (currentSpec === specTemplate()) {
|
|
497
|
+
rmSync(specPath, { force: true });
|
|
498
|
+
notes.push(`deleted scaffold spec ${specPath}`);
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
notes.push(`kept edited spec ${specPath}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// 6. Query and print remaining branches and preserved refs
|
|
505
|
+
let branches = [];
|
|
506
|
+
try {
|
|
507
|
+
const out = removeGit(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads/tickmarkr/"]);
|
|
508
|
+
branches = out.split("\n").map((b) => b.trim()).filter(Boolean);
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
// ignore
|
|
512
|
+
}
|
|
513
|
+
let preservedRefs = [];
|
|
514
|
+
try {
|
|
515
|
+
const out = removeGit(cwd, ["for-each-ref", "--format=%(refname)", "refs/tickmarkr/preserved/"]);
|
|
516
|
+
preservedRefs = out.split("\n").map((r) => r.trim()).filter(Boolean);
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
// ignore
|
|
520
|
+
}
|
|
521
|
+
for (const b of branches) {
|
|
522
|
+
notes.push(`left branch: ${b}`);
|
|
523
|
+
}
|
|
524
|
+
for (const r of preservedRefs) {
|
|
525
|
+
notes.push(`left ref: ${r}`);
|
|
526
|
+
}
|
|
527
|
+
return notes.join("\n");
|
|
528
|
+
}
|
|
343
529
|
export async function init(argv, cwd = process.cwd(), io = {}) {
|
|
344
530
|
let bannerEmitted = false;
|
|
345
531
|
const emitBanner = () => {
|
|
@@ -348,6 +534,8 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
|
|
|
348
534
|
process.stdout.write(BANNER);
|
|
349
535
|
}
|
|
350
536
|
};
|
|
537
|
+
// banner at START on the visual surface — every init path, not just the wizard, and never trailing the probe (operator report 2026-07-17)
|
|
538
|
+
emitBanner();
|
|
351
539
|
const { values } = parseArgs({
|
|
352
540
|
args: argv,
|
|
353
541
|
options: {
|
|
@@ -357,13 +545,15 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
|
|
|
357
545
|
docs: { type: "boolean" },
|
|
358
546
|
fresh: { type: "boolean" },
|
|
359
547
|
yes: { type: "boolean" },
|
|
548
|
+
remove: { type: "boolean" },
|
|
360
549
|
},
|
|
361
550
|
});
|
|
362
|
-
// banner at START on the visual surface — every init path, not just the wizard, and never trailing the probe (operator report 2026-07-17)
|
|
363
|
-
emitBanner();
|
|
364
551
|
if (packageName(cwd) === "tickmarkr") {
|
|
365
552
|
throw new Error("OBS-584: tickmarkr init refuses to run inside the tickmarkr source repository");
|
|
366
553
|
}
|
|
554
|
+
if (values.remove) {
|
|
555
|
+
return handleRemove(cwd);
|
|
556
|
+
}
|
|
367
557
|
const gdir = values["global-dir"] ?? globalConfigDir();
|
|
368
558
|
mkdirSync(gdir, { recursive: true });
|
|
369
559
|
const notes = [];
|
|
@@ -7,7 +7,7 @@ import { GLYPHS, dim, rule, title, warn } from "../../brand.js";
|
|
|
7
7
|
import { collateralLints, sourceScopeLints } from "../../compile/collateral.js";
|
|
8
8
|
import { classifyContextPath } from "../../compile/native.js";
|
|
9
9
|
import { DEFAULT_CONFIG, effectiveReviewPolicy, overlayPreferShapes, ROUTING_MODES, TIER_RANK } from "../../config/config.js";
|
|
10
|
-
import { graphDefinitionHash, loadGraph, stateDirName } from "../../graph/graph.js";
|
|
10
|
+
import { chainDepth, dispatchWaves, graphDefinitionHash, loadGraph, stateDirName } from "../../graph/graph.js";
|
|
11
11
|
import { renderAcceptanceItem } from "../../graph/schema.js";
|
|
12
12
|
import { resolveRunMode } from "../../run/daemon.js";
|
|
13
13
|
import { disallowedBy, excludedChannels, exclusionLine, routingEntrySeatLines } from "../../route/preference.js";
|
|
@@ -364,11 +364,27 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
364
364
|
: "";
|
|
365
365
|
return { line: `${channelKey(reviewer)}${rotationNote} — reviewPolicy full`, cost: seatCost(reviewer) };
|
|
366
366
|
};
|
|
367
|
+
// OBS-1018: depth and wave come from the same functions the daemon admits by, so the plan shows the
|
|
368
|
+
// critical path the run will actually follow at this concurrency.
|
|
369
|
+
const depths = chainDepth(g);
|
|
370
|
+
const waves = dispatchWaves(g, cfg.concurrency);
|
|
371
|
+
const byId = new Map(g.tasks.map((t) => [t.id, t]));
|
|
372
|
+
const chainLine = (t) => {
|
|
373
|
+
const wave = waves.get(t.id);
|
|
374
|
+
// Map absence never implies status: a pending task stays wave-less when a dependency is
|
|
375
|
+
// running/gated/failed/human, so render the real status and name the blocking deps.
|
|
376
|
+
const tail = wave !== undefined
|
|
377
|
+
? `wave ${wave} at concurrency ${cfg.concurrency}`
|
|
378
|
+
: t.status !== "pending"
|
|
379
|
+
? `no wave (${t.status})`
|
|
380
|
+
: `no wave (pending; blocked by ${t.deps.filter((d) => byId.get(d)?.status !== "done").map((d) => `${d}:${byId.get(d)?.status ?? "missing"}`).join(", ")})`;
|
|
381
|
+
return ` chain: depth ${depths.get(t.id)} · ${tail}`;
|
|
382
|
+
};
|
|
367
383
|
const routed = [];
|
|
368
384
|
for (const t of g.tasks) {
|
|
369
385
|
const refusals = [...(oracleRefusals.get(t.id) ?? []), ...(inputRefusals.get(t.id) ?? [])];
|
|
370
386
|
if (refusals.length) {
|
|
371
|
-
lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} !! pre-dispatch refusal — ${refusals.join("; ")}
|
|
387
|
+
lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} !! pre-dispatch refusal — ${refusals.join("; ")}`, chainLine(t));
|
|
372
388
|
continue;
|
|
373
389
|
}
|
|
374
390
|
try {
|
|
@@ -382,7 +398,7 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
382
398
|
const judge = judgeSeat(t);
|
|
383
399
|
const review = reviewSeat(t, r.assignment);
|
|
384
400
|
cost += est + judge.cost + review.cost;
|
|
385
|
-
lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} c${String(t.complexity).padEnd(3)}→ ${r.assignment.adapter}:${r.assignment.model} [${r.assignment.channel}/${r.assignment.tier}]${t.timeoutMinutes !== undefined ? ` (timeout ${t.timeoutMinutes}m)` : ""}${t.humanGate ? " (human gate)" : ""}${est ? ` ~$${est.toFixed(2)}` : ""} — ${r.provenance}`, ` judge: ${judge.line}${judge.cost ? ` ~$${judge.cost.toFixed(2)}` : ""}`, ` review: ${review.line}${review.cost ? ` ~$${review.cost.toFixed(2)}` : ""}
|
|
401
|
+
lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} c${String(t.complexity).padEnd(3)}→ ${r.assignment.adapter}:${r.assignment.model} [${r.assignment.channel}/${r.assignment.tier}]${t.timeoutMinutes !== undefined ? ` (timeout ${t.timeoutMinutes}m)` : ""}${t.humanGate ? " (human gate)" : ""}${est ? ` ~$${est.toFixed(2)}` : ""} — ${r.provenance}`, ` judge: ${judge.line}${judge.cost ? ` ~$${judge.cost.toFixed(2)}` : ""}`, ` review: ${review.line}${review.cost ? ` ~$${review.cost.toFixed(2)}` : ""}`, chainLine(t));
|
|
386
402
|
const d = derivation(t.shape);
|
|
387
403
|
if (d)
|
|
388
404
|
lines.push(d);
|
|
@@ -406,7 +422,7 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
406
422
|
if (!(e instanceof RoutingError))
|
|
407
423
|
throw e;
|
|
408
424
|
const msg = `${e.message}${exclusionReason(e.message)}`;
|
|
409
|
-
lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} !! ${msg}
|
|
425
|
+
lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} !! ${msg}`, chainLine(t));
|
|
410
426
|
const d = derivation(t.shape);
|
|
411
427
|
if (d)
|
|
412
428
|
lines.push(d);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { loadConfig } from "../../config/config.js";
|
|
3
|
-
import { parseDriverOverride, pickDriver } from "../../drivers/index.js";
|
|
3
|
+
import { classifyHost, parseDriverOverride, pickDriver, preflightHostDriver } from "../../drivers/index.js";
|
|
4
4
|
import { loadGraph } from "../../graph/graph.js";
|
|
5
5
|
import { formatSummary, runDaemon } from "../../run/daemon.js";
|
|
6
6
|
import { denyPreferCollisionLine, denyPreferCollisions } from "../../route/preference.js";
|
|
@@ -38,6 +38,8 @@ export async function resume(argv, cwd = process.cwd()) {
|
|
|
38
38
|
throw new Error(collisions.map(denyPreferCollisionLine).join("; "));
|
|
39
39
|
}
|
|
40
40
|
await assertRefsWritable(cwd, "resume");
|
|
41
|
+
const host = classifyHost();
|
|
42
|
+
preflightHostDriver(cfg, driverOverride, host);
|
|
41
43
|
const narrate = narrationSink(runId);
|
|
42
44
|
const s = await runDaemon(cwd, {
|
|
43
45
|
runId,
|
|
@@ -45,7 +47,7 @@ export async function resume(argv, cwd = process.cwd()) {
|
|
|
45
47
|
graphChanged,
|
|
46
48
|
retryFailed,
|
|
47
49
|
// bound to the same sink the daemon gets, so a driver-journaled recovery reaches this rail too
|
|
48
|
-
driver: bindNarration(pickDriver(cfg, driverOverride), narrate),
|
|
50
|
+
driver: bindNarration(pickDriver(cfg, driverOverride, host), narrate),
|
|
49
51
|
// v1.99 T2: the ONE narration sink — the quiet rail on a TTY, the raw journal formatter on a
|
|
50
52
|
// pipe. A resumed run meets the same surface a fresh one does; printing the raw formatter here
|
|
51
53
|
// would leave `resume` as the last place the old unfiltered dump survives. Bound to the run id
|