tickmarkr 1.49.0 → 1.51.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.
@@ -17,6 +17,7 @@ export type RoutedAssignment = {
17
17
  /** Advisory only — absent windows config or undeclared model window ⇒ no lint. */
18
18
  export declare function contextWindowLints(tasks: ReadonlyArray<Task>, assignments: ReadonlyArray<RoutedAssignment>, cfg: TickmarkrConfig, repoRoot: string): string[];
19
19
  export declare function seedPreferLints(cfg: TickmarkrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], overlayPreferShapes?: ReadonlySet<string>): string[];
20
+ export declare function mapTierLints(cfg: TickmarkrConfig): string[];
20
21
  export declare function modelLints(cfg: TickmarkrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], opts?: {
21
22
  tty?: boolean;
22
23
  stateDir?: string;
@@ -95,6 +95,14 @@ export function seedPreferLints(cfg, health, adapters, overlayPreferShapes = new
95
95
  }
96
96
  return lints;
97
97
  }
98
+ // v1.51 T3: built-in defaults no longer carry map tiers, so any tier surviving the merge came from an
99
+ // overlay. Deprecated — routing.floors is the single band authority; MapEntrySchema.tier removal itself
100
+ // is deferred to the next version. One lint per shape, identical text on every surface (plan + doctor).
101
+ export function mapTierLints(cfg) {
102
+ return Object.entries(cfg.routing.map)
103
+ .filter(([, entry]) => entry.tier !== undefined)
104
+ .map(([shape, entry]) => `routing.map.${shape}.tier: ${entry.tier} is deprecated — move this value to routing.floors.${shape} (single band authority; tier removal deferred to next version)`);
105
+ }
98
106
  // Diffs detected models (doctor.json) against configured tiers, both directions, per adapter id in cfg.tiers.
99
107
  // No ` ! ` prefix here — the consumer (doctor rows / plan lints) owns that. Pre-v1.5 doctor.json (models:[], no
100
108
  // modelsDetectedAt) is the compat baseline: `?.`/`?? []` everywhere, no zod (would reject old files).
@@ -137,6 +145,7 @@ export function modelLints(cfg, health, adapters, opts) {
137
145
  }
138
146
  }
139
147
  lints.push(...seedPreferLints(cfg, health, adapters, opts?.overlayPreferShapes));
148
+ lints.push(...mapTierLints(cfg)); // v1.51 T3: shared here so plan and doctor draw the exact same message
140
149
  return lints;
141
150
  }
142
151
  // T2/T6: one lint per exclusion, naming the probe reason and date. TTY truncates reasons to 60 chars and
package/dist/brand.d.ts CHANGED
@@ -1,9 +1,71 @@
1
+ import type { OwnedName } from "./drivers/types.js";
1
2
  export declare const BANNER: string;
2
3
  /** ANSI-stripped, trailing-space-trimmed twin of BANNER — README hero and other plain surfaces. */
3
4
  export declare const PLAIN_BANNER: string;
5
+ export declare const PANE_IDENTITY_ENV = "TICKMARKR_PANE_IDENTITY";
6
+ /** One-line pane identity through the T1 ownership contract: role · task · attempt · run. */
7
+ export declare function paneIdentityLine(o: OwnedName): string;
4
8
  export declare function bannerShell(): string;
5
9
  export declare const TICKMARKR_EXIT_TRAILER = "printf '\\nTICKMARKR_''EXIT:%s\\n' $?";
6
10
  /** OBS-50: visible-pane bootstrap script — banner + agent command + byte-identical exit trailer. */
7
11
  export declare function paneDispatchScript(body: string[]): string;
8
12
  /** OBS-50: one short herdr pane-run line; bootstrap lives in the script file beside the prompt. */
9
13
  export declare function paneDispatchCommand(scriptPath: string): string;
