tickmarkr 1.85.0 → 1.87.0

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.
Files changed (78) hide show
  1. package/README.md +4 -2
  2. package/dist/adapters/catalog-remote.d.ts +64 -0
  3. package/dist/adapters/catalog-remote.js +287 -0
  4. package/dist/adapters/catalog.d.ts +108 -0
  5. package/dist/adapters/catalog.js +189 -0
  6. package/dist/adapters/claude-code.js +5 -3
  7. package/dist/adapters/fake.js +33 -4
  8. package/dist/adapters/model-lints.d.ts +25 -5
  9. package/dist/adapters/model-lints.js +184 -50
  10. package/dist/adapters/model-windows.d.ts +31 -0
  11. package/dist/adapters/model-windows.js +69 -0
  12. package/dist/adapters/prompt.d.ts +5 -1
  13. package/dist/adapters/prompt.js +13 -4
  14. package/dist/adapters/registry.d.ts +34 -27
  15. package/dist/adapters/registry.js +215 -112
  16. package/dist/adapters/types.js +15 -3
  17. package/dist/brand.d.ts +5 -1
  18. package/dist/brand.js +18 -2
  19. package/dist/cli/commands/doctor.d.ts +3 -0
  20. package/dist/cli/commands/doctor.js +43 -21
  21. package/dist/cli/commands/fleet.d.ts +7 -0
  22. package/dist/cli/commands/fleet.js +94 -74
  23. package/dist/cli/commands/init.js +118 -5
  24. package/dist/cli/commands/plan.js +11 -1
  25. package/dist/cli/commands/resume.js +7 -1
  26. package/dist/cli/commands/status.js +44 -18
  27. package/dist/compile/gsd.d.ts +2 -1
  28. package/dist/compile/gsd.js +68 -2
  29. package/dist/compile/native.d.ts +14 -0
  30. package/dist/compile/native.js +168 -12
  31. package/dist/config/config.d.ts +20 -5
  32. package/dist/config/config.js +96 -64
  33. package/dist/config/fleet-overlay.d.ts +25 -20
  34. package/dist/config/fleet-overlay.js +195 -77
  35. package/dist/config/fleet-why.d.ts +23 -0
  36. package/dist/config/fleet-why.js +42 -0
  37. package/dist/drivers/herdr.d.ts +1 -0
  38. package/dist/drivers/herdr.js +70 -19
  39. package/dist/gates/acceptance.js +7 -2
  40. package/dist/gates/llm.d.ts +0 -1
  41. package/dist/gates/llm.js +5 -30
  42. package/dist/gates/review.d.ts +2 -1
  43. package/dist/gates/review.js +9 -7
  44. package/dist/gates/run-gates.d.ts +1 -0
  45. package/dist/gates/run-gates.js +21 -2
  46. package/dist/gates/verdict-cause.d.ts +4 -0
  47. package/dist/gates/verdict-cause.js +63 -0
  48. package/dist/graph/schema.d.ts +6 -0
  49. package/dist/graph/schema.js +8 -5
  50. package/dist/route/preference.d.ts +1 -1
  51. package/dist/route/preference.js +8 -1
  52. package/dist/route/router.d.ts +0 -5
  53. package/dist/route/router.js +16 -20
  54. package/dist/run/consult.d.ts +6 -0
  55. package/dist/run/consult.js +49 -26
  56. package/dist/run/daemon.js +81 -20
  57. package/dist/run/journal.js +87 -7
  58. package/dist/tui/cockpit/capture.d.ts +12 -0
  59. package/dist/tui/cockpit/capture.js +37 -1
  60. package/dist/tui/cockpit/components.js +8 -8
  61. package/dist/tui/cockpit/theme.d.ts +32 -26
  62. package/dist/tui/cockpit/theme.js +11 -5
  63. package/dist/tui/ink/components.d.ts +0 -15
  64. package/dist/tui/ink/components.js +0 -17
  65. package/dist/tui/ink/fleet-app.d.ts +4 -1
  66. package/dist/tui/ink/fleet-app.js +134 -13
  67. package/fixtures/gateway-models.json +1 -0
  68. package/fixtures/sample.native.md +1 -1
  69. package/package.json +1 -1
  70. package/skills/tickmarkr-overseer/SKILL.md +464 -34
  71. package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +86 -0
  72. package/skills/tickmarkr-overseer/scripts/watch-panes.sh +1 -1
  73. package/dist/tui/ink/studio-app.d.ts +0 -59
  74. package/dist/tui/ink/studio-app.js +0 -320
  75. package/dist/tui/save.d.ts +0 -38
  76. package/dist/tui/save.js +0 -96
  77. package/dist/tui/staging.d.ts +0 -29
  78. package/dist/tui/staging.js +0 -78
@@ -1,11 +1,11 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { shq } from "../adapters/types.js";
3
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { appendFileSync, closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, rmSync, statSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { stringify } from "yaml";
7
7
  import { classifyDeadChannel, NO_TRAILER_SUMMARY, trailerPattern, UNPARSEABLE_TRAILER_SUMMARY, writePrompt } from "../adapters/prompt.js";
8
- import { allAdapters, discoverChannels, getAdapter, probeAll, readDoctor } from "../adapters/registry.js";
8
+ import { allAdapters, getAdapter, probeAll, readDoctor, rolePools } from "../adapters/registry.js";
9
9
  import { addUsage, channelKey, matchesTrustDialog, QUOTA_RE } from "../adapters/types.js";
10
10
  import { bannerShell, paneDispatchCommand } from "../brand.js";
11
11
  import { DEFAULT_DIFF_CAP, globalConfigDir, loadConfigWithMode, readOverlayFile, repoOverlayPath, } from "../config/config.js";
@@ -631,6 +631,71 @@ async function cherryPickCommits(wt, commits) {
631
631
  }
632
632
  return carried;
633
633
  }
