tickmarkr 2.6.4 → 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/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/model-discovery-check.d.ts +33 -0
- package/dist/adapters/model-discovery-check.js +123 -0
- package/dist/adapters/model-lints.d.ts +1 -1
- package/dist/adapters/model-lints.js +4 -0
- package/dist/adapters/prompt.js +3 -2
- package/dist/adapters/types.d.ts +7 -0
- package/dist/cli/commands/approve.js +10 -4
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +14 -2
- package/dist/cli/commands/fleet.d.ts +10 -1
- package/dist/cli/commands/fleet.js +55 -38
- package/dist/cli/commands/init.js +5 -1
- package/dist/cli/commands/plan.js +65 -4
- package/dist/cli/commands/verify.d.ts +1 -1
- package/dist/cli/commands/verify.js +28 -6
- 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/run/daemon.js +150 -19
- package/dist/run/git.js +9 -0
- package/dist/run/journal.d.ts +6 -2
- package/dist/run/journal.js +22 -7
- 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 +24 -1
- package/dist/tui/cockpit/live-store.js +99 -8
- 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 +32 -2
- package/skills/tickmarkr-loop/SKILL.md +46 -2
- package/skills/tickmarkr-overseer/SKILL.md +83 -8
|
@@ -363,20 +363,34 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
363
363
|
const channels = discoverChannels(cfg, adapters, health);
|
|
364
364
|
const profile = loadRoutingProfile(cwd, cfg, { preview: true });
|
|
365
365
|
const stagedDenyOf = (editable) => Object.fromEntries(DENY_SCOPES.map((scope) => [stagedDenyKeyOf(scope), editable[scope.key] ?? []]));
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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).
|
|
371
384
|
let reviewedStage = { tiers: initial.tiers, efforts: initial.efforts };
|
|
372
385
|
// OBS-1182: the layers under the written overlay — B2: the defaults alone under the USER overlay —
|
|
373
386
|
// read raw, so clearing an effort masks an inherited one. OBS-1188: re-read at every review and
|
|
374
387
|
// save, never an assembly-time snapshot; an unreadable read keeps the last good one for the
|
|
375
388
|
// preview only — the save guard below refuses it.
|
|
376
389
|
let lowerOverrides = {};
|
|
377
|
-
const stagedEditable = (map, deny) => ({
|
|
390
|
+
const stagedEditable = (map, deny, metadata) => ({
|
|
378
391
|
...structuredClone(initial),
|
|
379
|
-
|
|
392
|
+
efforts: undefined, // metadata carries efforts only when some are staged — as a review writes them
|
|
393
|
+
...structuredClone(metadata),
|
|
380
394
|
...Object.fromEntries(DENY_SCOPES.map((scope) => [scope.key, deny[stagedDenyKeyOf(scope)]])),
|
|
381
395
|
// OBS-1046: the allow complement is staged beside the deny lists, never folded into them
|
|
382
396
|
allowOut: deny.allowOut ?? initial.allowOut,
|
|
@@ -424,14 +438,17 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
424
438
|
};
|
|
425
439
|
// B2: a changed slot staged with no declaration (auto, no prefer) where the defaults under the user
|
|
426
440
|
// 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
|
|
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.
|
|
428
444
|
const maskClearedSlots = (base, staged) => {
|
|
429
445
|
const map = { ...staged.map };
|
|
430
446
|
for (const shape of new Set([...Object.keys(base.map), ...Object.keys(staged.map)])) {
|
|
431
447
|
const entry = staged.map[shape];
|
|
448
|
+
const lower = mapSlot(DEFAULT_CONFIG.routing.map[shape]);
|
|
432
449
|
if (JSON.stringify(base.map[shape]) === JSON.stringify(entry))
|
|
433
450
|
continue;
|
|
434
|
-
if (Object.keys(mapSlot(entry)).length || !Object.keys(
|
|
451
|
+
if (Object.keys(mapSlot(entry)).length || !Object.keys(lower).length || lower.pin)
|
|
435
452
|
continue;
|
|
436
453
|
map[shape] = { ...entry, prefer: [] };
|
|
437
454
|
}
|
|
@@ -449,6 +466,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
449
466
|
try {
|
|
450
467
|
const bytes = renderFleetOverlayWrite(userText, {
|
|
451
468
|
...rest, initial: destinationInitial(initial, unchanged, userText, universe), edited: unchanged, universe,
|
|
469
|
+
lowerMap: DEFAULT_CONFIG.routing.map,
|
|
452
470
|
});
|
|
453
471
|
const candidate = loadConfigWithMode(cwd, { globalDir, userOverlayText: bytes }).cfg;
|
|
454
472
|
target = universeOf(candidateChannels(candidate));
|
|
@@ -461,20 +479,23 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
461
479
|
// the full render below (or the reload guard) names the refusal
|
|
462
480
|
}
|
|
463
481
|
}
|
|
464
|
-
|
|
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 };
|
|
465
484
|
};
|
|
466
485
|
const candidateMemo = new Map();
|
|
467
|
-
const previewCfg = (mode, map, deny) => {
|
|
486
|
+
const previewCfg = (mode, map, deny, stage) => {
|
|
468
487
|
// 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
|
|
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;
|
|
470
491
|
const userText = currentUserText();
|
|
471
|
-
const key = JSON.stringify([mode, map, deny, userText, currentRepoOverlayText(cwd)]);
|
|
492
|
+
const key = JSON.stringify([mode, map, deny, metadata, userText, currentRepoOverlayText(cwd)]);
|
|
472
493
|
const hit = candidateMemo.get(key);
|
|
473
494
|
if (hit)
|
|
474
495
|
return hit;
|
|
475
496
|
let preview;
|
|
476
497
|
try {
|
|
477
|
-
const bytes = renderFleetOverlayWrite(userText, userLayerWrite(stagedEditable(map, deny), userText, {
|
|
498
|
+
const bytes = renderFleetOverlayWrite(userText, userLayerWrite(stagedEditable(map, deny, metadata), userText, {
|
|
478
499
|
lowerOverrides,
|
|
479
500
|
...(mode !== rm.mode.mode ? { mode } : {}),
|
|
480
501
|
}));
|
|
@@ -509,12 +530,12 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
509
530
|
// route() never deny-filters its pool — that contract lives in discoverChannels — so every
|
|
510
531
|
// preview call rebuilds the pool the staged deny would discover
|
|
511
532
|
const previewPool = (cfgPreview) => candidateChannels(cfgPreview).filter((c) => disallowedBy(c, cfgPreview.routing) === null);
|
|
512
|
-
const modeSpend = (mode, map, deny) => {
|
|
533
|
+
const modeSpend = (mode, map, deny, stage) => {
|
|
513
534
|
const tierCount = {};
|
|
514
535
|
let subs = 0;
|
|
515
536
|
let apiN = 0;
|
|
516
537
|
let apiUsd = 0;
|
|
517
|
-
const preview = previewCfg(mode, map, deny);
|
|
538
|
+
const preview = previewCfg(mode, map, deny, stage);
|
|
518
539
|
if (!preview.ok)
|
|
519
540
|
return ` mix: ${previewUnavailable(preview.error)}`;
|
|
520
541
|
const cfgPreview = preview.cfg;
|
|
@@ -547,10 +568,10 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
547
568
|
};
|
|
548
569
|
// B2: the floors the candidate user-layer bytes resolve to — the same config the routed mix ranks
|
|
549
570
|
// against, so a repo-declared mode that shadows the user's pick previews no floor change
|
|
550
|
-
const floorPreview = (mode, map, deny) => {
|
|
571
|
+
const floorPreview = (mode, map, deny, stage) => {
|
|
551
572
|
if (mode === rm.mode.mode)
|
|
552
573
|
return [];
|
|
553
|
-
const preview = previewCfg(mode, map, deny);
|
|
574
|
+
const preview = previewCfg(mode, map, deny, stage);
|
|
554
575
|
if (!preview.ok)
|
|
555
576
|
return []; // modeSpend names the refusal
|
|
556
577
|
const current = cfg.routing.floors;
|
|
@@ -585,8 +606,8 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
585
606
|
? { declaredAt: `routing.floors.${shape}` }
|
|
586
607
|
: { declaredAt: "routing.mode" };
|
|
587
608
|
};
|
|
588
|
-
const projectedShapeRows = (mode, map, deny) => {
|
|
589
|
-
const preview = previewCfg(mode, map, deny);
|
|
609
|
+
const projectedShapeRows = (mode, map, deny, stage) => {
|
|
610
|
+
const preview = previewCfg(mode, map, deny, stage);
|
|
590
611
|
if (!preview.ok) {
|
|
591
612
|
return projectFleetWhy(SHAPES.map((shape) => ({
|
|
592
613
|
id: shape,
|
|
@@ -611,7 +632,9 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
611
632
|
// a pin. The declaration is the operator's decision; the winner is today's dice.
|
|
612
633
|
const declaredPool = entry?.pool;
|
|
613
634
|
const poolPrefix = declaredPool ? `pool(${declaredPool.mode}·${declaredPool.channels.length}) → ` : "";
|
|
614
|
-
|
|
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}`;
|
|
615
638
|
const mapProducedValue = entry?.pin !== undefined || entry?.pool !== undefined
|
|
616
639
|
|| routed.provenance.includes("via prefer");
|
|
617
640
|
return {
|
|
@@ -633,7 +656,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
633
656
|
});
|
|
634
657
|
return projectFleetWhy(values, { repoRoot: cwd, globalDir });
|
|
635
658
|
};
|
|
636
|
-
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 }));
|
|
637
660
|
// OBS-530: the picker silently omitted every excluded channel — the operator asked "why is X
|
|
638
661
|
// missing" three times in one session and the answer lived only in config archaeology. One dim
|
|
639
662
|
// line now names each bucket. Static buckets (unauthed, unclassified) computed once; the
|
|
@@ -802,8 +825,8 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
802
825
|
const order = (key) => (carried.includes(key) ? carried.indexOf(key) : carried.length);
|
|
803
826
|
return [...lines, ...unnamed].sort((a, b) => order(a.key) - order(b.key)).map((row) => row.line);
|
|
804
827
|
};
|
|
805
|
-
const candidatesForShape = (shape, mode, map, deny) => {
|
|
806
|
-
const preview = previewCfg(mode, map, deny);
|
|
828
|
+
const candidatesForShape = (shape, mode, map, deny, stage) => {
|
|
829
|
+
const preview = previewCfg(mode, map, deny, stage);
|
|
807
830
|
if (!preview.ok)
|
|
808
831
|
return { rows: [], excludedNote: previewUnavailable(preview.error) };
|
|
809
832
|
const cfgPreview = preview.cfg;
|
|
@@ -882,21 +905,13 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
882
905
|
staged[scope.key] = state[scope.key];
|
|
883
906
|
staged.allowOut = state.allowOut ?? initial.allowOut;
|
|
884
907
|
staged.map = state.map;
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
if (
|
|
888
|
-
staged.efforts = efforts;
|
|
908
|
+
const metadata = stagedMetadataOf(state);
|
|
909
|
+
staged.tiers = metadata.tiers;
|
|
910
|
+
if (metadata.efforts)
|
|
911
|
+
staged.efforts = metadata.efforts;
|
|
889
912
|
else
|
|
890
913
|
delete staged.efforts;
|
|
891
|
-
|
|
892
|
-
for (const classification of state.classifications) {
|
|
893
|
-
staged.tiers[classification.adapter] ??= {};
|
|
894
|
-
staged.tiers[classification.adapter][classification.model] = {
|
|
895
|
-
tier: classification.tier,
|
|
896
|
-
provenance: `${classification.note} — fleet ${today}`,
|
|
897
|
-
};
|
|
898
|
-
}
|
|
899
|
-
reviewedStage = { tiers: staged.tiers, efforts: staged.efforts };
|
|
914
|
+
reviewedStage = metadata;
|
|
900
915
|
// This callback is the sole candidate-overlay builder. The Ink component renders
|
|
901
916
|
// the diff and asks for confirmation, but owns neither filesystem access nor a writer.
|
|
902
917
|
// B2: defaults/user/repo are re-read here, at review — the destination is the user overlay.
|
|
@@ -971,7 +986,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
971
986
|
initialMode: rm.mode.mode,
|
|
972
987
|
modeOptions: ROUTING_MODES.map((mode) => ({ id: mode, gloss: MODE_GLOSS[mode] })),
|
|
973
988
|
initialMap: editable.map,
|
|
974
|
-
modePreview: (mode, map, deny) => [modeSpend(mode, map, deny), ...floorPreview(mode, map, deny)],
|
|
989
|
+
modePreview: (mode, map, deny, stage) => [modeSpend(mode, map, deny, stage), ...floorPreview(mode, map, deny, stage)],
|
|
975
990
|
shapeRows: routedShapeRows,
|
|
976
991
|
candidatesForShape,
|
|
977
992
|
preferOptionsForShape: (_shape, current) => [
|
|
@@ -1050,5 +1065,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
1050
1065
|
props,
|
|
1051
1066
|
commit,
|
|
1052
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,
|
|
1053
1070
|
};
|
|
1054
1071
|
}
|
|
@@ -132,7 +132,11 @@ you were handed the whole picture — list the repository root, open every guida
|
|
|
132
132
|
- **The gates are the product.** Seven, defined in \`src/graph/schema.ts\`:
|
|
133
133
|
\`build test lint evidence scope acceptance review\`. **That is DECLARATION order, not execution order** —
|
|
134
134
|
the first five run as a battery that stops at its first red, then \`acceptance\` and \`review\` run
|
|
135
|
-
CONCURRENTLY (\`run-gates.ts:39\`, *"judge ‖ review"*).
|
|
135
|
+
CONCURRENTLY (\`run-gates.ts:39\`, *"judge ‖ review"*). One repair reorders them: a repair the REVIEW sent
|
|
136
|
+
back (carried \`review:material\`, test selection on, no required repair test) runs build, lint, evidence and
|
|
137
|
+
scope, then \`acceptance\` ‖ \`review\`, then its test screen, so a review that re-reds an unchanged subject
|
|
138
|
+
starts no test at all; a test-red repair keeps the battery first. Either way nothing merges until the FULL
|
|
139
|
+
suite has passed on the merge-candidate commit. The first five are MANDATORY; only \`acceptance\` and
|
|
136
140
|
\`review\` may be omitted per task. Implementations are in \`src/gates/\` — \`baseline.ts\` (build/test/lint,
|
|
137
141
|
diffed against a recorded baseline so pre-existing failures are forgiven), \`evidence.ts\`, \`scope.ts\`,
|
|
138
142
|
\`acceptance.ts\` (its judge reads the DIFF and every criterion must cite a changed hunk), \`review.ts\`
|
|
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
import { allAdapters, doctorAgeMs, modelAuthExclusions, probeAll, readDoctor, rolePools, servableExclusions, servabilityLine } from "../../adapters/registry.js";
|
|
5
|
+
import { aliasIdentityHeadlines } from "../../adapters/model-discovery-check.js";
|
|
5
6
|
import { formatModelAuthLine, contextWindowLints, modelLints, preferEntryLints, ttyVisual } from "../../adapters/model-lints.js";
|
|
6
7
|
import { GLYPHS, dim, rule, title, warn } from "../../brand.js";
|
|
7
8
|
import { collateralLints, sourceScopeLints } from "../../compile/collateral.js";
|
|
@@ -15,9 +16,10 @@ import { disallowedBy, excludedChannels, exclusionLine, observedSeat, routingEnt
|
|
|
15
16
|
import { decayWeight, HALF_LIFE_RUNS, staffLedEvidence } from "../../route/profile.js";
|
|
16
17
|
import { resolvedFloor, route, RoutingError } from "../../route/router.js";
|
|
17
18
|
import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
|
|
18
|
-
import { modelId, modelProvider, pickReviewer } from "../../gates/review.js";
|
|
19
|
+
import { carriedAuthorVendors, gateReviewerFloor, modelId, modelProvider, pickReviewer } from "../../gates/review.js";
|
|
19
20
|
import { Journal, loadRoutingProfile, readProfileCursor, recordedGraphDefinitionHash, RUNS_WINDOW } from "../../run/journal.js";
|
|
20
21
|
import { harnessLine, resolveHarness } from "../harness.js";
|
|
22
|
+
import { codexCommitHeadline } from "../../adapters/codex-commit-check.js";
|
|
21
23
|
import { channelKey, shq } from "../../adapters/types.js";
|
|
22
24
|
import { shGit } from "../../run/git.js";
|
|
23
25
|
import { runLockRunId, runStatusLine } from "../../run/lock.js";
|
|
@@ -135,6 +137,13 @@ async function taskInputFindings(tasks, cwd) {
|
|
|
135
137
|
}
|
|
136
138
|
return findings;
|
|
137
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* v2.6.5 T9 (D-862): a task the run never dispatched gates under the fresh route's author, so plan can
|
|
142
|
+
* mirror its review eligibility exactly. Once dispatched, resume state only the daemon decides (a restored
|
|
143
|
+
* or failed-over author, a tier climb, carried commits, a parked recheck) picks the author — fail closed.
|
|
144
|
+
* ponytail: unknown, not exact; call production's own resume functions once daemon.ts can export them (POOL).
|
|
145
|
+
*/
|
|
146
|
+
const resumeDecidesAuthor = (events, taskId) => events.some((row) => row.taskId === taskId && row.event === "task-dispatch");
|
|
138
147
|
// v1.89 T4: harnessFrom is the resolver's INPUT — a caller (the byte-pinned goldens) fixes the location
|
|
139
148
|
// and keeps this machine's absolute paths out of a fixture. The default is the INVOKED entrypoint,
|
|
140
149
|
// `process.argv[1]`: the bin symlink a global install puts on PATH, which resolves to dist/cli/index.js.
|
|
@@ -183,6 +192,26 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
183
192
|
.filter((event) => event.event === "gate-result" && event.data.gate === "review" && typeof event.data.reviewer === "string")
|
|
184
193
|
.map((event) => event.data.reviewer)
|
|
185
194
|
: [];
|
|
195
|
+
// daemon.ts replayedReviewerExclusions: a journaled review-pool-demotion is excluded from every review
|
|
196
|
+
// pick of the resumed run this plan previews (reviewer role only).
|
|
197
|
+
const replayedReviewerExclusions = [...new Set((matchingEvents ?? [])
|
|
198
|
+
.filter((event) => event.event === "review-pool-demotion" && typeof event.data.reviewer === "string")
|
|
199
|
+
.map((event) => event.data.reviewer))];
|
|
200
|
+
const demotedReviewers = new Set(replayedReviewerExclusions);
|
|
201
|
+
// daemon.ts demotedChannels: the resumed run's worker exclusions (journal replayExcludedChannels).
|
|
202
|
+
const runJournal = matchingRunId ? Journal.open(cwd, matchingRunId) : undefined;
|
|
203
|
+
const demotedWorkers = runJournal?.replayExcludedChannels() ?? new Set();
|
|
204
|
+
// run-gates.ts retired (OBS-1025 add.2): seats with two no-verdicts this run are out of every pick, tallied
|
|
205
|
+
// as daemon.ts seeds reviewNoVerdicts — noVerdict rows only, and a checkout-proof timeout never strikes.
|
|
206
|
+
const noVerdicts = new Map();
|
|
207
|
+
for (const event of matchingEvents ?? []) {
|
|
208
|
+
if (event.event !== "review-no-verdict" || event.data.noVerdict !== true || typeof event.data.reviewer !== "string")
|
|
209
|
+
continue;
|
|
210
|
+
if (event.data.cause === "seat-launch-failed" && event.data.launchCause === "checkout-proof-timeout")
|
|
211
|
+
continue;
|
|
212
|
+
noVerdicts.set(event.data.reviewer, (noVerdicts.get(event.data.reviewer) ?? 0) + 1);
|
|
213
|
+
}
|
|
214
|
+
const retiredReviewers = [...noVerdicts].filter(([, n]) => n >= 2).map(([seat]) => seat);
|
|
186
215
|
// OBS-947 repair: a resumed run re-dispatches only tasks whose replayed status is pending (or that the
|
|
187
216
|
// journal never mentions). done, failed and human are all skipped alike, so rendering their preview
|
|
188
217
|
// picks must not advance the shared reviewHistory — a phantom draw wraps the LRU and makes a later
|
|
@@ -243,6 +272,10 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
243
272
|
if (judgeDeny?.by === "deny") {
|
|
244
273
|
lines.push(`REFUSAL OBS-576: judge seat ${cfg.judge.adapter}:${cfg.judge.model} is removed by flat routing.deny (${judgeDeny.entry}) — move worker-only policy to routing.deny.workers`, "");
|
|
245
274
|
}
|
|
275
|
+
// v2.6.5 T9 (H): alias drift is read from the identity doctor persisted, never re-probed here.
|
|
276
|
+
const aliasHeadlines = aliasIdentityHeadlines(cfg, health);
|
|
277
|
+
if (aliasHeadlines.length)
|
|
278
|
+
lines.push(...aliasHeadlines, "");
|
|
246
279
|
const entrySeats = routingEntrySeatLines(cfg);
|
|
247
280
|
if (entrySeats.length)
|
|
248
281
|
lines.push(...entrySeats, "");
|
|
@@ -359,9 +392,36 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
359
392
|
return { line: "none — reviewPolicy judge-only: declared leaf paths", cost: 0 };
|
|
360
393
|
let rotationSeat = 1;
|
|
361
394
|
let rotationCount = 0;
|
|
362
|
-
|
|
395
|
+
// Dispatched before: no count, no single-seat claim, no previewed reviewer (and so no rotation draw this
|
|
396
|
+
// plan could name). Never a guess.
|
|
397
|
+
if (resumeDecidesAuthor(matchingEvents ?? [], task.id)) {
|
|
398
|
+
lints.push(`${task.id}: eligible cross-vendor review seat count unknown — resume state decides the author`);
|
|
399
|
+
return { line: "unknown — resume state decides the author", cost: 0 };
|
|
400
|
+
}
|
|
401
|
+
// Never dispatched, daemon.ts subjectAuthors is just the fresh author; its vendor stays off the review.
|
|
402
|
+
const authors = [channelKey(author)];
|
|
403
|
+
const authorVendors = carriedAuthorVendors(pools.review, authors);
|
|
404
|
+
// Production's inputs as the task's first review round sees them: excludeReviewers = daemon badReviewers =
|
|
405
|
+
// replayedReviewerExclusions. run-gates.ts:1303 floors on THIS task's journaled reviewers at their dispatched
|
|
406
|
+
// tiers (daemon taskReviewers) ∪ excludeReviewers, so a replayed exclusion RAISES the floor; :1305 picks
|
|
407
|
+
// with excludeReviewers ∪ retired. prefer only reorders (the seat count below passes none).
|
|
408
|
+
const priorReviewers = [...(matchingEvents ?? []).filter((e) => e.taskId === task.id && typeof e.data.reviewer === "string"
|
|
409
|
+
&& ((e.event === "gate-result" && e.data.gate === "review") || e.event === "review-no-verdict"))
|
|
410
|
+
.map((e) => ({ reviewer: e.data.reviewer, tier: e.data.reviewerTier })), ...replayedReviewerExclusions];
|
|
411
|
+
const reviewFloor = gateReviewerFloor(task, cfg, author, pools.review, priorReviewers).floor;
|
|
412
|
+
const exclusions = [...replayedReviewerExclusions, ...retiredReviewers];
|
|
413
|
+
const reviewer = pickReviewer(author, pools.review, exclusions, cfg.review.prefer ?? [], reviewFloor, reviewHistory, (seat, count) => { rotationSeat = seat; rotationCount = count; }, demotedReviewers, authorVendors, authors);
|
|
363
414
|
if (!reviewer)
|
|
364
415
|
return { line: "none — no cross-vendor reviewer available", cost: 0 };
|
|
416
|
+
// v2.6.5 T9 (H): one flaked or demoted seat away from no reviewer. Enumerate the production filter
|
|
417
|
+
// (pickReviewer at the gate's floor) and count served identities, so two aliases of one model count once.
|
|
418
|
+
const seats = [];
|
|
419
|
+
for (let s; (s = pickReviewer(author, pools.review, [...exclusions, ...seats.map(channelKey)], [], reviewFloor, [], undefined, demotedReviewers, authorVendors, authors));)
|
|
420
|
+
seats.push(s);
|
|
421
|
+
const identities = new Set(seats.map((c) => c.identity ?? modelId(c.model)));
|
|
422
|
+
if (identities.size === 1) {
|
|
423
|
+
lints.push(`${task.id}: exactly one eligible cross-vendor review seat remains — identity ${[...identities][0]} via ${seats.map(channelKey).join(", ")}; one flake or demotion leaves no reviewer`);
|
|
424
|
+
}
|
|
365
425
|
// Advance the simulated rotation only for tasks the run would actually dispatch next — the same
|
|
366
426
|
// rule as readyTasks (src/graph/graph.ts): pending, with every dependency already done. A pending
|
|
367
427
|
// task behind a parked/failed/pending dependency cannot run, so it consumes no draw (RULING-229-14).
|
|
@@ -402,7 +462,8 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
402
462
|
continue;
|
|
403
463
|
}
|
|
404
464
|
try {
|
|
405
|
-
|
|
465
|
+
// daemon.ts:3322: the resumed run routes around its replayed worker exclusions.
|
|
466
|
+
const r = route(t, cfg, channels, dispatchProfile, undefined, demotedWorkers);
|
|
406
467
|
routed.push({ taskId: t.id, adapter: r.assignment.adapter, model: r.assignment.model });
|
|
407
468
|
for (const l of r.lints) {
|
|
408
469
|
const suf = exclusionReason(l);
|
|
@@ -412,7 +473,7 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
412
473
|
const judge = judgeSeat(t);
|
|
413
474
|
const review = reviewSeat(t, r.assignment);
|
|
414
475
|
cost += est + judge.cost + review.cost;
|
|
415
|
-
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));
|
|
476
|
+
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}`, ...[codexCommitHeadline(r.assignment.adapter, adapters, health)].filter((l) => l !== null), ` judge: ${judge.line}${judge.cost ? ` ~$${judge.cost.toFixed(2)}` : ""}`, ` review: ${review.line}${review.cost ? ` ~$${review.cost.toFixed(2)}` : ""}`, chainLine(t));
|
|
416
477
|
const d = derivation(t);
|
|
417
478
|
if (d)
|
|
418
479
|
lines.push(d);
|
|
@@ -7,7 +7,7 @@ export declare const HUMAN_AUTHOR: Assignment;
|
|
|
7
7
|
export declare function verifyStateDir(cwd: string): string;
|
|
8
8
|
/** Linked worktrees share operational state with the repository that owns the common git dir. */
|
|
9
9
|
export declare function verifyStateRoot(cwd: string): Promise<string>;
|
|
10
|
-
/**
|
|
10
|
+
/** Repository content plus the effective runner inputs the daemon hashes; never a checkout path. */
|
|
11
11
|
export declare function baselineCachePath(cwd: string, baseSha: string, commands: Record<string, string>): string;
|
|
12
12
|
export declare function excludeAuthorProvider(channels: BillingChannel[], author: BillingChannel): BillingChannel[];
|
|
13
13
|
export declare const VERIFY_HELP = "usage: tickmarkr verify [--base <ref>] [--criteria <file> | --task <id>] [--json]\nThe final verdict and JSON result are written to stdout; progress and diagnostics are written to stderr.\nDo not merge stdout and stderr (for example with 2>&1): doing so corrupts the verdict stream.";
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
|
-
import { allAdapters, probeAll, readDoctor, rolePools } from "../../adapters/registry.js";
|
|
6
|
+
import { allAdapters, probeAll, readDoctor, rolePools, writeDoctor } from "../../adapters/registry.js";
|
|
7
7
|
import { channelKey } from "../../adapters/types.js";
|
|
8
8
|
import { loadConfig } from "../../config/config.js";
|
|
9
9
|
import { captureBaseline, detectGateCommands, staleFileCountCommands } from "../../gates/baseline.js";
|
|
10
|
-
import { getVerdictStore } from "../../gates/cache.js";
|
|
10
|
+
import { getVerdictStore, runnerInputsHash } from "../../gates/cache.js";
|
|
11
11
|
import { modelProvider } from "../../gates/review.js";
|
|
12
12
|
import { runGates } from "../../gates/run-gates.js";
|
|
13
13
|
import { getTask, loadGraph } from "../../graph/graph.js";
|
|
@@ -17,6 +17,7 @@ import { linkNodeModules, removeWorktree, shGit, shGitOk } from "../../run/git.j
|
|
|
17
17
|
import { foldOwedChecks, integrationMapped, owedCriteria, Journal, OWED_DISCHARGE_EVENT } from "../../run/journal.js";
|
|
18
18
|
import { withRepositoryLease } from "../../run/lease.js";
|
|
19
19
|
import { isPidLive } from "../../run/lock.js";
|
|
20
|
+
import { recordLaunchServices } from "../../run/launchservices-check.js";
|
|
20
21
|
/**
|
|
21
22
|
* tickmarkr verify — the gate battery as a standalone command (OPERATING-MODEL-2026-08-11 item 3).
|
|
22
23
|
*
|
|
@@ -86,7 +87,7 @@ const hashParts = (parts) => {
|
|
|
86
87
|
hash.update(part);
|
|
87
88
|
return hash.digest("hex").slice(0, 12);
|
|
88
89
|
};
|
|
89
|
-
/**
|
|
90
|
+
/** Repository content plus the effective runner inputs the daemon hashes; never a checkout path. */
|
|
90
91
|
export function baselineCachePath(cwd, baseSha, commands) {
|
|
91
92
|
const lockParts = [];
|
|
92
93
|
for (const file of LOCKFILES) {
|
|
@@ -94,7 +95,7 @@ export function baselineCachePath(cwd, baseSha, commands) {
|
|
|
94
95
|
lockParts.push(file, existsSync(path) ? readFileSync(path) : "<absent>");
|
|
95
96
|
}
|
|
96
97
|
const commandParts = Object.entries(commands).sort(([a], [b]) => a.localeCompare(b)).map(([name, command]) => `${name}\0${command}\0`);
|
|
97
|
-
return join(realpathSync(tmpdir()), "tickmarkr-verify", "cache", `baseline-${baseSha.slice(0, 12)}-${hashParts(lockParts)}-${hashParts(commandParts)}.json`);
|
|
98
|
+
return join(realpathSync(tmpdir()), "tickmarkr-verify", "cache", `baseline-${baseSha.slice(0, 12)}-${hashParts(lockParts)}-${hashParts(commandParts)}-${runnerInputsHash()}.json`);
|
|
98
99
|
}
|
|
99
100
|
const verdictlessCommands = (baseline, commands) => Object.keys(commands).filter((name) => baseline.commands[name]?.exitCode === undefined);
|
|
100
101
|
export function excludeAuthorProvider(channels, author) {
|
|
@@ -378,7 +379,7 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
378
379
|
console.error(`verify: reusing cached baseline for ${mergeBase.slice(0, 12)} (${names.join(", ")}: ${cachePath})`);
|
|
379
380
|
return cached;
|
|
380
381
|
}
|
|
381
|
-
console.error(`verify: capturing baseline at merge-base ${mergeBase.slice(0, 12)} for ${names.join(", ")} (cached by base, lockfile and
|
|
382
|
+
console.error(`verify: capturing baseline at merge-base ${mergeBase.slice(0, 12)} for ${names.join(", ")} (cached by base, lockfile, command and effective runner hashes at ${cachePath})`);
|
|
382
383
|
// Unleased captures of the SAME checkout overlap, so each owns a unique base worktree and removes
|
|
383
384
|
// only its own; the pid in the name lets a later verify sweep what a killed one left behind.
|
|
384
385
|
for (const entry of readdirSync(stateDir)) {
|
|
@@ -577,6 +578,27 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
577
578
|
...review,
|
|
578
579
|
});
|
|
579
580
|
}
|
|
581
|
+
// A1 (D-787/D-789): one bounded launchservicesd identity read AFTER the verdict is final, so a suite
|
|
582
|
+
// that restarted the daemon is caught and the verdict, exit code and retries never depend on it.
|
|
583
|
+
// It rides the doctor.json verify read; with none there it creates no state file.
|
|
584
|
+
const doctorRoot = fileRoot("doctor.json");
|
|
585
|
+
const priorDoctor = readDoctor(doctorRoot);
|
|
586
|
+
const launchServices = priorDoctor ? await recordLaunchServices(priorDoctor, priorDoctor) : undefined;
|
|
587
|
+
if (launchServices && launchServices.health !== priorDoctor) {
|
|
588
|
+
// Evidence only, not a probe: keep doctor.json's mtime (the auth-cache freshness signal, doctorAgeMs),
|
|
589
|
+
// and a failed write is an advisory diagnostic — the verdict below is returned regardless.
|
|
590
|
+
const doctorFile = join(doctorRoot, ".tickmarkr", "doctor.json");
|
|
591
|
+
try {
|
|
592
|
+
const { atime, mtime } = statSync(doctorFile);
|
|
593
|
+
writeDoctor(doctorRoot, launchServices.health);
|
|
594
|
+
utimesSync(doctorFile, atime, mtime);
|
|
595
|
+
}
|
|
596
|
+
catch (e) {
|
|
597
|
+
console.error(`verify: launchservicesd evidence not persisted (${e instanceof Error ? e.message : String(e)}) (advisory — verdict unchanged)`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (launchServices?.line)
|
|
601
|
+
console.error(`verify: ${launchServices.line}`);
|
|
580
602
|
if (values.json) {
|
|
581
603
|
return { out: JSON.stringify({ base: baseTip, head, mergeBase, green, artifactPath, artifactSha256, artifactAvailability, results,
|
|
582
604
|
...(owedAfter && owedLines.length ? { owed: { debt: owedAfter.debt, discharged: dischargeRefusal ? [] : owed.map((o) => o.id), ...(dischargeRefusal ? { refused: dischargeRefusal } : {}) } } : {}),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type FleetEditable, type FleetUniverseRow, type LowerLayerModelOverrides, type RoutingMode } from "./config.js";
|
|
1
|
+
import { type FleetEditable, type FleetUniverseRow, type LowerLayerModelOverrides, type MapEntry, type RoutingMode } from "./config.js";
|
|
2
2
|
/** Fleet-owned overlay keys — the only config surface `tickmarkr fleet` may write. */
|
|
3
3
|
export declare const FLEET_OVERLAY_KEYS: readonly ["routing", "tiers"];
|
|
4
4
|
type FleetOverlayWriteFields = {
|
|
@@ -20,6 +20,7 @@ type FleetOverlayWriteFields = {
|
|
|
20
20
|
};
|
|
21
21
|
};
|
|
22
22
|
universe?: FleetUniverseRow[];
|
|
23
|
+
lowerMap?: Record<string, MapEntry>;
|
|
23
24
|
};
|
|
24
25
|
type EffortFree = FleetEditable & {
|
|
25
26
|
efforts?: undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Fleet-overlay mutation, serialization, and diff rendering for the `tickmarkr fleet` write path.
|
|
2
|
-
import { isMap, isScalar, isSeq, parseDocument, stringify, visit } from "yaml";
|
|
2
|
+
import { isAlias, isMap, isNode, isScalar, isSeq, parseDocument, stringify, visit } from "yaml";
|
|
3
3
|
import { DENY_SCOPES, universeCovers, universeEntryMatches } from "./config.js";
|
|
4
4
|
// OBS-1099 add.1: the writer ranges over the schema-derived scopes. The flat (all-seats) scopes
|
|
5
5
|
// take part in the membership/allow-form write; every nested scope (routing.deny.workers.*) is a
|
|
@@ -161,7 +161,71 @@ function setStringSequencePreservingComments(doc, path, values) {
|
|
|
161
161
|
// Total forms: a delete treats the masked subtree as absent (the operator's tombstone already
|
|
162
162
|
// denies it, and survives untouched); a set clears the scalar and rebuilds what fleet owns —
|
|
163
163
|
// the routing.allow precedent below.
|
|
164
|
+
// T8 repair: an alias is no tombstone — `spec: *empty` resolves to a mapping. Each alias on the path
|
|
165
|
+
// is replaced by an un-anchored copy of its target, so what it resolves to decides, and a write under
|
|
166
|
+
// it never edits the anchored node its other aliases share.
|
|
167
|
+
function materializeAliases(doc, path) {
|
|
168
|
+
for (let depth = 1; depth < path.length; depth++) {
|
|
169
|
+
const node = doc.getIn(path.slice(0, depth), true);
|
|
170
|
+
if (!isAlias(node))
|
|
171
|
+
continue;
|
|
172
|
+
const target = node.resolve(doc);
|
|
173
|
+
if (target === undefined)
|
|
174
|
+
return;
|
|
175
|
+
doc.setIn(path.slice(0, depth), detachedCopy(doc, node, target, new Set()));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const nodesOf = (root) => {
|
|
179
|
+
const out = [];
|
|
180
|
+
visit(root, (_key, node) => {
|
|
181
|
+
if (isNode(node))
|
|
182
|
+
out.push(node);
|
|
183
|
+
});
|
|
184
|
+
return out;
|
|
185
|
+
};
|
|
186
|
+
// D-821: a copy carries NO anchor (a nested `&empty` would rebind `plan: *empty` and every later
|
|
187
|
+
// alias to the edited copy), and each inner alias must still name what it named in the original —
|
|
188
|
+
// one whose anchor is redefined before `at` is replaced by a detached copy of its original target.
|
|
189
|
+
function detachedCopy(doc, at, target, expanding) {
|
|
190
|
+
if (expanding.has(target))
|
|
191
|
+
throw new Error(`fleet cannot detach the recursive YAML alias *${at.source}`);
|
|
192
|
+
const visible = new Map();
|
|
193
|
+
visit(doc, (_key, node) => {
|
|
194
|
+
if (node === at)
|
|
195
|
+
return visit.BREAK;
|
|
196
|
+
if (isNode(node) && !isAlias(node) && node.anchor)
|
|
197
|
+
visible.set(node.anchor, node);
|
|
198
|
+
});
|
|
199
|
+
const copy = target.clone();
|
|
200
|
+
const originals = nodesOf(target);
|
|
201
|
+
const replace = new Map();
|
|
202
|
+
nodesOf(copy).forEach((node, index) => {
|
|
203
|
+
if (!isAlias(node)) {
|
|
204
|
+
node.anchor = undefined;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const resolved = originals[index].resolve(doc);
|
|
208
|
+
if (resolved !== undefined && visible.get(node.source) !== resolved) {
|
|
209
|
+
replace.set(node, detachedCopy(doc, at, resolved, new Set([...expanding, target])));
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
if (!replace.size)
|
|
213
|
+
return copy;
|
|
214
|
+
visit(copy, (_key, node) => (isNode(node) ? replace.get(node) : undefined));
|
|
215
|
+
return copy;
|
|
216
|
+
}
|
|
217
|
+
function underScalar(doc, path) {
|
|
218
|
+
for (let depth = 1; depth < path.length; depth++) {
|
|
219
|
+
const node = doc.getIn(path.slice(0, depth), true);
|
|
220
|
+
if (node === undefined)
|
|
221
|
+
return false;
|
|
222
|
+
if (!isMap(node))
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
164
227
|
function deleteAt(doc, path) {
|
|
228
|
+
materializeAliases(doc, path); // an aliased mapping is no tombstone: Auto under `spec: *pinned` deletes the pin
|
|
165
229
|
for (let depth = 1; depth < path.length; depth++) {
|
|
166
230
|
if (!isMap(doc.getIn(path.slice(0, depth), true)))
|
|
167
231
|
return;
|
|
@@ -171,6 +235,7 @@ function deleteAt(doc, path) {
|
|
|
171
235
|
doc.deleteIn(path);
|
|
172
236
|
}
|
|
173
237
|
function setAt(doc, path, value) {
|
|
238
|
+
materializeAliases(doc, path); // edit a copy of an aliased mapping, never drop what it resolved to
|
|
174
239
|
for (let depth = 1; depth < path.length; depth++) {
|
|
175
240
|
const node = doc.getIn(path.slice(0, depth), true);
|
|
176
241
|
if (node === undefined)
|
|
@@ -281,11 +346,19 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
281
346
|
for (const shape of new Set([...Object.keys(initial.map), ...Object.keys(edited.map)])) {
|
|
282
347
|
const before = initial.map[shape];
|
|
283
348
|
const after = edited.map[shape];
|
|
284
|
-
|
|
349
|
+
const pinPath = ["routing", "map", shape, "pin"];
|
|
350
|
+
const declares = after?.pin !== undefined || after?.pool !== undefined || after?.prefer !== undefined;
|
|
351
|
+
if (!declares && write.lowerMap?.[shape]?.pin !== undefined && JSON.stringify(before) !== JSON.stringify(after)) {
|
|
352
|
+
// pin: null tombstone — masks the lower-layer pin; a scalar tombstone above it already masks the entry
|
|
353
|
+
materializeAliases(doc, pinPath);
|
|
354
|
+
if (!underScalar(doc, pinPath))
|
|
355
|
+
setScalarPreservingComment(doc, pinPath, null);
|
|
356
|
+
}
|
|
357
|
+
else if (JSON.stringify(before?.pin) !== JSON.stringify(after?.pin)) {
|
|
285
358
|
if (after?.pin === undefined)
|
|
286
|
-
deleteAt(doc,
|
|
359
|
+
deleteAt(doc, pinPath);
|
|
287
360
|
else
|
|
288
|
-
setAt(doc,
|
|
361
|
+
setAt(doc, pinPath, doc.createNode(after.pin));
|
|
289
362
|
}
|
|
290
363
|
if (JSON.stringify(before?.pool) !== JSON.stringify(after?.pool)) {
|
|
291
364
|
const path = ["routing", "map", shape, "pool"];
|
package/dist/drivers/orca.d.ts
CHANGED
|
@@ -31,16 +31,18 @@ export declare class OrcaError extends Error {
|
|
|
31
31
|
readonly raw: string;
|
|
32
32
|
readonly code?: string;
|
|
33
33
|
readonly runtimeId?: string;
|
|
34
|
+
readonly launchCause?: SeatLaunchCause;
|
|
34
35
|
constructor(family: string, reason: string, raw: string, opts?: {
|
|
35
36
|
code?: string;
|
|
36
37
|
runtimeId?: string;
|
|
38
|
+
launchCause?: SeatLaunchCause;
|
|
37
39
|
});
|
|
38
40
|
}
|
|
39
41
|
/** The slot cannot be addressed: dead/unknown terminal record, or a handle that cannot be recovered
|
|
40
42
|
* to exactly one owned terminal in the slot's own worktree. Never a silent false or empty string. */
|
|
41
43
|
export declare class OrcaUnavailableError extends OrcaError {
|
|
42
44
|
readonly terminalStatus?: string | undefined;
|
|
43
|
-
constructor(family: string, reason: string, raw: string, terminalStatus?: string | undefined);
|
|
45
|
+
constructor(family: string, reason: string, raw: string, terminalStatus?: string | undefined, launchCause?: SeatLaunchCause);
|
|
44
46
|
}
|
|
45
47
|
export interface OrcaEnvelope {
|
|
46
48
|
result: Record<string, unknown>;
|
|
@@ -83,6 +85,10 @@ export declare function terminalWorktree(term: Record<string, unknown>): string
|
|
|
83
85
|
export declare function canonicalWorktreePath(path: string): string;
|
|
84
86
|
/** The proof line a worker terminal prints first: create, recovery and focus read it back. */
|
|
85
87
|
export declare const CHECKOUT_MARK = "TICKMARKR_CHECKOUT";
|
|
88
|
+
/** E1: the one typed launch cause a driver attaches to its launch failure. `checkout-proof-timeout`
|
|
89
|
+
* means the terminal printed NO proof frame by the ceiling — a slow host, not a misplaced seat. A
|
|
90
|
+
* foreign or malformed frame is placement evidence and carries no cause (never a timeout exemption). */
|
|
91
|
+
export type SeatLaunchCause = "checkout-proof-timeout";
|
|
86
92
|
/** The exact bytes the create command prints as its first line. */
|
|
87
93
|
export declare function checkoutProofLine(checkout: string): string;
|
|
88
94
|
/** Every complete frame in a scrollback, decoded and canonicalized; whether an incomplete one was seen. */
|
|
@@ -250,6 +256,18 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
250
256
|
* that supplied the ownership listing — else another terminal's or another runtime's bytes were
|
|
251
257
|
* answered and nothing is proven. Proven means provesCheckout: exact canonical full-path equality
|
|
252
258
|
* of a complete frame, no other checkout named, no incomplete frame.
|
|
259
|
+
* E1: with a `deadline` (create's window) there is ONE absolute deadline: a read is issued only while
|
|
260
|
+
* budget is left and is handed exactly that budget, and a page that still lands past it is late —
|
|
261
|
+
* nothing late proves the checkout. `seen` is what the scrollback showed, for the typed timeout:
|
|
262
|
+
* `misplaced` — a frame naming another checkout, a cut frame or an unparseable mark in anything read
|
|
263
|
+
* (the anchor's tail and every page included, even when paging stopped or a later read threw);
|
|
264
|
+
* `absent` — ONLY when every page down to the start of the scrollback was read before the ceiling
|
|
265
|
+
* and none carries a trace of the mark; `unread` — every other exit: PROOF_PAGES reached, a limited
|
|
266
|
+
* read with no (or the same) next cursor or a truncated anchor without one, a read that threw, a read
|
|
267
|
+
* the ceiling cut off or never let start (the anchor included), or a page that landed late (even one
|
|
268
|
+
* carrying the right proof), a malformed page (its tail, cursors or paging flags), or a blind stream
|
|
269
|
+
* page (isBlindStreamPage). Unread output is unknown — never an empty page, never absence.
|
|
270
|
+
* Without a deadline (reconcile, recovery) only `proven` is consumed.
|
|
253
271
|
*/
|
|
254
272
|
private checkoutProven;
|
|
255
273
|
private recover;
|