14
+ /** The settled brand green ramp (256-color), bright → deep — the BANNER hues. */
15
+ export declare const BRAND_RAMP: readonly [84, 78, 41, 35];
16
+ /** Brand green (ramp anchor 41) — the tickmark hue; also the ok/pass/authed verdict color. */
17
+ export declare const brand: (s: string) => string;
18
+ /** Ok verdicts (pass/authed/green) render in the brand green ramp — same hue as the tickmark. */
19
+ export declare const ok: (s: string) => string;
20
+ /** Fail verdicts (unauthed/red) — red, always paired with the ✗ shape. */
21
+ export declare const fail: (s: string) => string;
22
+ /** Attention (warn/lint) — amber, always paired with the ! shape. */
23
+ export declare const warn: (s: string) => string;
24
+ /** Chrome (legends, rules, parentheticals, inactive state) — dim. */
25
+ export declare const dim: (s: string) => string;
26
+ /** Emphasis (titles, selection, the product name) — bold. */
27
+ export declare const bold: (s: string) => string;
28
+ /** Every semantic color token, for sweeps: each is TTY-gated and NO_COLOR-aware. */
29
+ export declare const TOKENS: {
30
+ readonly brand: (s: string) => string;
31
+ readonly ok: (s: string) => string;
32
+ readonly fail: (s: string) => string;
33
+ readonly warn: (s: string) => string;
34
+ readonly dim: (s: string) => string;
35
+ readonly bold: (s: string) => string;
36
+ };
37
+ /**
38
+ * The glyph vocabulary — plain characters only; color layers on via tokens so
39
+ * shape survives NO_COLOR. Bracket toggles ([x]/[ ]) are forbidden on every surface.
40
+ */
41
+ export declare const GLYPHS: {
42
+ /** Cursor row pointer in list pickers. */
43
+ readonly pointer: "❯";
44
+ /** Active toggle: THE brand tickmark — always rendered via brand(), never brackets. */
45
+ readonly toggleActive: "✓";
46
+ /** Inactive toggle: dim circle — always rendered via dim(). */
47
+ readonly toggleInactive: "○";
48
+ /** Pass verdict. */
49
+ readonly pass: "✓";
50
+ /** Fail verdict. */
51
+ readonly fail: "✗";
52
+ /** Attention/warn verdict. */
53
+ readonly attention: "!";
54
+ /** Neutral/skip — the dash. */
55
+ readonly neutral: "-";
56
+ };
57
+ /** The active toggle as rendered: brand green tickmark on a TTY, plain ✓ otherwise. */
58
+ export declare const toggleActive: () => string;
59
+ /** The inactive toggle as rendered: dim circle on a TTY, plain ○ otherwise. */
60
+ export declare const toggleInactive: () => string;
61
+ /** Dominant title line — one glance answers "what am I looking at". Bold on a TTY. */
62
+ export declare const title: (text: string) => string;
63
+ /** Single dim legend line under a title (key hints) — never scattered, never a paragraph. */
64
+ export declare const legend: (text: string) => string;
65
+ /** Horizontal rule sized to the terminal (capped at 100 cols) — dim chrome. */
66
+ export declare const rule: (width?: number) => string;
67
+ /** Aligned key-value row: dim key, plain value. Padding applied BEFORE styling so columns hold. */
68
+ export declare const kvRow: (key: string, value: string, keyWidth?: number) => string;
69
+ export type Verdict = "pass" | "fail" | "warn" | "neutral";
70
+ /** Status row: verdict glyph FIRST, then the label — glyph-first, distinct shape per verdict. */
71
+ export declare const statusRow: (verdict: Verdict, label: string) => string;
package/dist/brand.js CHANGED
@@ -12,11 +12,21 @@ export const BANNER = [
12
12
  ].join("\n");
13
13
  /** ANSI-stripped, trailing-space-trimmed twin of BANNER — README hero and other plain surfaces. */
