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 +62 -0
- package/dist/brand.js +74 -2
- package/dist/cli/commands/doctor.js +33 -46
- package/dist/cli/commands/fleet.js +5 -7
- package/dist/cli/commands/init.js +22 -2
- package/dist/cli/commands/plan.js +18 -1
- package/dist/cli/commands/report.js +13 -1
- package/dist/cli/commands/run.d.ts +2 -0
- package/dist/cli/commands/run.js +17 -1
- package/dist/cli/commands/status.js +38 -41
- package/dist/drivers/herdr.js +6 -2
- package/dist/run/daemon.js +6 -2
- package/package.json +1 -1
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
|
-
//
|
|
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,
|
|
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
|
|
13
|
-
|
|
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
|
|
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(
|
|
70
|
-
|
|
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
|
|
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(
|
|
64
|
+
rows.push(alignedStatusRow("pass", a.id, "trust: trusted"));
|
|
85
65
|
else if (v.status === "seeded")
|
|
86
|
-
rows.push(
|
|
66
|
+
rows.push(alignedStatusRow("pass", a.id, "trust: seeded"));
|
|
87
67
|
else
|
|
88
|
-
rows.push(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 = `\
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
? `\
|
|
153
|
+
? `\n${legend("model status:")}\n${[...modelStatus, ...preferStatus].join("\n")}`
|
|
170
154
|
: "";
|
|
171
|
-
|
|
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 = [
|
|
79
|
-
rows.forEach((r, i) => lines.push(i === cursor ? `${
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
23
|
-
const taskBox = (status
|
|
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
|
-
|
|
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
|
|
64
|
-
const gateChain = (states, unicode) => GATE_NAMES.map((gate, i) =>
|
|
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
|
|
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
|
|
175
|
-
return [header,
|
|
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:
|
|
178
|
-
//
|
|
179
|
-
// zero display width and would corrupt
|
|
180
|
-
|
|
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 ?
|
|
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/,
|
|
188
|
-
.replace(/\bfinished\b/,
|
|
189
|
-
.replace(/\balive\b/,
|
|
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 = ` ${
|
|
187
|
+
const header = ` ${title(runId ? `run ${runId}` : "tickmarkr")}${dot}` +
|
|
193
188
|
(runId
|
|
194
|
-
?
|
|
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 ?
|
|
197
|
-
const
|
|
198
|
-
const
|
|
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
|
|
208
|
-
|
|
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 ` ${
|
|
206
|
+
return ` ${statusRow(taskVerdict(st), `${t.id.padEnd(idW)} ${goal} ${gateChain(states, true)} ${statusCell} ${dim(assignCol)}`)}`;
|
|
211
207
|
});
|
|
212
|
-
return [header,
|
|
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
|
|
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)
|
package/dist/drivers/herdr.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/run/daemon.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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);
|