634
+ // T7 (v1.86): a first run-end append that fails AFTER partial bytes landed leaves a torn tail at
635
+ // EOF with no newline; a blind retry would glue the run-end line onto those bytes and readJsonl's
636
+ // torn-line tolerance would drop the retry too — no terminal record despite a successful write.
637
+ // Terminating the torn fragment keeps it on disk (dropped as a malformed line, never truncated) so
638
+ // the retried run-end lands on a line of its own.
639
+ const terminateTornJournalTail = (journalPath) => {
640
+ if (!existsSync(journalPath))
641
+ return;
642
+ const size = statSync(journalPath).size;
643
+ if (size === 0)
644
+ return;
645
+ const fd = openSync(journalPath, "r");
646
+ try {
647
+ const tail = Buffer.alloc(1);
648
+ if (readSync(fd, tail, 0, 1, size - 1) === 1 && tail[0] !== 0x0a)
649
+ appendFileSync(journalPath, "\n");
650
+ }
651
+ finally {
652
+ closeSync(fd);
653
+ }
654
+ };
655
+ // T7 (v1.86): the fatal handler must never eat the error it reports. Both journal calls are guarded,
656
+ // so a sink failure is reported ALONGSIDE the original error (console.error — the dispatcher's
657
+ // operator-visible line stays the one-line form), never instead of it: a read failure degrades the
658
+ // duplicate-run-end check to "unknown" and fails toward recording; the append is retried ONCE; and
659
+ // a persistently unwritable sink reports a crash naming the journal path and carrying NO terminal
660
+ // record, rather than fabricating an ended run on evidence the harness could not write. (OBS-313:
661
+ // with the sink dead the crash CAUSE is unrecordable — recorded as an observation, not papered over.)
662
+ function recordFatalRunEnd(journal, runId, branch, err) {
663
+ const original = err instanceof Error ? err.message : String(err);
664
+ try {
665
+ if (journal.read().some((e) => e.event === "run-end"))
666
+ return; // already terminal — nothing to add
667
+ }
668
+ catch (readErr) {
669
+ console.error(`tickmarkr ${runId}: journal read failed while recording the fatal run-end (${readErr instanceof Error ? readErr.message : String(readErr)}) — original error: ${original}`);
670
+ }
671
+ const record = {
672
+ runId,
673
+ branch,
674
+ done: [],
675
+ failed: [],
676
+ human: [],
677
+ blocked: [],
678
+ pending: [],
679
+ phase: "setup",
680
+ fatal: true,
681
+ error: original,
682
+ };
683
+ try {
684
+ journal.append("run-end", undefined, record);
685
+ return;
686
+ }
687
+ catch {
688
+ // one retry below — the first failure is reported only if the retry also fails
689
+ }
690
+ const journalPath = join(journal.dir, "journal.jsonl");
691
+ try {
692
+ terminateTornJournalTail(journalPath);
693
+ journal.append("run-end", undefined, record);
694
+ }
695
+ catch (retryErr) {
696
+ console.error(`tickmarkr ${runId}: run crashed — no terminal record written; journal sink unwritable at ${journalPath} (${retryErr instanceof Error ? retryErr.message : String(retryErr)}) — original error: ${original}`);
697
+ }
698
+ }
634
699
  export async function runDaemon(repoRoot, opts = {}) {
635
700
  // v1.51 T2 / OBS-89 (v1.60): retired --quality env seam. Mode resolution owns premium routing;
636
701
  // route() no longer reads the retired env at all, so the old entrypoint scrub is gone with it.
@@ -664,7 +729,12 @@ export async function runDaemon(repoRoot, opts = {}) {
664
729
  // v1.51 T4: every dispatch provenance line begins with the mode and its source; when a pin won
665
730
  // the route (the final "→ " segment is a pin, not a degraded-to-auto tail) it names the mode it bypassed.
666
731
  const dispatchProvenance = (p) => `mode ${rm.mode.mode} (${rm.source})${p.split("→ ").pop().startsWith("pin ") ? ` — pin bypasses mode ${rm.mode.mode}` : ""} · ${p}`;
667
- const channels = discoverChannels(cfg, adapters, health);
732
+ // v1.87 T2: one pool per seat role, built once. `channels` stays the WORKER pool — every routing
733
+ // call below reads it exactly as before — while the judge, review and consult seats each receive
734
+ // the pool their own deny scope allows, so routing.deny.workers benches a channel for dispatch
735
+ // without also removing it from the seats that verify the work.
736
+ const pools = rolePools(cfg, adapters, health);
737
+ const channels = pools.worker;
668
738
  // v1.6 ROUTE-06: build the learned profile ONCE at startup (never per task, never in the comparator).
669
739
  // No preview — the daemon honors routing.learned:off and gets undefined; this snapshot is immutable
670
740
  // for the run, so this run's own telemetry never feeds back into its own routing.
@@ -1135,7 +1205,7 @@ export async function runDaemon(repoRoot, opts = {}) {
1135
1205
  // D-07: consult panes self-clean when the verdict is read (keepLlm) — only "forever" keeps them.
1136
1206
  // v1.54 T1: channels = this run's doctor-filtered live list — consult.prefer seat liveness
1137
1207
  // is judged against it, never rebuilt from config (installed-but-unauthed seats would stall).
1138
- { keep: keepLlm, onSlot: keepLlm ? (s) => keptSlots.push(s) : undefined, runId, channels });
1208
+ { keep: keepLlm, onSlot: keepLlm ? (s) => keptSlots.push(s) : undefined, runId, channels: pools.consult });
1139
1209
  };