14
14
  export const PLAIN_BANNER = BANNER.replace(/\x1b\[[0-9;]*m/g, "").replace(/[ \t]+$/gm, "");
15
- // Shell one-liner that prints the banner inside a pane before a gate command runs. ESC bytes are
15
+ // T5: the pane identity every visible pane announces under the logo. HerdrDriver seeds this env var
16
+ // into each pane shell at slot() time (paneIdentityLine of the pane's T1 owned name); the banner's
17
+ // identity line reads it at pane runtime, so every role — worker/judge/review/consult — wears the
18
+ // same header without the dispatch call sites threading identity through the script.
19
+ export const PANE_IDENTITY_ENV = "TICKMARKR_PANE_IDENTITY";
20
+ /** One-line pane identity through the T1 ownership contract: role · task · attempt · run. */
21
+ export function paneIdentityLine(o) {
22
+ return `${o.role} · ${o.taskId} · attempt ${o.attempt}${o.runId ? ` · ${o.runId}` : ""}`;
23
+ }
24
+ // Shell one-liner that prints the banner inside a pane before a gate command runs, followed by ONE
25
+ // dim identity line (the seeded $TICKMARKR_PANE_IDENTITY, else a bare "tickmarkr"). ESC bytes are
16
26
  // carried as printf %b escapes (never raw control bytes in a command string crossing the herdr socket).
17
27
  export function bannerShell() {
18
28
  const printable = BANNER.replaceAll("\x1b", "\\033").replaceAll("\n", "\\n");
19
- return `printf '%b\\n' '${printable}'`;
29
+ return `printf '%b\\n' '${printable}' "\\033[2m\${${PANE_IDENTITY_ENV}:-tickmarkr}\\033[0m"`;
20
30
  }
21
31
  // OBS-50: quote-split exit marker — herdr echoes the typed command into the transcript that
22
32
  // waitOutput matches, so the literal must not appear unsplit in the dispatch line.
@@ -29,3 +39,65 @@ export function paneDispatchScript(body) {
29
39
  export function paneDispatchCommand(scriptPath) {
30
40
  return `bash ${shq(scriptPath)}`;
31
41
  }
42
+ // ── design system (v1.50) ── contract: docs/codebase/CLI-DESIGN.md ──────────
43
+ // Every cockpit surface styles through these tokens/glyphs/helpers. Styled only
44
+ // on a real TTY with NO_COLOR unset; otherwise output is the plain text itself
45
+ // (non-TTY surfaces stay byte-pinned and machine-consumable).
46
+ /** The settled brand green ramp (256-color), bright → deep — the BANNER hues. */
47
+ export const BRAND_RAMP = [84, 78, 41, 35];
48
+ const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
49
+ const sgr = (code) => (s) => visual() ? `\x1b[${code}m${s}${R}` : s;
50
+ /** Brand green (ramp anchor 41) — the tickmark hue; also the ok/pass/authed verdict color. */
51
+ export const brand = sgr(`38;5;${BRAND_RAMP[2]}`);
52
+ /** Ok verdicts (pass/authed/green) render in the brand green ramp — same hue as the tickmark. */
53
+ export const ok = brand;
54
+ /** Fail verdicts (unauthed/red) — red, always paired with the ✗ shape. */
55
+ export const fail = sgr("31");
56
+ /** Attention (warn/lint) — amber, always paired with the ! shape. */
57
+ export const warn = sgr("33");
58
+ /** Chrome (legends, rules, parentheticals, inactive state) — dim. */
59
+ export const dim = sgr("2");
60
+ /** Emphasis (titles, selection, the product name) — bold. */
61
+ export const bold = sgr("1");
62
+ /** Every semantic color token, for sweeps: each is TTY-gated and NO_COLOR-aware. */
63
+ export const TOKENS = { brand, ok, fail, warn, dim, bold };
64
+ /**
65
+ * The glyph vocabulary — plain characters only; color layers on via tokens so
66
+ * shape survives NO_COLOR. Bracket toggles ([x]/[ ]) are forbidden on every surface.
67
+ */
68
+ export const GLYPHS = {
69
+ /** Cursor row pointer in list pickers. */
70
+ pointer: "❯",
71
+ /** Active toggle: THE brand tickmark — always rendered via brand(), never brackets. */
72
+ toggleActive: "✓",
73
+ /** Inactive toggle: dim circle — always rendered via dim(). */
74
+ toggleInactive: "○",
75
+ /** Pass verdict. */
76
+ pass: "✓",
77
+ /** Fail verdict. */
78
+ fail: "✗",
79
+ /** Attention/warn verdict. */
80
+ attention: "!",
81
+ /** Neutral/skip — the dash. */
82
+ neutral: "-",
83
+ };
84
+ /** The active toggle as rendered: brand green tickmark on a TTY, plain ✓ otherwise. */
85
+ export const toggleActive = () => brand(GLYPHS.toggleActive);
86
+ /** The inactive toggle as rendered: dim circle on a TTY, plain ○ otherwise. */
87
+ export const toggleInactive = () => dim(GLYPHS.toggleInactive);
88
+ /** Dominant title line — one glance answers "what am I looking at". Bold on a TTY. */
89
+ export const title = (text) => bold(text);
90
+ /** Single dim legend line under a title (key hints) — never scattered, never a paragraph. */
91
+ export const legend = (text) => dim(text);
92
+ /** Horizontal rule sized to the terminal (capped at 100 cols) — dim chrome. */
93
+ export const rule = (width = Math.min(process.stdout.columns ?? 80, 100)) => dim("─".repeat(width));
94
+ /** Aligned key-value row: dim key, plain value. Padding applied BEFORE styling so columns hold. */
95
+ export const kvRow = (key, value, keyWidth = 14) => ` ${dim(key.padEnd(keyWidth))} ${value}`;
96
+ const VERDICT = {
97
+ pass: () => ok(GLYPHS.pass),
98
+ fail: () => fail(GLYPHS.fail),
99
+ warn: () => warn(GLYPHS.attention),
100
+ neutral: () => dim(GLYPHS.neutral),
101
+ };
102
+ /** Status row: verdict glyph FIRST, then the label — glyph-first, distinct shape per verdict. */
103
+ export const statusRow = (verdict, label) => `${VERDICT[verdict]()} ${label}`;
@@ -1,36 +1,15 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { allAdapters, detectCandidateClis, formatCandidateCliRow, probeAll, probeModels, readAutoPrefer, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
4
- import { BANNER } from "../../brand.js";
3
+ import { allAdapters, detectCandidateClis, probeAll, probeModels, readAutoPrefer, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
4
+ import { BANNER, dim, fail, kvRow, legend, ok, rule, statusRow, title } from "../../brand.js";
5
5
  import { tickmarkrDir, stateDirName } from "../../graph/graph.js";
6
6
  import { declaredModelWindow, hasWindowsConfig, modelLints, suggestOverlay, ttyVisual } from "../../adapters/model-lints.js";
7
7
  import { DEFAULT_CONFIG, loadConfig, overlayPreferShapes } from "../../config/config.js";
8
8
  import { HerdrDriver } from "../../drivers/herdr.js";
9
9
  import { disallowedBy, excludedChannels, exclusionLine, preferRanks } from "../../route/preference.js";
10
- // same gate + palette as status's audit-ledger frame: styled only on a real TTY, plain otherwise
11
10
  const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
12
- const color = (text, code) => `\x1b[${code}m${text}\x1b[0m`;
13
- // TTY-only line pass: color the verdict symbols, dim the chrome (section titles, parentheticals,
14
- // n/a rows). Padding is untouched — every rewrite swaps equal-width text or wraps it in ANSI.
15
- function stylize(out) {
16
- if (!visual())
17
- return out;
18
- const rule = color("─".repeat(Math.min(process.stdout.columns ?? 80, 100)), 2);
19
- return out.split("\n").map((line) => {
20
- if (line.startsWith("tickmarkr doctor"))
21
- return ` ${color("✓", 32)} ${color("tickmarkr", 1)} doctor ${color("· capability matrix", 2)}\n${rule}`;
22
- if (/^(workspace trust|model status|model drift)/.test(line))
23
- return color(line, 2);
24
- return line
25
- .replace(/^(\s+)✓ /, (_, s) => `${s}${color("✓", 32)} `)
26
- .replace(/^(\s+)✗ /, (_, s) => `${s}${color("✗", 31)} `)
27
- .replace(/^(\s+)! /, (_, s) => `${s}${color("!", 33)} `)
28
- .replace(/^(\s+)= (.*)$/, (_, s, rest) => `${s}${color(`= ${rest}`, 2)}`)
29
- .replace(/(\(auth assumed; verified at dispatch \(failover on auth\/quota errors\)\))/, (m) => color(m, 2))
30
- .replace(/\bauthed\b(?!:)/, color("authed", 32))
31
- .replace(/\bunauthed:/, color("unauthed:", 33));
32
- }).join("\n");
33
- }
11
+ const alignedStatusRow = (verdict, key, value) => ` ${statusRow(verdict, kvRow(key, value).slice(2))}`;
12
+ const attentionRow = (text) => ` ${statusRow("warn", text)}`;
34
13
  export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(), opts = {}) {
35
14
  const cfg = loadConfig(cwd);
36
15
  // banner at START — the logo greets the operator before the ~60s probe wait, never trailing it (operator report 2026-07-17)
@@ -63,48 +42,49 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
63
42
  const rows = adapters.map((a) => {
64
43
  const h = health[a.id];
65
44
  const state = !h.installed ? "not installed" : `${h.version ?? "installed"}${h.note ? ` (${h.note})` : ""}`;
66
- return ` ${h.installed ? "" : ""} ${a.id.padEnd(14)} ${state}`;
45
+ return alignedStatusRow(h.installed ? "pass" : "fail", a.id, state);
67
46
  });
68
47
  // v1.48 T1: advisory sweep for known agent CLIs with no adapter — never written to doctor.json health.
69
- rows.push(...detectCandidateClis().map(formatCandidateCliRow));
70
- rows.push(` ${HerdrDriver.available() ? "✓" : "✗"} herdr ${HerdrDriver.available() ? "driver available (HERDR_ENV=1)" : "not detected — subprocess driver will be used"}`);
48
+ rows.push(...detectCandidateClis().map(({ binary, version }) => alignedStatusRow("warn", binary, `detected: ${version ?? "version unknown"} (no tickmarkr adapter — not routable)`)));
49
+ const herdr = HerdrDriver.available();
50
+ rows.push(alignedStatusRow(herdr ? "pass" : "fail", "herdr", herdr ? "driver available (HERDR_ENV=1)" : "not detected — subprocess driver will be used"));
71
51
  // v1.22 T5: workspace-trust pre-flight — per installed adapter: trusted | seeded | action-required | n/a.
72
52
  // action-required names the exact one-time command (or dialog) the operator must run once.
73
- rows.push("workspace trust:");
53
+ rows.push(legend("workspace trust:"));
74
54
  for (const a of adapters) {
75
55
  if (!health[a.id]?.installed)
76
56
  continue;
77
57
  if (!a.trust) {
78
- rows.push(` = ${a.id.padEnd(14)} trust: n/a`);
58
+ rows.push(` ${dim("=")} ${kvRow(a.id, "trust: n/a").slice(2)}`);
79
59
  continue;
80
60
  }
81
61
  try {
82
62
  const v = a.trust(cwd);
83
63
  if (v.status === "trusted")
84
- rows.push(` ✓ ${a.id.padEnd(14)} trust: trusted`);
64
+ rows.push(alignedStatusRow("pass", a.id, "trust: trusted"));
85
65
  else if (v.status === "seeded")
86
- rows.push(` ✓ ${a.id.padEnd(14)} trust: seeded`);
66
+ rows.push(alignedStatusRow("pass", a.id, "trust: seeded"));
87
67
  else
88
- rows.push(` ! ${a.id.padEnd(14)} trust: action-required — run ONCE: ${v.command}`);
68
+ rows.push(alignedStatusRow("warn", a.id, `trust: action-required — run ONCE: ${v.command}`));
89
69
  }
90
70
  catch (e) {
91
71
  // fail closed on the trust line only — never abort the rest of doctor
92
- rows.push(` ! ${a.id.padEnd(14)} trust: action-required — run ONCE: (trust check failed: ${e instanceof Error ? e.message : String(e)})`);
72
+ rows.push(alignedStatusRow("warn", a.id, `trust: action-required — run ONCE: (trust check failed: ${e instanceof Error ? e.message : String(e)})`));
93
73
  }
94
74
  }
95
75
  for (const [role, sel] of [["judge", cfg.judge], ["consult", cfg.consult]]) {
96
76
  if (!health[sel.adapter]?.installed) {
97
- rows.push(` ! ${role} runs on ${sel.adapter}:${sel.model} — NOT installed; that gate will fail closed until you install it or remap cfg.${role}`);
77
+ rows.push(attentionRow(`${role} runs on ${sel.adapter}:${sel.model} — NOT installed; that gate will fail closed until you install it or remap cfg.${role}`));
98
78
  }
99
79
  }
100
- rows.push(...modelLints(cfg, health, adapters, { tty: ttyVisual(), stateDir: stateDirName(cwd), overlayPreferShapes: overlayPreferShapes(cwd) }).map((l) => ` ! ${l}`));
80
+ rows.push(...modelLints(cfg, health, adapters, { tty: ttyVisual(), stateDir: stateDirName(cwd), overlayPreferShapes: overlayPreferShapes(cwd) }).map(attentionRow));
101
81
  const excluded = excludedChannels(cfg, adapters, health);
102
82
  if (excluded.length)
103
- rows.push(` ! ${exclusionLine(excluded)}`);
83
+ rows.push(attentionRow(exclusionLine(excluded)));
104
84
  // HYG-07(a): doctor just probed fresh (probeAll above), so servability attribution is current by construction.
105
85
  const servable = servableExclusions(cfg, adapters, health);
106
86
  if (servable.length)
107
- rows.push(` ! ${servabilityLine(servable)}`);
87
+ rows.push(attentionRow(servabilityLine(servable)));
108
88
  // MODEL-05/06: print-only drift fragment; advisory, whole-line-commented additions, tickmarkr NEVER applies it.
109
89
  // TTY gets a one-line summary + the fragment as a file (the full dump drowned everything else,
110
90
  // v1.33.1 onboarding); machine/CI surface keeps the inline dump — layout is pinned by tests.
@@ -114,7 +94,7 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
114
94
  if (visual()) {
115
95
  const overlayPath = join(tickmarkrDir(cwd), "doctor-overlay.yaml");
116
96
  writeFileSync(overlayPath, frag);
117
- drift = `\nmodel drift: unclassified models detected — paste-ready overlay written to ${overlayPath} (advisory; tickmarkr never applies it)`;
97
+ drift = `\n ${statusRow("warn", `model drift: unclassified models detected — paste-ready overlay written to ${overlayPath} ${dim("(advisory; tickmarkr never applies it)")}`)}`;
118
98
  }
119
99
  else {
120
100
  drift = `\nmodel drift — paste-ready overlay (advisory; tickmarkr never applies):\n${frag}`;
@@ -137,20 +117,24 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
137
117
  if (!models.length && !unclassified.length)
138
118
  return [];
139
119
  const w = Math.max(8, ...models.map((m) => m.length));
140
- const rows = [` ${a.id}`];
120
+ const rows = [` ${dim(a.id)}`];
141
121
  for (const m of models) {
142
122
  const v = h.modelAuth?.[m];
143
- const auth = !v ? "unknown" : v.authed ? "authed" : `unauthed: ${trunc(v.reason ?? "probe failed", 40)} (${dateOf(v.probedAt)})`;
123
+ const auth = !v
124
+ ? dim("unknown")
125
+ : v.authed
126
+ ? ok("authed")
127
+ : `${fail("unauthed:")} ${trunc(v.reason ?? "probe failed", 40)} (${dateOf(v.probedAt)})`;
144
128
  const d = disallowedBy({ adapter: a.id, model: m }, cfg.routing);
145
129
  const denied = d?.by === "deny" ? d.entry : "—";
146
130
  const pref = preferRanks({ adapter: a.id, model: m }, cfg).map((p) => `${p.shape}#${p.rank}`).join(",") || "—";
147
131
  const windowCol = showWindows
148
132
  ? ` ${String(declaredModelWindow(cfg, a.id, m) ?? "—").padEnd(8)}`
149
133
  : "";
150
- rows.push(` ${m.padEnd(w)} ${classified[m].padEnd(8)}${windowCol} ${auth} denied=${denied} prefer=${pref}`);
134
+ rows.push(` ${m.padEnd(w)} ${classified[m].padEnd(8)}${windowCol} ${auth} ${dim("denied=")}${denied} ${dim("prefer=")}${pref}`);
151
135
  }
152
136
  if (unclassified.length)
153
- rows.push(` (${unclassified.length} more listed, unclassified)`);
137
+ rows.push(` ${dim(`(${unclassified.length} more listed, unclassified)`)}`);
154
138
  return rows;
155
139
  });
156
140
  const autoPrefer = readAutoPrefer(cwd);
@@ -162,11 +146,14 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
162
146
  const seed = DEFAULT_CONFIG.routing.map[shape]?.prefer ?? [];
163
147
  if (!auto.length && !seed.length)
164
148
  return []; // nothing derived, nothing seeded — an empty line is noise
165
- return [` prefer ${shape} (auto): ${auto.join(" > ")} — seed was [${seed.join(", ")}]`];
149
+ return [` ${dim(`prefer ${shape} (auto):`)} ${auto.join(" > ")} ${dim("— seed was")} [${seed.join(", ")}]`];
166
150
  })
167
151
  : [];
168
152
  const modelSummary = modelStatus.length || preferStatus.length
169
- ? `\nmodel status:\n${[...modelStatus, ...preferStatus].join("\n")}`
153
+ ? `\n${legend("model status:")}\n${[...modelStatus, ...preferStatus].join("\n")}`
170
154
  : "";
171
- return stylize(`tickmarkr doctor — capability matrix:\n${rows.join("\n")}${modelSummary}${drift}\nwrote ${stateDirName(cwd)}/doctor.json`);
155
+ const header = visual()
156
+ ? `${statusRow("pass", `${title("tickmarkr doctor")} ${legend("· capability matrix")}`)}\n${rule()}`
157
+ : "tickmarkr doctor — capability matrix:";
158
+ return `${header}\n${rows.join("\n")}${modelSummary}${drift}\nwrote ${stateDirName(cwd)}/doctor.json`;
172
159
  }
