tickmarkr 1.49.0 → 1.50.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.
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,13 @@ 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 { GLYPHS, bold, legend as legendText, toggleActive, toggleInactive } from "../../brand.js";
8
9
  import { fleetEditableFromConfig, fleetEditableEquals, fleetRepoOverlayFromDelta, formatFleetPrint, globalConfigDir, loadConfig, overlayPreferShapes, readOverlayFile, repoOverlayPath, repoOverlayYaml, unifiedYamlDiff, } from "../../config/config.js";
9
10
  import { SHAPES, TIERS } from "../../graph/schema.js";
10
11
  import { route } from "../../route/router.js";
11
12
  import { loadRoutingProfile } from "../../run/journal.js";
12
13
  const NON_TTY_MSG = "tickmarkr fleet: interactive fleet editor requires a TTY — use `tickmarkr fleet --print` for non-interactive output";
13
14
  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
15
  const isDeniedAdapter = (id, editable) => editable.denyAdapters.includes(id);
18
16
  const isDeniedModel = (adapter, model, editable) => editable.denyModels.includes(`${adapter}:${model}`);
19
17
  function previewTask(shape) {
@@ -75,8 +73,8 @@ function moveCursor(k, cursor, count) {
75
73
  }
76
74
  // step title (bold) + one dim key legend + rows; exactly one pointer glyph on the cursor row
77
75
  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}`));
76
+ const lines = [bold(title), legendText(legend)];
77
+ rows.forEach((r, i) => lines.push(i === cursor ? `${GLYPHS.pointer} ${bold(r)}` : ` ${r}`));
80
78
  return lines;
81
79
  }
82
80
  // Keypress terminal over the (injectable) input stream. Keypress events are decoded by
@@ -192,7 +190,7 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
192
190
  const adapterRows = () => installed.map((a) => {
193
191
  const h = health[a.id];
194
192
  const on = !isDeniedAdapter(a.id, editable);
195
- return `[${on ? "x" : " "}] ${a.id} ${h?.version ?? "installed"} ${h?.authed ? "authed" : "unauthed"}`;
193
+ return `${on ? toggleActive() : toggleInactive()} ${a.id} ${h?.version ?? "installed"} ${h?.authed ? "authed" : "unauthed"}`;
196
194
  });
197
195
  const listLegend = "↑↓/jk move · space toggle · enter next · esc/q quit";
198
196
  let aCursor = 0;
@@ -238,7 +236,7 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
238
236
  }
239
237
  const tier = editable.tiers[a.id]?.[r.model];
240
238
  const denied = isDeniedModel(a.id, r.model, editable);
241
- return `[${denied ? " " : "x"}] ${r.model} ${tier?.tier ?? "???"} ${denied ? "denied" : "allowed"}`;
239
+ return `${denied ? toggleInactive() : toggleActive()} ${r.model} ${tier?.tier ?? "???"} ${denied ? "denied" : "allowed"}`;
242
240
  });
243
241
  const title = `step 3/4 · models · ${a.id}`;
244
242
  const modelsLegend = "↑↓/jk move · space toggle · t tier · enter next · esc/q quit";
@@ -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,5 +1,6 @@
1
1
  import { allAdapters, discoverChannels, doctorAgeMs, modelAuthExclusions, probeAll, readDoctor, servableExclusions, servabilityLine } from "../../adapters/registry.js";
2
2
  import { formatModelAuthLine, contextWindowLints, modelLints, ttyVisual } from "../../adapters/model-lints.js";
3
+ import { GLYPHS, dim, rule, title, warn } from "../../brand.js";
3
4
  import { collateralLints } from "../../compile/collateral.js";
4
5
  import { loadConfig } from "../../config/config.js";
5
6
  import { loadGraph } from "../../graph/graph.js";
@@ -7,6 +8,22 @@ import { excludedChannels, exclusionLine } from "../../route/preference.js";
7
8
  import { route, RoutingError } from "../../route/router.js";
8
9
  import { modelId } from "../../gates/review.js";
9
10
  import { loadRoutingProfile } from "../../run/journal.js";
11
+ // T4 (v1.50): TTY-only brand pass — the title helper frames the routing table, lint/unroutable
12
+ // markers carry the attention glyph, section labels dim to chrome (the doctor/status system).
13
+ // Gated on ttyVisual(): the non-TTY surface returns untouched (byte-pinned, machine-consumable).
14
+ const stylizePlan = (out) => {
15
+ if (!ttyVisual())
16
+ return out;
17
+ return out.split("\n").map((line, i) => {
18
+ if (i === 0)
19
+ return `${title(line)}\n${rule()}`;
20
+ if (/^(routing|context window|scope) lints:$/.test(line))
21
+ return dim(line);
22
+ return line
23
+ .replace(/^(\s+)! /, (_, s) => `${s}${warn(GLYPHS.attention)} `)
24
+ .replace(/^( \S+\s+\S+\s+)!! /, (_, p) => `${p}${warn("!!")} `);
25
+ }).join("\n");
26
+ };
10
27
  const fleetCanCrossVendorReview = (channels) => {
11
28
  for (let i = 0; i < channels.length; i++)
12
29
  for (let j = 0; j < channels.length; j++)
@@ -114,5 +131,5 @@ export async function plan(_argv, cwd = process.cwd(), adapters = allAdapters())
114
131
  const scopeLints = collateralLints(g.tasks, cwd);
115
132
  if (scopeLints.length)
116
133
  lines.push("", "scope lints:", ...scopeLints.map((l) => ` ! ${l}`));
117
- return lines.join("\n");
134
+ return stylizePlan(lines.join("\n"));
118
135
  }
@@ -1,5 +1,7 @@
1
1
  import { parseArgs } from "node:util";
2
+ import { ttyVisual } from "../../adapters/model-lints.js";
2
3
  import { addUsage } from "../../adapters/types.js";
4
+ import { dim, rule, title } from "../../brand.js";
3
5
  import { loadConfig } from "../../config/config.js";
4
6
  import { estimateCosts } from "../../report/cost.js";
5
7
  import { cellsOf, cellSummary } from "../../route/profile.js";
@@ -287,6 +289,16 @@ function textReport(runId, events, rows, cwd) {
287
289
  ` probes this run (route-deviation explore): ${probes}`,
288
290
  ].join("\n");
289
291
  }
292
+ // T4 (v1.50): TTY-only brand pass over the text report — title frame + dim section chrome; row
293
+ // text and alignment untouched (the doctor/status system). Gated on ttyVisual(): the non-TTY
294
+ // surface returns untouched, and --md never styles (the record is a document surface).
295
+ const stylizeReport = (out) => {
296
+ if (!ttyVisual())
297
+ return out;
298
+ return out
299
+ .replace(/^.*$/m, (first) => `${title(first)}\n${rule()}`) // non-global /m ⇒ first line only
300
+ .replace(/^(engagement summary — audit trail:|spend — tokens[^\n]*|spend — money:|learning \([^\n]*)$/gm, (l) => dim(l));
301
+ };
290
302
  export async function report(argv, cwd = process.cwd()) {
291
303
  const { values, positionals } = parseArgs({
292
304
  args: argv,
@@ -302,5 +314,5 @@ export async function report(argv, cwd = process.cwd()) {
302
314
  const rows = j.readTelemetry();
303
315
  return renderMarkdownRecord(runId, events, estimateCosts(rows, loadConfig(cwd).cost), rows);
304
316
  }
305
- return textReport(runId, events, j.readTelemetry(), cwd);
317
+ return stylizeReport(textReport(runId, events, j.readTelemetry(), cwd));
306
318
  }
@@ -1,3 +1,5 @@
1
+ import { type JournalEvent } from "../../run/journal.js";
2
+ export declare const narrationLine: (event: JournalEvent) => string;
1
3
  export declare function run(argv: string[], cwd?: string): Promise<{
2
4
  out: string;
3
5
  code: number;
@@ -6,6 +6,22 @@ import { loadGraph } from "../../graph/graph.js";
6
6
  import { formatSummary, runDaemon } from "../../run/daemon.js";
7
7
  import { route, QUALITY_ENV, NO_EXPLORE_ENV } from "../../route/router.js";
8
8
  import { formatJournalNarration, loadRoutingProfile } from "../../run/journal.js";
9
+ import { ttyVisual } from "../../adapters/model-lints.js";
10
+ import { statusRow } from "../../brand.js";
11
+ // T4 (v1.50): lifecycle verdict glyphs on the live narration stream — glyph-first, message text
12
+ // unchanged (the doctor/status visual system). TTY-gated: the piped narration surface stays
13
+ // byte-identical to formatJournalNarration.
14
+ const NARRATION_VERDICTS = {
15
+ "task-dispatch": "neutral",
16
+ "task-done": "pass",
17
+ "task-failed": "fail",
18
+ "task-human": "warn",
19
+ };
20
+ export const narrationLine = (event) => {
21
+ const line = formatJournalNarration(event);
22
+ const verdict = NARRATION_VERDICTS[event.event];
23
+ return verdict !== undefined && ttyVisual() ? statusRow(verdict, line) : line;
24
+ };
9
25
  const summaryGreen = (s) => s.failed.length === 0 && s.human.length === 0 && s.blocked.length === 0 && s.pending.length === 0
10
26
  && s.tipVerify !== "failed";
11
27
  export async function run(argv, cwd = process.cwd()) {
@@ -46,7 +62,7 @@ export async function run(argv, cwd = process.cwd()) {
46
62
  const s = await runDaemon(cwd, {
47
63
  concurrency: values.concurrency ? Number(values.concurrency) : undefined,
48
64
  driver: pickDriver(cfg, values.driver),
49
- narrate: (event) => console.log(formatJournalNarration(event)),
65
+ narrate: (event) => console.log(narrationLine(event)),
50
66
  });
51
67
  const out = `run ${s.runId} finished — ${formatSummary(s)} (merge to main is a human decision)`;
52
68
  return { out, code: summaryGreen(s) ? 0 : 2 };
@@ -1,4 +1,4 @@
1
- import { BANNER } from "../../brand.js";
1
+ import { BANNER, GLYPHS, dim, fail, legend, ok, rule, statusRow, title, warn } from "../../brand.js";
2
2
  import { blockedTasks, graphDefinitionHash, loadGraph, pendingTasks } from "../../graph/graph.js";
3
3
  import { GATE_NAMES } from "../../graph/schema.js";
4
4
  import { Journal, engagementComparable } from "../../run/journal.js";
@@ -19,17 +19,8 @@ const attemptStartIdx = (events, taskId) => {
19
19
  return idx;
20
20
  };
21
21
  const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
22
- const color = (text, code, enabled) => enabled ? `\x1b[${code}m${text}\x1b[0m` : text;
23
- const taskBox = (status, unicode) => {
24
- if (unicode) {
25
- if (status === "done")
26
- return "✓";
27
- if (status === "failed")
28
- return "✗";
29
- if (status === "human")
30
- return "⏸";
31
- return "☐";
32
- }
22
+ // non-TTY machine surface only the TTY frame draws task verdicts via statusRow
23
+ const taskBox = (status) => {
33
24
  if (status === "done")
34
25
  return "[x]";
35
26
  if (status === "failed" || status === "human")
@@ -37,8 +28,10 @@ const taskBox = (status, unicode) => {
37
28
  return "[ ]";
38
29
  };
39
30
  const gateBox = (state, unicode) => {
40
- if (unicode)
41
- return state === "pass" ? "✓" : state === "fail" ? "✗" : state === "skip" ? "·" : "☐";
31
+ if (unicode) {
32
+ // shared glyph vocabulary: pass/fail verdicts, dash for skip, dim circle for not-yet-run
33
+ return state === "pass" ? GLYPHS.pass : state === "fail" ? GLYPHS.fail : state === "skip" ? GLYPHS.neutral : GLYPHS.toggleInactive;
34
+ }
42
35
  return state === "pass" ? "[x]" : state === "fail" ? "[!]" : state === "skip" ? "." : "[ ]";
43
36
  };
44
37
  const defaultGateStates = (task) => GATE_NAMES.map((gate) => task.gates.includes(gate) ? "open" : "skip");
@@ -59,9 +52,12 @@ const gateStates = (task, events) => {
59
52
  }
60
53
  return GATE_NAMES.map((gate) => task.gates.includes(gate) ? outcomes.get(gate) ?? "open" : "skip");
61
54
  };
62
- // verdict semantics only: pass green, fail red, skip/open dim chrome — everything else stays quiet
63
- const GATE_STATE_COLOR = { pass: 32, fail: 31, skip: 2, open: 2 };
64
- const gateChain = (states, unicode) => GATE_NAMES.map((gate, i) => color(`${GATE_KEYS[gate]}${gateBox(states[i], unicode)}`, GATE_STATE_COLOR[states[i]], unicode)).join(" ");
55
+ // verdict semantics only: pass brand green, fail red, skip/open dim chrome — everything else stays quiet
56
+ const GATE_STATE_TOKEN = { pass: ok, fail, skip: dim, open: dim };
57
+ const gateChain = (states, unicode) => GATE_NAMES.map((gate, i) => {
58
+ const chip = `${GATE_KEYS[gate]}${gateBox(states[i], unicode)}`;
59
+ return unicode ? GATE_STATE_TOKEN[states[i]](chip) : chip;
60
+ }).join(" ");
65
61
  // plain (uncolored) chip width for column math — ANSI codes have zero display width
66
62
  const gateChainWidth = (unicode) => GATE_NAMES.reduce((w, gate) => w + GATE_KEYS[gate].length + (unicode ? 1 : 3) + 1, -1);
67
63
  const shortGoal = (goal, max) => {
@@ -159,61 +155,62 @@ const renderFrame = (cwd) => {
159
155
  const assignCol = contexts.has(t.id) ? `${channel}${divider}ctx ${contexts.get(t.id)}` : channel;
160
156
  return { t, st, label, assignCol, states: comparable ? gateStates(t, events) : defaultGateStates(t) };
161
157
  });
162
- const statusColor = (st) => st === "done" ? 32 : st === "failed" ? 31 : st === "human" ? 33 : 36;
163
158
  if (!unicode) {
164
159
  // machine/CI surface — layout unchanged (pipes, greps, and the golden pins depend on it)
165
160
  const rows = cells.map(({ t, st, label, assignCol, states }) => {
166
161
  const chain = gateChain(states, false);
167
- const prefix = ` ${taskBox(st, false)} ${t.id} `;
162
+ const prefix = ` ${taskBox(st)} ${t.id} `;
168
163
  const suffix = ` ${chain} ${String(st)}${label} ${assignCol}`;
169
164
  return `${prefix}${shortGoal(t.goal, Math.max(0, width - prefix.length - suffix.length))}${suffix}`;
170
165
  });
171
166
  const header = runId
172
167
  ? `tickmarkr status${divider}run ${runId}${!comparable ? `${divider}${NOT_COMPARABLE_NOTICE}` : ""}${divider}${liveness(events).replaceAll(" · ", divider)}${divider}${done}/${g.tasks.length} done`
173
168
  : `tickmarkr status${divider}no runs yet${divider}${done}/${g.tasks.length} done`;
174
- const legend = ` gates: ${GATE_NAMES.map((gate) => `${GATE_KEYS[gate]} ${gate}`).join(divider)}`;
175
- return [header, legend, ...rows].join("\n");
169
+ const legendLine = ` gates: ${GATE_NAMES.map((gate) => `${GATE_KEYS[gate]} ${gate}`).join(divider)}`;
170
+ return [header, legendLine, ...rows].join("\n");
176
171
  }
177
- // TTY: audit-ledger frame bold brand mark, dim chrome, semantic color only on verdicts,
178
- // completion gauge, and column-aligned rows (pad plain text FIRST, colorize after: ANSI has
179
- // zero display width and would corrupt padEnd math)
180
- const dim = (s) => color(s, 2, true);
172
+ // TTY: cockpit frame composed through src/brand.ts (CLI-DESIGN.md) dominant run title,
173
+ // dim chrome, semantic color only on verdicts, completion gauge, and column-aligned status
174
+ // rows (pad plain text FIRST, colorize after: ANSI has zero display width and would corrupt
175
+ // padEnd math)
181
176
  const dot = dim(" · ");
182
177
  const anyFailed = cells.some((c) => c.st === "failed");
183
178
  const gaugeCells = 10;
184
179
  const fill = g.tasks.length ? Math.round((done / g.tasks.length) * gaugeCells) : 0;
185
- const gauge = (fill ? color("█".repeat(fill), anyFailed ? 31 : 32, true) : "") + (fill < gaugeCells ? dim("░".repeat(gaugeCells - fill)) : "");
180
+ const gauge = (fill ? (anyFailed ? fail : ok)("█".repeat(fill)) : "") + (fill < gaugeCells ? dim("░".repeat(gaugeCells - fill)) : "");
186
181
  const live = liveness(events)
187
- .replace(/\bdead\b/, color("dead", 31, true))
188
- .replace(/\bfinished\b/, color("finished", 2, true))
189
- .replace(/\balive\b/, color("alive", 32, true))
182
+ .replace(/\bdead\b/, fail("dead"))
183
+ .replace(/\bfinished\b/, dim("finished"))
184
+ .replace(/\balive\b/, ok("alive"))
190
185
  .replaceAll(" · ", dot);
191
186
  const tally = `${done}/${g.tasks.length} done`;
192
- const header = ` ${color("✓", 32, true)} ${color("tickmarkr", 1, true)}${dot}` +
187
+ const header = ` ${title(runId ? `run ${runId}` : "tickmarkr")}${dot}` +
193
188
  (runId
194
- ? `run ${runId}${dot}${!comparable ? `${NOT_COMPARABLE_NOTICE}${dot}` : ""}${live}${dot}`
189
+ ? `${!comparable ? `${warn(NOT_COMPARABLE_NOTICE)}${dot}` : ""}${live}${dot}`
195
190
  : `no runs yet${dot}`) +
196
- `${gauge} ${done === g.tasks.length && g.tasks.length > 0 ? color(tally, 32, true) : tally}`;
197
- const rule = dim("─".repeat(Math.min(width, 100)));
198
- const legend = dim(` gates: ${GATE_NAMES.map((gate) => `${GATE_KEYS[gate]} ${gate}`).join(" · ")}`);
191
+ `${gauge} ${done === g.tasks.length && g.tasks.length > 0 ? ok(tally) : tally}`;
192
+ const hr = rule(Math.min(width, 100));
193
+ const gatesLegend = legend(` gates: ${GATE_NAMES.map((gate) => `${GATE_KEYS[gate]} ${gate}`).join(" · ")}`);
194
+ const taskVerdict = (st) => st === "done" ? "pass" : st === "failed" ? "fail" : st === "human" ? "warn" : "neutral";
199
195
  const idW = Math.max(...cells.map((c) => c.t.id.length), 2);
200
196
  const stW = Math.max(...cells.map((c) => (String(c.st) + c.label).length));
201
197
  const chainW = gateChainWidth(true);
202
198
  const assignW = Math.max(...cells.map((c) => c.assignCol.length));
203
199
  const goalW = Math.max(8, width - (5 + idW) - 2 - chainW - 2 - stW - 2 - assignW);
204
200
  const rows = cells.map(({ t, st, label, assignCol, states }) => {
205
- const box = color(taskBox(st, true), st === "done" ? 32 : st === "failed" ? 31 : st === "human" ? 33 : 2, true);
206
201
  const goal = shortGoal(t.goal, goalW).padEnd(goalW);
207
- const statusCell = color(String(st), statusColor(st), true) +
208
- (label ? (label === " starved" ? color(label, 31, true) : dim(label)) : "") +
202
+ const stWord = st === "done" ? ok(String(st)) : st === "failed" ? fail(String(st)) : st === "human" ? warn(String(st)) : String(st);
203
+ const statusCell = stWord +
204
+ (label ? (label === " starved" ? fail(label) : dim(label)) : "") +
209
205
  " ".repeat(stW - (String(st) + label).length);
210
- return ` ${box} ${t.id.padEnd(idW)} ${goal} ${gateChain(states, true)} ${statusCell} ${dim(assignCol)}`;
206
+ return ` ${statusRow(taskVerdict(st), `${t.id.padEnd(idW)} ${goal} ${gateChain(states, true)} ${statusCell} ${dim(assignCol)}`)}`;
211
207
  });
212
- return [header, rule, legend, ...rows].join("\n");
208
+ return [header, hr, gatesLegend, ...rows].join("\n");
213
209
  };
214
210
  export async function status(argv, cwd = process.cwd(), opts = {}) {
211
+ // cockpit surface: banner + frame on a TTY (doctor's pattern); pipes get the bare frame
215
212
  if (!argv.includes("--watch"))
216
- return renderFrame(cwd);
213
+ return visual() ? BANNER + renderFrame(cwd) : renderFrame(cwd);
217
214
  const iterations = opts.iterations ?? Infinity;
218
215
  const sleep = opts.sleep ?? defaultSleep;
219
216
  const bounded = Number.isFinite(iterations);
@@ -223,7 +220,7 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
223
220
  for (let i = 0; i < iterations; i++) {
224
221
  const frame = renderFrame(cwd);
225
222
  if (tty)
226
- process.stdout.write(`\x1b[2J\x1b[H${BANNER}${frame}\n\x1b[2m watching · refresh ${REFRESH_MS / 1000}s · ^C to quit\x1b[0m`);
223
+ process.stdout.write(`\x1b[2J\x1b[H${BANNER}${frame}\n${legend(` watching · refresh ${REFRESH_MS / 1000}s · ^C to quit`)}`);
227
224
  else
228
225
  process.stdout.write(frame + sep);
229
226
  if (bounded)
@@ -1,4 +1,5 @@
1
1
  import { shq } from "../adapters/types.js";
2
+ import { PANE_IDENTITY_ENV, paneIdentityLine } from "../brand.js";
2
3
  import { createWorktree, sh } from "../run/git.js";
3
4
  import { herdrSealShellPrefix } from "./subprocess.js";
4
5
  import { canonicalizeLegacyName, formatOwnedName, panesToClose, parseOwnedName } from "./types.js";
@@ -115,6 +116,8 @@ export class HerdrDriver {
115
116
  const rootPane = res?.root_pane?.pane_id; // auto-created shell pane (SKILL:343)
116
117
  if (typeof tabId !== "string" || !tabId)
117
118
  throw new Error(`herdr tab create returned no tab_id (refusing untargeted placement): ${t.stdout}`);
119
+ // T5: the banner's pane identity line, derived from the T1 owned name (legacy names pass through).
120
+ const identity = shq(paneIdentityLine(canonicalizeLegacyName(name, "")));
118
121
  let r = await this.herdr(`agent start ${shq(name)} --cwd ${shq(cwd)} --tab ${shq(tabId)} --no-focus -- bash`);
119
122
  if (r.code !== 0 && /agent_name_taken/.test(r.stderr + r.stdout)) {
120
123
  // DEFECT-01: a prior (killed) process's kept pane still holds this durable name — reclaim it.
@@ -149,7 +152,7 @@ export class HerdrDriver {
149
152
  // worker GETS, not a rule it must remember (P40-02 probe leak). Fail closed: a failed seed rejects.
150
153
  // v1.22 T3 / OBS-17: also strip HERDR_ENV + socket path so the worker cannot open/mutate panes in
151
154
  // the operator's session. Daemon-side this.herdr() calls keep process.env (unsealed).
152
- const seed = await this.herdr(`pane run ${shq(id)} ${shq(`export HERDR_WORKSPACE_ID=${shq(this.ws)}; ${herdrSealShellPrefix()}`)}`);
155
+ const seed = await this.herdr(`pane run ${shq(id)} ${shq(`export HERDR_WORKSPACE_ID=${shq(this.ws)}; export ${PANE_IDENTITY_ENV}=${identity}; ${herdrSealShellPrefix()}`)}`);
153
156
  if (seed.code !== 0)
154
157
  throw new Error(`herdr workspace-id seed failed (refusing untargeted pane): ${seed.stderr || seed.stdout}`);
155
158
  return { id, name, cwd, tabId };
@@ -260,7 +263,8 @@ export class HerdrDriver {
260
263
  // like a failed cd (return null → caller falls back to a per-slot tab). this.ws is guaranteed set —
261
264
  // joinGroup runs only after the first member's tabSlot succeeded, which requires it (VIS-10).
262
265
  // v1.22 T3: same env seal as tabSlot — strip control-plane vars after the workspace seed.
263
- const seed = await this.herdr(`pane run ${shq(pane)} ${shq(`export HERDR_WORKSPACE_ID=${shq(this.ws)}; ${herdrSealShellPrefix()}`)}`);
266
+ // T5: same brand identity seed as tabSlot every group member's banner announces its own name.
267
+ const seed = await this.herdr(`pane run ${shq(pane)} ${shq(`export HERDR_WORKSPACE_ID=${shq(this.ws)}; export ${PANE_IDENTITY_ENV}=${shq(paneIdentityLine(canonicalizeLegacyName(name, "")))}; ${herdrSealShellPrefix()}`)}`);
264
268
  if (seed.code !== 0) {
265
269
  await this.herdr(`pane close ${shq(pane)}`);
266
270
  return null;
@@ -5,6 +5,7 @@ import { join } from "node:path";
5
5
  import { trailerPattern, writePrompt } from "../adapters/prompt.js";
6
6
  import { allAdapters, discoverChannels, getAdapter, probeAll, readDoctor } from "../adapters/registry.js";
7
7
  import { addUsage, channelKey, matchesTrustDialog, QUOTA_RE } from "../adapters/types.js";
8
+ import { bannerShell } from "../brand.js";
8
9
  import { loadConfig } from "../config/config.js";
9
10
  import { herdrSealShellPrefix, SubprocessDriver } from "../drivers/subprocess.js";
10
11
  import { formatOwnedName } from "../drivers/types.js";
@@ -489,7 +490,9 @@ export async function runDaemon(repoRoot, opts = {}) {
489
490
  if (interactive) {
490
491
  // v1.2 interactive: the TUI doesn't exit on completion — the trailer is the finish line.
491
492
  // The exit wrapper still fires if the TUI dies (crash/quit): fast-fail instead of burning the timeout.
492
- await driver.run(slot, `${icmd}; ${exitMarkerCmd}`);
493
+ // T5: the pane announces itself first — same brand header (logo + dim identity) as the
494
+ // judge/review/consult dispatch scripts; one printf one-liner, dispatch mechanics unchanged.
495
+ await driver.run(slot, `${bannerShell()}; ${icmd}; ${exitMarkerCmd}`);
493
496
  let paged = false;
494
497
  // v1.22 T5 / OBS-19: auto-answer a fingerprint-matched trust dialog exactly once per slot.
495
498
  // Any other blocked/idle dialog still pages the operator (paged latch below).
@@ -576,7 +579,8 @@ export async function runDaemon(repoRoot, opts = {}) {
576
579
  }
577
580
  else {
578
581
  const inv = adapter.invoke(t, wt, assignment, { promptFile });
579
- await driver.run(slot, `${inv.command}; ${exitMarkerCmd}`);
582
+ // T5: same brand header as the interactive path and the gate/consult dispatch scripts.
583
+ await driver.run(slot, `${bannerShell()}; ${inv.command}; ${exitMarkerCmd}`);
580
584
  // OBS-54: headless workers have the same output-inactivity budget as visible panes.
581
585
  const stallWindowMs = taskTimeoutMinutes * 60_000;
582
586
  let lastOutput = await driver.read(slot, 500);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "1.49.0",
3
+ "version": "1.50.0",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",