1140
1210
  // returns true → continue attempting, false → task is terminal (parked)
1141
1211
  // trigger (why the consult ran) is threaded in so the decompose/human park keeps its cause —
@@ -1281,7 +1351,7 @@ export async function runDaemon(repoRoot, opts = {}) {
1281
1351
  journal.phaseStart(t.id, "gates");
1282
1352
  const { results } = await runGates(resumedTask, {
1283
1353
  worktree: wt, baseRef: taskBase, result: priorResult, author: gateAuthor,
1284
- commands, baseline, channels, adapters, cfg, artifactDir: journal.dir,
1354
+ commands, baseline, channels: pools.review, judgeChannels: pools.judge, adapters, cfg, artifactDir: journal.dir,
1285
1355
  // a recheck re-verifies a human's release: it never selects tests down, it runs the suite.
1286
1356
  pipeline: "v185",
1287
1357
  via: cfg.visibility.llm === "pane"
@@ -2441,7 +2511,7 @@ export async function runDaemon(repoRoot, opts = {}) {
2441
2511
  journal.phaseStart(t.id, "gates");
2442
2512
  ({ results, commits } = await runGates(t, {
2443
2513
  worktree: wt, baseRef: taskBase, result, author: assignment,
2444
- commands, baseline, channels, adapters, cfg, artifactDir: journal.dir,
2514
+ commands, baseline, channels: pools.review, judgeChannels: pools.judge, adapters, cfg, artifactDir: journal.dir,
2445
2515
  pipeline: "v185", selectTests: !testGateFailed,
2446
2516
  via: cfg.visibility.llm === "pane"
2447
2517
  ? {
@@ -2719,20 +2789,11 @@ export async function runDaemon(repoRoot, opts = {}) {
2719
2789
  return summary;
2720
2790
  }
2721
2791
  catch (err) {
2722
- if (runStarted && !taskLoopStarted && !journal.read().some((e) => e.event === "run-end")) {
2723
- journal.append("run-end", undefined, {
2724
- runId,
2725
- branch,
2726
- done: [],
2727
- failed: [],
2728
- human: [],
2729
- blocked: [],
2730
- pending: [],
2731
- phase: "setup",
2732
- fatal: true,
2733
- error: err instanceof Error ? err.message : String(err),
2734
- });
2735
- }
2792
+ // T7 (v1.86): guarded — a journal read/append failure while recording the fatal run-end is
2793
+ // reported alongside err, never instead of it; recordFatalRunEnd never throws, so the original
2794
+ // error (message, stack, cause) always reaches the caller verbatim.
2795
+ if (runStarted && !taskLoopStarted)
2796
+ recordFatalRunEnd(journal, runId, branch, err);
2736
2797
  throw err;
2737
2798
  }
2738
2799
  finally {
@@ -195,7 +195,7 @@ export function structuredFindings(gate, details, _scopeFiles = []) {
195
195
  const VOLATILE_TOKENS = [
196
196
  [/\u001b\[[0-9;]*[a-zA-Z]/g, ""], // ANSI styling
197
197
  [/\b\d{4}-\d{2}-\d{2}[T ][\d:]+(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?\b/g, "<ts>"], // timestamps
198
- [/\brun-\d{8}-\d{6}\b/g, "<run>"], // run identifiers
198
+ [/\brun-\d{8}-\d{6}(?:-\d{16})?\b/g, "<run>"], // run identifiers
199
199
  [/\b0x[0-9a-fA-F]+\b/g, "<addr>"], // memory addresses
200
200
  // An absolute path INTO the repo keeps its repo-relative tail — that tail IS identity (a defect in
201
201
  // daemon.ts is not a defect in journal.ts); only the machine/worktree prefix ahead of it is volatile.
@@ -227,15 +227,67 @@ const eraseVolatile = (text) => VOLATILE_TOKENS.reduce((out, [re, replacement])
227
227
  // difference and banned a retry that was never redundant. Newlines survive (payload spans cannot
228
228
  // cross one) and the line-wise trim below finishes the job.
229
229
  const collapseRuns = (text) => text.replace(/[^\S\n]+/g, " ");
230
+ // v1.85 T34: the runner's TALLY moves with the base, not with the defect. When another task merges
231
+ // ahead and adds test files, vitest re-counts the suite — `192 passed (197)` becomes `193 passed
232
+ // (198)` — while the FAIL headlines name the same defect in the same order, so a substantively
233
+ // identical gate failure re-fingerprinted and gate-fingerprint-cap never counted the repeat
234
+ // (measured on T21 in run-20260805-164546: details at 17:33:02 and 18:11:01 differ only so).
235
+ //
236
+ // Why provenance AND shape, and why not digits as a class: a blanket \d+ collapse also erases counts
237
+ // that ARE identity — the failed-assertion count, the failing-suite count, an exit status, an error
238
+ // code — and the rule above governs: a missed cap costs one extra round, a false cap bans a
239
+ // legitimate retry. So the mask is gated on PROVENANCE first: only lines inside the `failing tests:`
240
+ // block baseline.ts:209-219 emits (up to its blank-line/`new failure fingerprints` boundary) are
241
+ // eligible, which keeps review prose, quoted diffs and assertion payloads structurally unreachable —
242
+ // and that block's own `FAIL … > test name` headlines are excluded by SHAPE: only a line that starts
243
+ // with the runner's own `Tests`/`Test Files` summary token is a tally line.
244
+ //
245
+ // Masked tally FIELDS, exactly: the passed count, the skipped and todo counts, and the derived
246
+ // parenthesized total immediately following the tally sequence. Deliberately KEPT (a reader uses
247
+ // each of them to tell two failures apart): every `N failed` count on the line (which suites and how
248
+ // many assertions actually broke — the defect's identity), everything after the derived total (an
249
+ // appended `| exit 1`, an appended `(404)` code), and every other number anywhere.
250
+ const RUNNER_TALLY_LINE = /^([ \t]*(?:Test Files|Tests)[ \t]+)((?:\d+ (?:failed|passed|skipped|todo)[ \t]*\|[ \t]*)*\d+ (?:failed|passed|skipped|todo))([ \t]*\(\d+\))?(.*)$/;
251
+ const TALLY_MOVED_FIELD = /\d+ (?=passed|skipped|todo)/g;
252
+ const maskTallyLine = (line) => {
253
+ const m = RUNNER_TALLY_LINE.exec(line);
254
+ if (!m)
255
+ return line;
256
+ const fields = m[2].replace(TALLY_MOVED_FIELD, "#");
257
+ const total = (m[3] ?? "").replace(/\d+/, "#");
258
+ return m[1] + fields + total + m[4];
259
+ };
260
+ // Computed on the FULL details text BEFORE any payload split: every recorded tally-bearing detail
261
+ // carries the `failing tests:` header, so gating on it closes the false-line-start axis (the mask
262
+ // never sees a slice boundary) and the wrong-provenance axis in one place.
263
+ const maskRunnerTallies = (text) => {
264
+ let inBlock = false;
265
+ return text
266
+ .split("\n")
267
+ .map((line) => {
268
+ if (!inBlock) {
269
+ if (line.trim() === "failing tests:")
270
+ inBlock = true;
271
+ return line;
272
+ }
273
+ if (line.trim() === "" || line.startsWith("new failure fingerprints")) {
274
+ inBlock = false;
275
+ return line;
276
+ }
277
+ return maskTallyLine(line);
278
+ })
279
+ .join("\n");
280
+ };
230
281
  /** Normalized identity of a gate failure: the same defect, seen twice, normalizes to the same bytes. */
231
282
  export function normalizeGateFailure(details) {
283
+ const masked = maskRunnerTallies(details);
232
284
  let out = "";
233
285
  let last = 0;
234
- for (const m of details.matchAll(PAYLOAD_SPAN)) {
235
- out += collapseRuns(eraseVolatile(details.slice(last, m.index))) + m[0];
286
+ for (const m of masked.matchAll(PAYLOAD_SPAN)) {
287
+ out += collapseRuns(eraseVolatile(masked.slice(last, m.index))) + m[0];
236
288
  last = m.index + m[0].length;
237
289
  }
238
- out += collapseRuns(eraseVolatile(details.slice(last)));
290
+ out += collapseRuns(eraseVolatile(masked.slice(last)));
239
291
  return out.split("\n").map((l) => l.trim()).filter(Boolean).join("\n");
240
292
  }
241
293
  // Two normalized-identical failures of one gate on one task buy no more rounds (the ladder cannot fix
@@ -538,9 +590,37 @@ export function engagementComparable(events, loadedHash) {
538
590
  return { comparable: false, reason: "unbound" };
539
591
  return recorded === loadedHash ? { comparable: true, recorded } : { comparable: false, reason: "mismatch", recorded };
540
592
  }
593
+ const RUN_SEQUENCE_WIDTH = 16;
594
+ // The daemon mints before acquiring graph.lock, and each CLI invocation has fresh module state. Claim
595
+ // a repository-state ticket with mkdir's atomic EEXIST boundary instead: a losing process advances and
596
+ // retries, while the winning directory remains as the durable high-water evidence for later invocations.
597
+ function claimRunSequence(repoRoot = process.cwd()) {
598
+ const dir = join(tickmarkrDir(repoRoot), "run-id-sequence");
599
+ mkdirSync(dir, { recursive: true });
600
+ const allocated = readdirSync(dir).filter((name) => /^\d{16}$/.test(name));
601
+ let candidate = allocated.reduce((max, name) => {
602
+ const value = BigInt(name);
603
+ return value > max ? value : max;
604
+ }, 0n) + 1n;
605
+ while (true) {
606
+ const suffix = candidate.toString().padStart(RUN_SEQUENCE_WIDTH, "0");
607
+ if (suffix.length > RUN_SEQUENCE_WIDTH)
608
+ throw new Error("run id sequence exhausted");
609
+ try {
610
+ mkdirSync(join(dir, suffix));
611
+ return suffix;
612
+ }
613
+ catch (error) {
614
+ if (error.code !== "EEXIST")
615
+ throw error;
616
+ candidate += 1n;
617
+ }
618
+ }
619
+ }
541
620
  export function newRunId(now = new Date()) {
542
621
  const p = (n, w = 2) => String(n).padStart(w, "0");
543
- return `run-${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
622
+ const instant = `${now.getUTCFullYear()}${p(now.getUTCMonth() + 1)}${p(now.getUTCDate())}-${p(now.getUTCHours())}${p(now.getUTCMinutes())}${p(now.getUTCSeconds())}`;
623
+ return `run-${instant}-${claimRunSequence()}`;
544
624
  }
545
625
  // Sol #4: one strict parser for every journal open/create path — generated run-… ids plus test
546
626
  // suffix chars only; forbid path separators, dot-segments, and empty ids.
@@ -578,7 +658,7 @@ function readJsonl(path) {
578
658
  return out;
579
659
  }
580
660
  // Cross-run telemetry for Phase-12 profile derivation: the last K runs' rows, each
581
- // tagged with its runId (runIds are zero-padded run-YYYYMMDD-HHMMSS ⇒ plain .sort() is
661
+ // tagged with its runId (runIds are zero-padded run-UTCYYYYMMDD-HHMMSS-sequence ⇒ plain .sort() is
582
662
  // chronological, same as latestRunId). Rows are facts, not classifications — Phase 12
583
663
  // owns the quality-denominator/reward policy. A safeParse failure drops that one row
584
664
  // (same posture as a torn line); a garbage row must never crash profile derivation.
@@ -589,7 +669,7 @@ export function readAllTelemetry(repoRoot, lastK, opts = {}) {
589
669
  if (!existsSync(dir))
590
670
  return [];
591
671
  let runIds = readdirSync(dir).filter((d) => d.startsWith("run-")).sort();
592
- // VIS-03 reset cursor: runIds are zero-padded run-YYYYMMDD-HHMMSS ⇒ string > is chronological
672
+ // VIS-03 reset cursor: UTC clock fields and a fixed-width sequence make string > chronological
593
673
  if (opts.after)
594
674
  runIds = runIds.filter((id) => id > opts.after);
595
675
  runIds = runIds.slice(-lastK);
@@ -209,6 +209,18 @@ export type RegeneratedGoldenFrame = GoldenFrameCase & {
209
209
  readonly emitted: string;
210
210
  };
211
211
  export declare function regenerateGoldenFrames(binaryVersion?: string): Promise<readonly RegeneratedGoldenFrame[]>;
212
+ /**
213
+ * Canonicalize the one release-owned token in an ACTIVE golden frame.
214
+ *
215
+ * The grammar is deliberately positional: plain output owns the whole first
216
+ * line, while a setup frame owns the right-anchored `v<semver> · binary ✓`
217
+ * suffix. The latter preserves the suffix's right edge, so a longer version
218
+ * does not turn renderer padding into drift while any actual alignment change
219
+ * still does. Retired rendered frames never enter this exception.
220
+ */
221
+ export declare function normalizeGoldenFrameVersionForComparison(frame: GoldenFrameCase, bytes: string): string;
222
+ /** An absent committed frame is a mismatch; it is never an empty agreement. */
223
+ export declare function goldenFrameMatchesCommitted(frame: RegeneratedGoldenFrame, committed: string | undefined): boolean;
212
224
  export declare function findGoldenFrameMismatches(regenerated: readonly RegeneratedGoldenFrame[], committed: ReadonlyMap<string, string>): string[];
213
225
  export type RegeneratedColourFrame = ColourFrameCase & {
214
226
  readonly output: string;
@@ -564,13 +564,49 @@ export async function regenerateGoldenFrames(binaryVersion) {
564
564
  }
565
565
  return regenerated;
566
566
  }
567
+ const SEMVER_TOKEN = String.raw `\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?`;
568
+ const PLAIN_VERSION_HEADER = new RegExp(`^(tickmarkr v)(${SEMVER_TOKEN})$`, "u");
569
+ const FRAME_VERSION_HEADER = new RegExp(`^(.*\\S)( +)v(${SEMVER_TOKEN})( · binary ✓)$`, "u");
570
+ const COMPARISON_VERSION = "<binary-version>";
571
+ /**
572
+ * Canonicalize the one release-owned token in an ACTIVE golden frame.
573
+ *
574
+ * The grammar is deliberately positional: plain output owns the whole first
575
+ * line, while a setup frame owns the right-anchored `v<semver> · binary ✓`
576
+ * suffix. The latter preserves the suffix's right edge, so a longer version
577
+ * does not turn renderer padding into drift while any actual alignment change
578
+ * still does. Retired rendered frames never enter this exception.
579
+ */
580
+ export function normalizeGoldenFrameVersionForComparison(frame, bytes) {
581
+ if (isRetiredGoldenFrame(frame))
582
+ return bytes;
583
+ const lineBreak = bytes.indexOf("\n");
584
+ const header = lineBreak === -1 ? bytes : bytes.slice(0, lineBreak);
585
+ const rest = lineBreak === -1 ? "" : bytes.slice(lineBreak);
586
+ let normalized = header;
587
+ if (!frame.interactive || frame.ci) {
588
+ normalized = header.replace(PLAIN_VERSION_HEADER, `$1${COMPARISON_VERSION}`);
589
+ }
590
+ else {
591
+ normalized = header.replace(FRAME_VERSION_HEADER, (_match, prefix, gap, versionToken, suffix) => `${prefix}${" ".repeat(gap.length + versionToken.length)}v${COMPARISON_VERSION}${suffix}`);
592
+ }
593
+ return normalized === header ? bytes : `${normalized}${rest}`;
594
+ }
595
+ /** An absent committed frame is a mismatch; it is never an empty agreement. */
596
+ export function goldenFrameMatchesCommitted(frame, committed) {
597
+ return committed !== undefined
598
+ && normalizeGoldenFrameVersionForComparison(frame, committed)
599
+ === normalizeGoldenFrameVersionForComparison(frame, frame.output);
600
+ }
567
601
  export function findGoldenFrameMismatches(regenerated, committed) {
568
602
  // Membership is judged against the whole shipped manifest, retired cases
569
603
  // included: a retired fixture is claimed, so only its bytes are exempt.
570
604
  // Anything committed that no case claims is still named.
571
605
  const expected = new Set(GOLDEN_FRAME_CASES.map((frame) => frame.fixture));
572
606
  return [
573
- ...regenerated.flatMap((frame) => committed.get(frame.fixture) === frame.output ? [] : [frame.fixture]),
607
+ ...regenerated.flatMap((frame) => goldenFrameMatchesCommitted(frame, committed.get(frame.fixture))
608
+ ? []
609
+ : [frame.fixture]),
574
610
  ...[...committed.keys()].filter((fixture) => !expected.has(fixture)),
575
611
  ].sort();
576
612
  }
@@ -170,24 +170,24 @@ function statePresentation(state) {
170
170
  case "active":
171
171
  return {
172
172
  glyph: GLYPHS.toggleActive,
173
- color: `ansi256(${COCKPIT_DATA_RAMP[1].xterm})`,
173
+ color: COCKPIT_DATA_RAMP[1].hex,
174
174
  };
175
175
  case "inactive":
176
176
  return { glyph: GLYPHS.toggleInactive, dimmed: true };
177
177
  case "pass":
178
178
  return {
179
179
  glyph: GLYPHS.pass,
180
- color: `ansi256(${COCKPIT_DATA_RAMP[1].xterm})`,
180
+ color: COCKPIT_DATA_RAMP[1].hex,
181
181
  };
182
182
  case "fail":
183
183
  return {
184
184
  glyph: GLYPHS.fail,
185
- color: `ansi256(${COCKPIT_INKS.failure.xterm})`,
185
+ color: COCKPIT_INKS.failure.hex,
186
186
  };
187
187
  case "warn":
188
188
  return {
189
189
  glyph: GLYPHS.attention,
190
- color: `ansi256(${COCKPIT_INKS.warning.xterm})`,
190
+ color: COCKPIT_INKS.warning.hex,
191
191
  };
192
192
  case "neutral":
193
193
  return { glyph: GLYPHS.neutral, dimmed: true };
@@ -201,7 +201,7 @@ export function Panel({ title, focused = false, width, flexGrow, children, }) {
201
201
  const content = Children.map(children, (child) => typeof child === "string" || typeof child === "number"
202
202
  ? _jsx(BodyText, { children: child })
203
203
  : child);
204
- return (_jsxs(Box, { flexDirection: "column", flexGrow: flexGrow, width: width, paddingX: 1, borderStyle: focused ? "double" : "round", borderColor: `ansi256(${focused ? COCKPIT_DATA_RAMP[0].xterm : COCKPIT_DATA_RAMP[3].xterm})`, borderDimColor: !focused, children: [_jsx(Box, { children: focused ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: `ansi256(${COCKPIT_DATA_RAMP[0].xterm})`, children: GLYPHS.pointer }), _jsxs(BodyText, { emphasis: "strong", children: [" ", title] })] })) : (_jsx(BodyText, { emphasis: "strong", children: title })) }), _jsx(Box, { flexDirection: "column", children: content })] }));
204
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: flexGrow, width: width, paddingX: 1, borderStyle: focused ? "double" : "round", borderColor: focused ? COCKPIT_DATA_RAMP[0].hex : COCKPIT_DATA_RAMP[3].hex, borderDimColor: !focused, children: [_jsx(Box, { children: focused ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: COCKPIT_DATA_RAMP[0].hex, children: GLYPHS.pointer }), _jsxs(BodyText, { emphasis: "strong", children: [" ", title] })] })) : (_jsx(BodyText, { emphasis: "strong", children: title })) }), _jsx(Box, { flexDirection: "column", children: content })] }));
205
205
  }
206
206
  export function CockpitGrid({ children, columns, columnContents, allocation, }) {
207
207
  const items = Children.toArray(children);
@@ -229,7 +229,7 @@ export function Sparkline({ samples, }) {
229
229
  const level = sparklineLevel(sample, minimum, maximum);
230
230
  const rampIndex = COCKPIT_DATA_RAMP.length - 1 - Math.round((level / (SPARKLINE_GLYPHS.length - 1)) * (COCKPIT_DATA_RAMP.length - 1));
231
231
  const ink = COCKPIT_DATA_RAMP[rampIndex] ?? COCKPIT_DATA_RAMP[0];
232
- return (_jsx(Text, { color: `ansi256(${ink.xterm})`, children: SPARKLINE_GLYPHS[level] }, `${index}:${sample}`));
232
+ return (_jsx(Text, { color: ink.hex, children: SPARKLINE_GLYPHS[level] }, `${index}:${sample}`));
233
233
  }) }));
234
234
  }
235
235
  export function StatTile({ label, value, samples, focused, width, }) {
@@ -242,7 +242,7 @@ export function ProgressMeter({ value, width = 24, }) {
242
242
  const valueLabel = Number.isInteger(percentage)
243
243
  ? percentage.toFixed(0)
244
244
  : percentage.toFixed(1);
245
- return (_jsxs(Box, { flexDirection: "row", flexWrap: "nowrap", height: 1, overflow: "hidden", "aria-role": "progressbar", "aria-label": `${valueLabel}% complete`, children: [_jsx(Text, { color: `ansi256(${COCKPIT_DATA_RAMP[0].xterm})`, children: "█".repeat(filled) }), _jsx(Text, { dimColor: true, children: "░".repeat(safeWidth - filled) }), _jsxs(BodyText, { emphasis: "strong", children: [" ", valueLabel, "%"] })] }));
245
+ return (_jsxs(Box, { flexDirection: "row", flexWrap: "nowrap", height: 1, overflow: "hidden", "aria-role": "progressbar", "aria-label": `${valueLabel}% complete`, children: [_jsx(Text, { color: COCKPIT_DATA_RAMP[0].hex, children: "█".repeat(filled) }), _jsx(Text, { dimColor: true, children: "░".repeat(safeWidth - filled) }), _jsxs(BodyText, { emphasis: "strong", children: [" ", valueLabel, "%"] })] }));
246
246
  }
247
247
  function OneLineItems({ children, width, label, }) {
248
248
  return (_jsx(Box, { flexDirection: "row", flexWrap: "nowrap", width: width, height: 1, overflow: "hidden", "aria-label": label, children: children }));
@@ -285,6 +285,6 @@ export function StatusStrip({ items, width, }) {
285
285
  }
286
286
  export function JournalRowPanel({ rows, width, focused, title = "JOURNAL", selection, hover, }) {
287
287
  return (_jsx(Panel, { title: title, focused: focused, width: width, children: rows.map((row, index) => (_jsxs(Box, { flexDirection: "row", flexWrap: "nowrap", height: 1, overflow: "hidden", children: [_jsx(BodyText, { emphasis: "dim", children: row.time }), _jsx(BodyText, { children: " " }), _jsx(StateGlyph, { state: row.state }), _jsx(BodyText, { emphasis: index === hover ? "strong" : "normal", children: index === hover ? HOVER_MARKER : " " }), index === selection && (_jsxs(BodyText, { emphasis: "strong", children: [GLYPHS.pointer, " "] })), index === hover
288
- ? (_jsx(Text, { inverse: true, color: `ansi256(${COCKPIT_DATA_RAMP[0].xterm})`, children: row.text }))
288
+ ? (_jsx(Text, { inverse: true, color: COCKPIT_DATA_RAMP[0].hex, children: row.text }))
289
289
  : _jsx(BodyText, { children: row.text })] }, row.id))) }));
290
290
  }
@@ -7,19 +7,25 @@ export type ColourValue = {
7
7
  export declare const LOGO_RAMP: readonly [84, 78, 41, 35];
8
8
  /** Near-black panel fill against which every data shade must remain legible. */
9
9
  export declare const COCKPIT_SURFACE: "#1a1a19";
10
- /** Bright-to-deep, single-hue data ink. Depth communicates magnitude, never tile identity. */
10
+ /**
11
+ * Bright-to-deep, single-hue data ink. Depth communicates magnitude, never tile identity.
12
+ * Anchored on xterm 114 (#87d787) — operator-chosen 2026-08-03 — with the hue (OKLab 144.2°)
13
+ * held constant down the ramp; steps descend in even OKLab lightness (0.094) and were searched
14
+ * and verified through guardDataRamp below. hex is the rendered ink (truecolor terminals);
15
+ * xterm is the nearest-distinct 256-colour fallback, documentation not render source.
16
+ */
11
17
  export declare const COCKPIT_DATA_RAMP: readonly [{
12
- readonly xterm: 84;
13
- readonly hex: "#5fff5f";
18
+ readonly xterm: 114;
19
+ readonly hex: "#87d787";
14
20
  }, {
15
- readonly xterm: 41;
16
- readonly hex: "#00d75f";
21
+ readonly xterm: 71;
22
+ readonly hex: "#5bbc5e";
17
23
  }, {
18
- readonly xterm: 35;
19
- readonly hex: "#00af5f";
24
+ readonly xterm: 65;
25
+ readonly hex: "#4f9a51";
20
26
  }, {
21
27
  readonly xterm: 29;
22
- readonly hex: "#00875f";
28
+ readonly hex: "#437944";
23
29
  }];
24
30
  /** Documented near-misses retained as guard fixtures, not special cases in the guard. */
25
31
  export declare const EXCLUDED_DATA_SHADES: {
@@ -38,17 +44,17 @@ export declare const EXCLUDED_DATA_SHADES: {
38
44
  */
39
45
  export declare const COCKPIT_INKS: {
40
46
  readonly dataRamp: readonly [{
41
- readonly xterm: 84;
42
- readonly hex: "#5fff5f";
47
+ readonly xterm: 114;
48
+ readonly hex: "#87d787";
43
49
  }, {
44
- readonly xterm: 41;
45
- readonly hex: "#00d75f";
50
+ readonly xterm: 71;
51
+ readonly hex: "#5bbc5e";
46
52
  }, {
47
- readonly xterm: 35;
48
- readonly hex: "#00af5f";
53
+ readonly xterm: 65;
54
+ readonly hex: "#4f9a51";
49
55
  }, {
50
56
  readonly xterm: 29;
51
- readonly hex: "#00875f";
57
+ readonly hex: "#437944";
52
58
  }];
53
59
  readonly warning: {
54
60
  readonly xterm: 214;
@@ -65,17 +71,17 @@ export declare const COCKPIT_INKS: {
65
71
  };
66
72
  /** Every drawable cockpit ink; the panel surface and text emphasis are not inks. */
67
73
  export declare const COCKPIT_INK_VOCABULARY: readonly [{
68
- readonly xterm: 84;
69
- readonly hex: "#5fff5f";
74
+ readonly xterm: 114;
75
+ readonly hex: "#87d787";
70
76
  }, {
71
- readonly xterm: 41;
72
- readonly hex: "#00d75f";
77
+ readonly xterm: 71;
78
+ readonly hex: "#5bbc5e";
73
79
  }, {
74
- readonly xterm: 35;
75
- readonly hex: "#00af5f";
80
+ readonly xterm: 65;
81
+ readonly hex: "#4f9a51";
76
82
  }, {
77
83
  readonly xterm: 29;
78
- readonly hex: "#00875f";
84
+ readonly hex: "#437944";
79
85
  }, {
80
86
  readonly xterm: 214;
81
87
  readonly hex: "#ffaf00";
@@ -105,16 +111,16 @@ export declare const TEXT_EMPHASIS_TOKENS: {
105
111
  /** Deliberate 16-colour choices. Renderers select this map; they do not infer one. */
106
112
  export declare const REDUCED_COLOUR_MAPPING: {
107
113
  readonly dataRamp: readonly [{
108
- readonly from: "#5fff5f";
114
+ readonly from: "#87d787";
109
115
  readonly to: "bright-green";
110
116
  }, {
111
- readonly from: "#00d75f";
117
+ readonly from: "#5bbc5e";
112
118
  readonly to: "green";
113
119
  }, {
114
- readonly from: "#00af5f";
120
+ readonly from: "#4f9a51";
115
121
  readonly to: "green";
116
122
  }, {
117
- readonly from: "#00875f";
123
+ readonly from: "#437944";
118
124
  readonly to: "dim-green";
119
125
  }];
120
126
  readonly warning: {
@@ -3,12 +3,18 @@ import { BRAND_RAMP, GLYPHS } from "../../brand.js";
3
3
  export const LOGO_RAMP = BRAND_RAMP;
4
4
  /** Near-black panel fill against which every data shade must remain legible. */
5
5
  export const COCKPIT_SURFACE = "#1a1a19";
6
- /** Bright-to-deep, single-hue data ink. Depth communicates magnitude, never tile identity. */
6
+ /**
7
+ * Bright-to-deep, single-hue data ink. Depth communicates magnitude, never tile identity.
8
+ * Anchored on xterm 114 (#87d787) — operator-chosen 2026-08-03 — with the hue (OKLab 144.2°)
9
+ * held constant down the ramp; steps descend in even OKLab lightness (0.094) and were searched
10
+ * and verified through guardDataRamp below. hex is the rendered ink (truecolor terminals);
11
+ * xterm is the nearest-distinct 256-colour fallback, documentation not render source.
12
+ */
7
13
  export const COCKPIT_DATA_RAMP = [
8
- { xterm: 84, hex: "#5fff5f" },
9
- { xterm: 41, hex: "#00d75f" },
10
- { xterm: 35, hex: "#00af5f" },
11
- { xterm: 29, hex: "#00875f" },
14
+ { xterm: 114, hex: "#87d787" },
15
+ { xterm: 71, hex: "#5bbc5e" },
16
+ { xterm: 65, hex: "#4f9a51" },
17
+ { xterm: 29, hex: "#437944" },
12
18
  ];
13
19
  /** Documented near-misses retained as guard fixtures, not special cases in the guard. */
14
20
  export const EXCLUDED_DATA_SHADES = {
@@ -1,5 +1,4 @@
1
1
  import type { ReactNode } from "react";
2
- import { type SaveTarget } from "../save.js";
3
2
  export type FleetListRow = {
4
3
  id: string;
5
4
  content: ReactNode;
@@ -22,17 +21,3 @@ export declare function FleetReviewScreen({ title, legend, diff, }: {
22
21
  legend: string;
23
22
  diff: string;
24
23
  }): import("react").JSX.Element;
25
- export type DiffConfirmationProps = {
26
- target: SaveTarget;
27
- targetPath: string;
28
- diff: string;
29
- liveRun: boolean;
30
- };
31
- export declare function DiffConfirmationScreen(props: DiffConfirmationProps): import("react").JSX.Element;
32
- export declare function StudioScreen({ tabs, active, lines, status, children, }: {
33
- tabs: string[];
34
- active: number;
35
- lines: string[];
36
- status?: string;
37
- children?: ReactNode;
38
- }): import("react").JSX.Element;
@@ -1,7 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  import { GLYPHS } from "../../brand.js";
4
- import { RELOAD_GUARD_NOTICE } from "../save.js";
5
4
  function keyedLines(lines) {
6
5
  const occurrences = new Map();
7
6
  return lines.map((text) => {
@@ -24,19 +23,3 @@ export function FleetListScreen({ title, legend, rows, cursor, details = [], })
24
23
  export function FleetReviewScreen({ title, legend, diff, }) {
25
24
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: title }), _jsx(Text, { dimColor: true, children: legend }), _jsx(Text, { children: diff })] }));
26
25
  }
27
- export function DiffConfirmationScreen(props) {
28
- return _jsx(TextLines, { lines: [
29
- `Save overlay — ${props.targetPath} (${props.target === "repo" ? "repository" : "global"} target)`,
30
- "t toggle target · y confirm · n/esc cancel",
31
- ...(props.liveRun ? [RELOAD_GUARD_NOTICE] : []),
32
- "─ diff ─",
33
- props.diff || "(no overlay changes)",
34
- ] });
35
- }
36
- export function StudioScreen({ tabs, active, lines, status = "no staged changes", children, }) {
37
- const tabBar = [
38
- "tickmarkr ui ──",
39
- ...tabs.map((label, index) => `[${index + 1}]${index === active ? "*" : ""}${label}`),
40
- ].join(" ");
41
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: tabBar }), children ?? _jsx(TextLines, { lines: lines }), _jsx(Text, { dimColor: true, children: status })] }));
42
- }
@@ -1,4 +1,4 @@
1
- import type { AuthHealth, WorkerAdapter } from "../../adapters/types.js";
1
+ import { type AuthHealth, type WorkerAdapter } from "../../adapters/types.js";
2
2
  import type { MapEntry, RoutingMode, Tier } from "../../config/config.js";
3
3
  import { type Shape } from "../../graph/schema.js";
4
4
  export type FleetSteeringKey = "review" | "consult";
@@ -40,6 +40,7 @@ type AgentCli = {
40
40
  };
41
41
  export type FleetModelGroup = {
42
42
  adapter: string;
43
+ vendor?: string;
43
44
  rows: Array<{
44
45
  model: string;
45
46
  tier?: Tier;
@@ -51,6 +52,8 @@ export type FleetClassification = {
51
52
  model: string;
52
53
  tier: Tier;
53
54
  note: string;
55
+ vendor?: string;
56
+ channel?: "sub" | "api";
54
57
  };
55
58
  export type FleetModeOption = {
56
59
  id: RoutingMode;