@@ -5,15 +5,14 @@ import { createInterface } from "node:readline/promises";
5
5
  import { parseArgs } from "node:util";
6
6
  import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse, readAutoPrefer } from "../../adapters/registry.js";
7
7
  import { fleetUnclassifiedModels } from "../../adapters/model-lints.js";
8
- import { fleetEditableFromConfig, fleetEditableEquals, fleetRepoOverlayFromDelta, formatFleetPrint, globalConfigDir, loadConfig, overlayPreferShapes, readOverlayFile, repoOverlayPath, repoOverlayYaml, unifiedYamlDiff, } from "../../config/config.js";
8
+ import { GLYPHS, bold, legend as legendText, toggleActive, toggleInactive } from "../../brand.js";
9
+ import { fleetEditableFromConfig, fleetEditableEquals, fleetRepoOverlayFromDelta, formatFleetPrint, globalConfigDir, overlayPreferShapes, readOverlayFile, repoOverlayPath, repoOverlayYaml, ROUTING_MODES, unifiedYamlDiff, } from "../../config/config.js";
9
10
  import { SHAPES, TIERS } from "../../graph/schema.js";
10
11
  import { route } from "../../route/router.js";
12
+ import { resolveRunMode } from "../../run/daemon.js";
11
13
  import { loadRoutingProfile } from "../../run/journal.js";
12
14
  const NON_TTY_MSG = "tickmarkr fleet: interactive fleet editor requires a TTY — use `tickmarkr fleet --print` for non-interactive output";
13
15
  const QUIT = "fleet: quit without writing";
14
- const BOLD = (s) => `\x1b[1m${s}\x1b[0m`;
15
- const DIM = (s) => `\x1b[2m${s}\x1b[0m`;
16
- const POINTER = "❯";
17
16
  const isDeniedAdapter = (id, editable) => editable.denyAdapters.includes(id);
18
17
  const isDeniedModel = (adapter, model, editable) => editable.denyModels.includes(`${adapter}:${model}`);
19
18
  function previewTask(shape) {
@@ -57,10 +56,21 @@ function provenanceMap(editable) {
57
56
  }
58
57
  return out;
59
58
  }
60
- function proposedOverlayText(initial, editable, repoRoot) {
61
- const merged = fleetRepoOverlayFromDelta(initial, editable, readOverlayFile(repoOverlayPath(repoRoot)));
62
- return repoOverlayYaml(merged, provenanceMap(editable));
59
+ // v1.51 T4: serializeFleetOverlay predates routing.mode — splice the mode line under routing:
60
+ // so a repo-declared mode survives fleet writes and a mode selection lands as routing.mode.
61
+ function withModeLine(yaml, mode) {
62
+ if (!mode)
63
+ return yaml;
64
+ if (/^routing:$/m.test(yaml))
65
+ return yaml.replace(/^routing:$/m, `routing:\n mode: ${mode}`);
66
+ return `routing:\n mode: ${mode}\n${yaml}`;
63
67
  }
68
+ // v1.51 T4: one gloss per routing mode on the fleet mode screen — mirrors the preset compiler.
69
+ const MODE_GLOSS = {
70
+ "partner-led": "every shape frontier · explore off",
71
+ "risk-based": "risk-tiered default floors",
72
+ "staff-led": "implement/refactor one band down · integrity shapes hold frontier",
73
+ };
64
74
  const isAbort = (k) => k.name === "escape" || k.name === "q" || (k.ctrl === true && k.name === "c");
65
75
  const isNext = (k) => k.name === "return" || k.name === "enter";
66
76
  const isToggle = (k) => k.name === "space";
@@ -75,8 +85,8 @@ function moveCursor(k, cursor, count) {
75
85
  }
76
86
  // step title (bold) + one dim key legend + rows; exactly one pointer glyph on the cursor row
77
87
  function listFrame(title, legend, rows, cursor) {
78
- const lines = [BOLD(title), DIM(legend)];
79
- rows.forEach((r, i) => lines.push(i === cursor ? `${POINTER} ${BOLD(r)}` : ` ${r}`));
88
+ const lines = [bold(title), legendText(legend)];
89
+ rows.forEach((r, i) => lines.push(i === cursor ? `${GLYPHS.pointer} ${bold(r)}` : ` ${r}`));
80
90
  return lines;
81
91
  }
82
92
  // Keypress terminal over the (injectable) input stream. Keypress events are decoded by
@@ -156,8 +166,14 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
156
166
  const input = io.input ?? process.stdin;
157
167
  const output = io.output ?? process.stdout;
158
168
  const interactive = input.isTTY === true && output.isTTY === true;
159
- if (print)
160
- return formatFleetPrint(cwd, { globalDir });
169
+ if (print) {
170
+ // v1.51 T4: the print surface names the mode and its source layer right under the header —
171
+ // comment-prefixed so the YAML body stays machine-parseable and regex-stable.
172
+ const rm = resolveRunMode(cwd, { globalDir });
173
+ const body = formatFleetPrint(cwd, { globalDir });
174
+ const nl = body.indexOf("\n");
175
+ return `${body.slice(0, nl)}\n# mode: ${rm.mode.mode} (${rm.source})${body.slice(nl)}`;
176
+ }
161
177
  if (!interactive)
162
178
  return { out: NON_TTY_MSG, code: 1 };
163
179
  const fresh = values.fresh ?? false;
@@ -168,15 +184,16 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
168
184
  code: 1,
169
185
  };
170
186
  }
171
- const cfg = loadConfig(cwd, { globalDir });
187
+ const rm = resolveRunMode(cwd, { globalDir });
188
+ const cfg = rm.cfg;
172
189
  const initial = fleetEditableFromConfig(cfg);
173
190
  const editable = structuredClone(initial);
174
191
  const health = cached;
175
192
  const term = openTerm(input, output);
176
193
  try {
177
- // step 1/4 — probe data
194
+ // step 1/5 — probe data
178
195
  const ageLine = formatAge(doctorAgeMs(cwd));
179
- term.frame(listFrame("step 1/4 · probe data", "enter continue · r refresh via doctor · esc/q quit", [`probe data: ${ageLine} (.tickmarkr/doctor.json)`], 0));
196
+ term.frame(listFrame("step 1/5 · probe data", "enter continue · r refresh via doctor · esc/q quit", [`probe data: ${ageLine} (.tickmarkr/doctor.json)`], 0));
180
197
  for (;;) {
181
198
  const k = await term.key();
182
199
  if (isAbort(k))
@@ -187,16 +204,16 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
187
204
  if (isNext(k))
188
205
  break;
189
206
  }
190
- // step 2/4 — agent CLIs (space toggles adapter deny)
207
+ // step 2/5 — agent CLIs (space toggles adapter deny)
191
208
  const installed = adapters.filter((a) => health[a.id]?.installed);
192
209
  const adapterRows = () => installed.map((a) => {
193
210
  const h = health[a.id];
194
211
  const on = !isDeniedAdapter(a.id, editable);
195
- return `[${on ? "x" : " "}] ${a.id} ${h?.version ?? "installed"} ${h?.authed ? "authed" : "unauthed"}`;
212
+ return `${on ? toggleActive() : toggleInactive()} ${a.id} ${h?.version ?? "installed"} ${h?.authed ? "authed" : "unauthed"}`;
196
213
  });
197
214
  const listLegend = "↑↓/jk move · space toggle · enter next · esc/q quit";
198
215
  let aCursor = 0;
199
- term.frame(listFrame("step 2/4 · agent CLIs", listLegend, adapterRows(), aCursor));
216
+ term.frame(listFrame("step 2/5 · agent CLIs", listLegend, adapterRows(), aCursor));
200
217
  for (;;) {
201
218
  const k = await term.key();
202
219
  if (isAbort(k))
@@ -220,9 +237,9 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
220
237
  changed = true;
221
238
  }
222
239
  if (changed)
223
- term.frame(listFrame("step 2/4 · agent CLIs", listLegend, adapterRows(), aCursor));
240
+ term.frame(listFrame("step 2/5 · agent CLIs", listLegend, adapterRows(), aCursor));
224
241
  }
225
- // step 3/4 — models per enabled adapter (space toggles deny, t assigns tier from the row)
242
+ // step 3/5 — models per enabled adapter (space toggles deny, t assigns tier from the row)
226
243
  const enabledAdapters = installed.filter((a) => !isDeniedAdapter(a.id, editable));
227
244
  for (const a of enabledAdapters) {
228
245
  const unclassifiedAll = fleetUnclassifiedModels(cfg, health, adapters).filter((u) => u.adapter === a.id);
@@ -238,9 +255,9 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
238
255
  }
239
256
  const tier = editable.tiers[a.id]?.[r.model];
240
257
  const denied = isDeniedModel(a.id, r.model, editable);
241
- return `[${denied ? " " : "x"}] ${r.model} ${tier?.tier ?? "???"} ${denied ? "denied" : "allowed"}`;
258
+ return `${denied ? toggleInactive() : toggleActive()} ${r.model} ${tier?.tier ?? "???"} ${denied ? "denied" : "allowed"}`;
242
259
  });
243
- const title = `step 3/4 · models · ${a.id}`;
260
+ const title = `step 3/5 · models · ${a.id}`;
244
261
  const modelsLegend = "↑↓/jk move · space toggle · t tier · enter next · esc/q quit";
245
262
  let mCursor = 0;
246
263
  term.frame(listFrame(title, modelsLegend, renderRows(rowsData()), mCursor));
@@ -285,7 +302,44 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
285
302
  term.frame(listFrame(title, modelsLegend, renderRows(rowsData()), mCursor));
286
303
  }
287
304
  }
288
- // step 4/4shape routing (typed entry for pin/tier/prefer from the highlighted row)
305
+ // step 4/5 — routing mode (selection is in-memory; the write happens only through the diff confirm).
306
+ // Candidate floor tables come from the ONE preset compiler via resolveRunMode — no mode math here.
307
+ const modeCfgs = Object.fromEntries(ROUTING_MODES.map((m) => [m, m === rm.mode.mode ? rm : resolveRunMode(cwd, { flag: m, globalDir })]));
308
+ const modeRows = () => ROUTING_MODES.map((m) => `${m === rm.mode.mode ? toggleActive() : " "} ${m.padEnd(11)} ${MODE_GLOSS[m]}`);
309
+ // preview: the highlighted mode's resolved floor table diffed against the current mode
310
+ const floorPreview = (cand) => {
311
+ if (cand === rm.mode.mode)
312
+ return [];
313
+ const cur = cfg.routing.floors;
314
+ const next = modeCfgs[cand].cfg.routing.floors;
315
+ const changed = SHAPES.filter((s) => cur[s] !== next[s]);
316
+ return [
317
+ ` floors vs ${rm.mode.mode}:`,
318
+ ...(changed.length ? changed.map((s) => ` ${s}: ${cur[s]} → ${next[s]}`) : [" (no floor changes)"]),
319
+ ];
320
+ };
321
+ const modeTitle = "step 4/5 · routing mode";
322
+ const modeLegend = "↑↓/jk move · enter select · esc/q quit";
323
+ let modeCursor = ROUTING_MODES.indexOf(rm.mode.mode);
324
+ let selectedMode = rm.mode.mode;
325
+ const modeFrame = () => [...listFrame(modeTitle, modeLegend, modeRows(), modeCursor), ...floorPreview(ROUTING_MODES[modeCursor])];
326
+ term.frame(modeFrame());
327
+ for (;;) {
328
+ const k = await term.key();
329
+ if (isAbort(k))
330
+ return QUIT;
331
+ if (isNext(k)) {
332
+ selectedMode = ROUTING_MODES[modeCursor];
333
+ break;
334
+ }
335
+ const moved = moveCursor(k, modeCursor, ROUTING_MODES.length);
336
+ if (moved !== modeCursor) {
337
+ modeCursor = moved;
338
+ term.frame(modeFrame());
339
+ }
340
+ }
341
+ // step 5/5 — shape routing (typed entry for pin/tier/prefer from the highlighted row);
342
+ // previews route under the SELECTED mode's resolved floors, never a floor edit of its own
289
343
  const channels = discoverChannels(cfg, adapters, health);
290
344
  const profile = loadRoutingProfile(cwd, cfg, { preview: true });
291
345
  const autoPrefer = readAutoPrefer(cwd);
@@ -293,7 +347,7 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
293
347
  const shapeRows = () => SHAPES.map((shape) => {
294
348
  let now;
295
349
  try {
296
- const r = route(previewTask(shape), { ...cfg, routing: { ...cfg.routing, map: editable.map, floors: editable.floors } }, channels, profile);
350
+ const r = route(previewTask(shape), { ...cfg, routing: { ...cfg.routing, map: editable.map, floors: modeCfgs[selectedMode].cfg.routing.floors } }, channels, profile);
297
351
  now = `${r.assignment.adapter}:${r.assignment.model} (${r.assignment.channel}, ${r.assignment.tier})`;
298
352
  }
299
353
  catch (e) {
@@ -302,7 +356,7 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
302
356
  const auto = autoPrefer?.[shape] && !overlayShapes.has(shape) ? " (auto-prefer active)" : "";
303
357
  return `${shape} → ${now}${auto}`;
304
358
  });
305
- const shapesTitle = "step 4/4 · shape routing";
359
+ const shapesTitle = "step 5/5 · shape routing";
306
360
  const shapesLegend = "↑↓/jk move · a auto · p pin · t tier · f prefer · enter next · esc/q quit";
307
361
  let sCursor = 0;
308
362
  term.frame(listFrame(shapesTitle, shapesLegend, shapeRows(), sCursor));
@@ -351,10 +405,17 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
351
405
  if (changed)
352
406
  term.frame(listFrame(shapesTitle, shapesLegend, shapeRows(), sCursor));
353
407
  }
354
- // review — unified diff + typed confirm
408
+ // review — unified diff + typed confirm (the ONLY write path; the mode screen funnels through here)
355
409
  const before = currentRepoOverlayText(cwd);
356
- const after = proposedOverlayText(initial, editable, cwd);
357
- if (fleetEditableEquals(initial, editable) || before === after) {
410
+ const existing = readOverlayFile(repoOverlayPath(cwd));
411
+ const modeChanged = selectedMode !== rm.mode.mode;
412
+ // preserve a repo-declared mode line verbatim; write a new one only when the selection changed it
413
+ const writeMode = modeChanged ? selectedMode : existing.routing?.mode;
414
+ const merged = fleetEditableEquals(initial, editable)
415
+ ? structuredClone(existing)
416
+ : fleetRepoOverlayFromDelta(initial, editable, existing);
417
+ const after = withModeLine(repoOverlayYaml(merged, provenanceMap(editable)), writeMode);
418
+ if ((!modeChanged && fleetEditableEquals(initial, editable)) || before === after) {
358
419
  return "fleet: no overlay changes (empty diff)";
359
420
  }
360
421
  const diff = unifiedYamlDiff(before, after, repoOverlayPath(cwd));
@@ -362,11 +423,9 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
362
423
  const confirm = await term.askTyped("write overlay? [y/N] ");
363
424
  if (!/^y(?:es)?$/i.test(confirm))
364
425
  return "fleet: discarded overlay changes";
365
- const merged = fleetRepoOverlayFromDelta(initial, editable, readOverlayFile(repoOverlayPath(cwd)));
366
- const body = repoOverlayYaml(merged, provenanceMap(editable));
367
426
  const path = repoOverlayPath(cwd);
368
427
  mkdirSync(dirname(path), { recursive: true });
369
- writeFileSync(path, body);
428
+ writeFileSync(path, after);
370
429
  return `fleet: wrote ${path}`;
371
430
  }
372
431
  finally {
@@ -5,7 +5,7 @@ import { parseArgs } from "node:util";
5
5
  import { allAdapters, formatDoctorAgeForInit, formatDoctorReport, initDoctorReuse } from "../../adapters/registry.js";
6
6
  import { configTemplate, DEFAULT_CONFIG, globalConfigDir, loadConfig } from "../../config/config.js";
7
7
  import { LEGACY_PREFIX, specTemplate } from "../../compile/native.js";
8
- import { BANNER } from "../../brand.js";
8
+ import { BANNER, kvRow, legend, rule, statusRow, title } from "../../brand.js";
9
9
  import { tickmarkrDir } from "../../graph/graph.js";
10
10
  import { Journal } from "../../run/journal.js";
11
11
  import { doctor } from "./doctor.js";
@@ -264,5 +264,25 @@ export async function init(argv, cwd = process.cwd()) {
264
264
  }
265
265
  if (values.agent)
266
266
  await installAgentFiles(cwd, values.force ?? false, values.docs ?? false, notes);
267
- return `${notes.join("\n")}\n${doc}\n${nextSteps(cwd, SCAFFOLD_SPEC)}\n${ENVIRONMENTS_FOOTER}`;
267
+ const next = nextSteps(cwd, SCAFFOLD_SPEC);
268
+ if (!visual())
269
+ return `${notes.join("\n")}\n${doc}\n${next}\n${ENVIRONMENTS_FOOTER}`;
270
+ const noteRows = notes.map((note) => ` ${statusRow(/^(?:wrote|overwrote|appended)/.test(note) ? "pass" : note.startsWith("skipped") ? "warn" : "neutral", note)}`);
271
+ const footerRows = ENVIRONMENTS_FOOTER.split("\n").slice(1).map((line) => {
272
+ const separator = line.indexOf(" — ");
273
+ return kvRow(line.slice(2, separator), line.slice(separator + 1), 12);
274
+ });
275
+ return [
276
+ title("tickmarkr init"),
277
+ legend("· setup notes"),
278
+ rule(),
279
+ ...noteRows,
280
+ doc,
281
+ title("next steps"),
282
+ legend("· continue from the repository's current state"),
283
+ rule(),
284
+ kvRow("next", next.replace(/^next:\s*/, "")),
285
+ legend("environments:"),
286
+ ...footerRows,
287
+ ].join("\n");
268
288
  }
@@ -1,2 +1,2 @@
1
1
  import type { WorkerAdapter } from "../../adapters/types.js";
2
- export declare function plan(_argv: string[], cwd?: string, adapters?: WorkerAdapter[]): Promise<string>;
2
+ export declare function plan(argv: string[], cwd?: string, adapters?: WorkerAdapter[]): Promise<string>;