fix(tui,cli): slice-17 guard corrections — public-bin front door, spec hierarchy depth, runtime meta, locked │ rails

Guard BLOCKING at 6e8f2e8c, all four findings closed:
1. bin-wrapper (the real public path) now owns bare-TTY invocations via the
   re-exported runFrontDoor (feature-detected); compiled PTY pin via tmux +
   an in-process forced-TTY belt.
2. spec: rows derive depth from the shipped label indent (6-space = foldered)
   so an expanded folder's child renders one level deeper, never a sibling;
   PIN-1 actions/keys pinned unchanged.
3. agent meta = runtime · ctx% (mockup's first-token display form) under an
   IDENTITY-FIRST width policy: richest meta that fits beside the FULL name,
   ctx-only fallback, name truncates only as last resort; pinned all three.
4. locked │ rails render; the pane border is located by its FIXED boundary
   column in stylize and in the two floor call sites (guard-sanctioned
   truthful floor update — assertions unchanged).

TUI 133/133, CLI front-door 14/14, full CLI green vs the 31 pre-existing,
lint, builds. Captures refreshed through the PUBLIC bin.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
v-openrig-buildandClaude Fable 5 committed 2026-08-03 21:33:40 +00:00
1 parent 6e8f2e8c20
commit b55d95c602
7 files changed
+175 -30

No files matched your search

+7
View File
@@ -77,7 +77,14 @@ export async function run(argv = process.argv): Promise<void> {
const mod = await import(entryUrl) as {
createProgram: () => import("commander").Command;
runProgram: (program: import("commander").Command, argv: string[]) => Promise<number>;
runFrontDoor?: (argv: readonly string[]) => Promise<boolean>;
};
// Slice 17: the PUBLIC bin is the real front door — bare `rig` in a TTY
// opens the TUI here, not only under a direct `node dist/index.js` run
// (guard finding 1: importing the entry makes its isDirectRun false).
// Feature-detected so the wrapper still runs an older sibling entry.
const owned = mod.runFrontDoor ? await mod.runFrontDoor(normalizedArgv) : false;
if (owned) return;
// Slice 15: run through the shared error path so `--json` failures emit a JSON
// error object with a nonzero exit instead of plain Commander text.
await mod.runProgram(mod.createProgram(), normalizedArgv);
+3
View File
@@ -248,6 +248,9 @@ export function isDirectRun(argv1 = process.argv[1], moduleUrl = import.meta.url
// Slice 15 — the shared CLI error/exit path (re-exported for bin-wrapper + tests).
export { runProgram, wantsJsonOutput } from "./cli-error.js";
// Slice 17 — the bare-rig front door (re-exported so the PUBLIC bin-wrapper
// path owns bare TTY invocations too, not just direct entry runs).
export { runFrontDoor } from "./front-door.js";
// Only parse when executed directly (not imported for testing)
if (isDirectRun()) {
+57
View File
@@ -118,6 +118,63 @@ describe("resolveTuiPath — monorepo-first, bundled fallback (the daemon resolv
});
});
describe("PUBLIC bin ownership (guard finding 1 — the wrapper is the real front door)", () => {
const binWrapper = join(__dirname, "..", "dist", "bin-wrapper.js");
const hasTmux = spawnSync("tmux", ["-V"], { encoding: "utf-8" }).status === 0;
it.skipIf(!existsSync(binWrapper) || !hasTmux)("bare PUBLIC bin under a REAL PTY reaches the front-door path (daemon-down degrade proves ownership)", () => {
// tmux gives the process a real TTY on BOTH streams (BSD script(1) cannot
// allocate one under a piped test runner). A dead daemon URL forces the
// front door's degrade branch — its distinctive message proves
// runFrontDoor ran on the PUBLIC path (commander's usage text would mean
// the bypass guard reported).
const session = `frontdoor-pin-${process.pid}`;
spawnSync("tmux", ["kill-session", "-t", session], { encoding: "utf-8" });
const run = spawnSync(
"tmux",
["new-session", "-d", "-s", session, "-x", "120", "-y", "30",
`OPENRIG_URL=http://127.0.0.1:9 ${process.execPath} ${binWrapper}; sleep 15`],
{ encoding: "utf-8", timeout: 10000 },
);
expect(run.status).toBe(0);
let text = "";
for (let i = 0; i < 20 && !/daemon not running|Usage: rig/.test(text); i++) {
spawnSync("sleep", ["0.5"]);
text = spawnSync("tmux", ["capture-pane", "-t", session, "-p"], { encoding: "utf-8", timeout: 5000 }).stdout ?? "";
}
spawnSync("tmux", ["kill-session", "-t", session], { encoding: "utf-8" });
expect(text).toMatch(/daemon not running — try: rig up/);
expect(text).not.toMatch(/Usage: rig \[options\] \[command\]/); // NOT the commander bypass
expect(text).not.toMatch(/\n\s+at /);
});
it.skipIf(!existsSync(binWrapper))("compiled wrapper wiring: forced-TTY in-process run reaches the degrade branch (belt for PTY-less environments)", () => {
const probe = spawnSync(
process.execPath,
["-e", `process.stdin.isTTY = true; process.stdout.isTTY = true; process.argv[1] = ${JSON.stringify(binWrapper)}; import(${JSON.stringify(binWrapper)});`],
{ encoding: "utf-8", timeout: 10000, env: { ...process.env, OPENRIG_URL: "http://127.0.0.1:9" } },
);
const text = `${probe.stdout}${probe.stderr}`;
expect(text).toMatch(/daemon not running — try: rig up/);
expect(text).not.toMatch(/\n\s+at /);
});
it.skipIf(!existsSync(binWrapper))("piped PUBLIC bin keeps the commander usage baseline (no TUI, fast exit)", () => {
const result = spawnSync(process.execPath, [binWrapper], {
input: "x\n",
timeout: 5000,
encoding: "utf-8",
});
expect(result.status).toBe(0);
expect(`${result.stderr}${result.stdout}`).toMatch(/Usage: rig/);
});
it.skipIf(!existsSync(binWrapper))("PUBLIC bin subcommands are untouched (rig --version via wrapper)", () => {
const result = spawnSync(process.execPath, [binWrapper, "--version"], { timeout: 5000, encoding: "utf-8" });
expect(result.status).toBe(0);
expect(result.stdout.trim()).toMatch(/^\d+\.\d+\.\d+/);
});
});
describe("script-safety integration (the compiled front door)", () => {
const cliEntry = join(__dirname, "..", "dist", "index.js");
it.skipIf(!existsSync(cliEntry))("`echo x | rig` returns usage fast — no hang (exit code preserves today's clean-help baseline)", () => {
+40 -21
View File
@@ -26,8 +26,8 @@ function parseKey(key: string | undefined): KeyParts | null {
return { kind: key.slice(0, at), parts: key.slice(at + 1).split("/") };
}
function keyDepth(key: string | undefined): number {
const parsed = parseKey(key);
function keyDepth(row: ExplorerRow): number {
const parsed = parseKey(row.key);
if (!parsed) return -1; // keyless rows (filter, needs items) keep their label
switch (parsed.kind) {
case "section":
@@ -41,7 +41,11 @@ function keyDepth(key: string | undefined): number {
case "pod":
return 3;
case "spec":
return parsed.parts[0]?.includes("/") ? 3 : 2;
// The row model's spec keys carry no parent relation (guard finding 2);
// the folder membership IS encoded in the shipped label indent — a
// foldered spec indents 6 spaces, a root spec 4 (state.ts) — so the
// child renders one level BELOW its folder, never as a sibling.
return /^ {6}/.test(row.label) ? 3 : 2;
case "agent":
return 4;
default:
@@ -59,24 +63,32 @@ function contentOf(row: ExplorerRow, kind: string): string {
return stripped;
}
function metaOf(row: ExplorerRow, snap: FleetSnapshot): string {
/** meta candidates, RICHEST first — the renderer picks the first that fits
* WITHOUT sacrificing row identity (names never become ambiguous to make
* room for telemetry) */
function metaOf(row: ExplorerRow, snap: FleetSnapshot): string[] {
const parsed = parseKey(row.key);
if (!parsed) return "";
if (!parsed) return [];
if (parsed.kind === "agent") {
const [host, rig, pod, ...name] = parsed.parts;
const agent = snap.hosts
.find((h) => h.name === host)?.rigs.find((r) => r.name === rig)
?.pods.find((p) => p.name === pod)?.agents.find((a) => a.name === name.join("/"));
if (!agent) return "";
// honest-unknown: no served ctx → an explicit —, never a fabricated value
return agent.context == null ? "—" : `${agent.context}%`;
if (!agent) return [];
// Locked meta = runtime · ctx% (mini-req 1). The runtime's DISPLAY form is
// its first hyphen token — the literal mockups render exactly "claude 18%"
// for claude-code seats (tui-ascii-aesthetic.html) — served data unchanged.
// Honest-unknown: no served ctx → an explicit —, never a fabricated value.
const runtimeToken = agent.runtime.split("-")[0] || agent.runtime;
const ctx = agent.context == null ? "—" : `${agent.context}%`;
return [`${runtimeToken} ${ctx}`, ctx];
}
if (parsed.kind === "pod") {
const [host, rig, pod] = parsed.parts;
const found = snap.hosts.find((h) => h.name === host)?.rigs.find((r) => r.name === rig)?.pods.find((p) => p.name === pod);
return found ? String(found.agents.length) : "";
return found ? [String(found.agents.length)] : [];
}
return "";
return [];
}
/**
@@ -85,7 +97,7 @@ function metaOf(row: ExplorerRow, snap: FleetSnapshot): string {
* rails read continuously (the `tree` command look).
*/
export function navigatorLabels(rows: ExplorerRow[], snap: FleetSnapshot, width: number): string[] {
const depths = rows.map((row) => keyDepth(row.key));
const depths = rows.map((row) => keyDepth(row));
const isLast = rows.map((_, i) => {
const depth = depths[i]!;
if (depth <= 0) return true;
@@ -98,26 +110,33 @@ export function navigatorLabels(rows: ExplorerRow[], snap: FleetSnapshot, width:
return true;
});
// NB: the continuation rail is ┊ (not │) — the pane BORDER is │ and the
// shipped floor treats the first │ on a line as pane structure; the rail
// must never masquerade as the border (same glyph-collision class as the
// emoji-width lesson). Branch glyphs ├─ └─ are the mockup's exact chars.
// The continuation rail is the mockup's literal │ (guard finding 4, locked
// glyph). The pane border is ALSO │ but lives at a FIXED column (EXPL_W) —
// the paint layer and floors locate it by that boundary, never by scanning
// for the first │ (which a rail would shadow).
const railOpen: boolean[] = []; // per depth level: does a later sibling exist?
return rows.map((row, i) => {
const depth = depths[i]!;
if (depth < 0) return row.label; // keyless rows untouched
if (depth === 0) return row.label; // section headers keep their ▾/▸ identity
railOpen[depth] = !isLast[i]!;
const guides = Array.from({ length: depth - 1 }, (_, level) => (railOpen[level + 1] ? "┊ " : " ")).join("");
const guides = Array.from({ length: depth - 1 }, (_, level) => (railOpen[level + 1] ? "│ " : " ")).join("");
const branch = isLast[i] ? "└─ " : "├─ ";
const parsed = parseKey(row.key)!;
let content = contentOf(row, parsed.kind);
const meta = metaOf(row, snap);
const candidates = metaOf(row, snap);
const prefix = ` ${guides}${branch}`;
if (!meta) return `${prefix}${content}`;
// meta is glance data — guaranteed; over-long content truncates honestly
const contentMax = width - prefix.length - meta.length - 1;
if (content.length > contentMax) content = `${content.slice(0, Math.max(contentMax - 1, 0))}…`;
if (candidates.length === 0) return `${prefix}${content}`;
// IDENTITY-FIRST width policy: pick the richest meta that fits beside the
// FULL name (runtime · ctx% where geometry allows, ctx% otherwise); only
// when even the leanest meta cannot fit does the name truncate with … —
// rows must never become ambiguous to make room for telemetry.
const fits = candidates.find((m) => prefix.length + content.length + 1 + m.length <= width);
const meta = fits ?? candidates.at(-1)!;
if (!fits) {
const contentMax = width - prefix.length - meta.length - 1;
content = `${content.slice(0, Math.max(contentMax - 1, 0))}…`;
}
const gap = Math.max(width - prefix.length - content.length - meta.length, 1);
return `${prefix}${content}${" ".repeat(gap)}${meta}`;
});
+5 -2
View File
@@ -31,7 +31,7 @@ function paintExplorer(text: string, s: Style, focused: boolean): string {
if (/[▾▸] (TOPOLOGY|SPECS|NEEDS-YOU)/.test(text)) return s.paint("bright", text, { bold: true });
// Slice-17 re-skin: branch guides paint faint (chrome), the row body keeps
// its own rules — the tree rails read as structure, never as content.
const tree = text.match(/^( *(?:[┊ ] )*(?:├─|└─) )(.*)$/);
const tree = text.match(/^( *(?:[│ ] )*(?:├─|└─) )(.*)$/);
if (tree) return s.paint("chrome", tree[1]!) + paintExplorerBody(tree[2]!, s);
return paintExplorerBody(text, s);
}
@@ -155,7 +155,10 @@ export function stylizeLines(screen: Screen, s: Style): string[] {
else if (tail.startsWith("⚠")) tail = s.paint("warn", tail);
return `${s.paint("accent", line.slice(0, closeAt + 1), { bold: true })}${s.paint("bright", path)}${tail}`;
}
const border = line.indexOf("│");
// The pane border lives at the FIXED boundary column (EXPL_W) — located
// positionally, never by scanning: the navigator's │ rails would shadow
// a first-index search (slice-17 locked-rail resolution).
const border = line.charAt(EXPL_W) === "│" ? EXPL_W : -1;
if (border >= EXPL_W - 1 && border <= EXPL_W) {
const left = line.slice(0, border);
const marker = line.slice(border + 1, border + 2);
+57 -5
View File
@@ -54,15 +54,67 @@ describe("file-tree re-skin (Direction B navigator)", () => {
expect(pane.some((l) => l.includes("dev50.driver"))).toBe(true);
});
it("agent rows carry right-aligned ctx% meta; a null ctx renders — (honest-unknown)", () => {
it("agent meta is right-aligned runtime · ctx% where it fits, ctx% otherwise — IDENTITY-FIRST (locked mini-req + guard finding 3)", () => {
// demo names are long: full identity is kept, the meta degrades to ctx-only
const s = makeStore();
s.dispatch({ type: "drill", resource: "pod", name: "dev50", target: { host: "vm-host", rig: "openrig-build" } });
const screen = renderScreen(s.get(), snap, { cols: 120, rows: 32 });
const pane = explorerPane(screen.lines);
const pane = explorerPane(renderScreen(s.get(), snap, { cols: 120, rows: 32 }).lines);
const driver = pane.find((l) => l.includes("dev50.driver"))!;
expect(driver.trimEnd()).toMatch(/62%$/); // demo fixture: driver ctx 62
expect(driver.trimEnd()).toMatch(/62%$/); // full name preserved, ctx at the edge
const qa = pane.find((l) => l.includes("dev50.qa"))!;
expect(qa.trimEnd()).toMatch(/—$/); // demo fixture: qa ctx null → honest
expect(qa.trimEnd()).toMatch(/—$/); // ctx null → honest-unknown
// a short name leaves room: the FULL runtime · ctx meta renders (the
// mockup's "claude 18%" display form for a claude-code seat)
const shortSnap = {
...snap,
hosts: [{ name: "h", reachable: true, rigs: [{ name: "r", pods: [{ name: "p", agents: [
{ name: "ok", runtime: "claude-code", spec: "", context: 5, tokens: null, status: "active", live: true },
] }] }] }],
};
const s2 = createViewState({ instanceId: "nav-short", getSnapshot: () => shortSnap });
s2.dispatch({ type: "drill", resource: "pod", name: "p", target: { host: "h", rig: "r" } });
const shortPane = explorerPane(renderScreen(s2.get(), shortSnap, { cols: 120, rows: 32 }).lines);
expect(shortPane.find((l) => l.includes("● ok"))!.trimEnd()).toMatch(/claude 5%$/);
});
it("only an identity too long for even the leanest meta truncates with … — meta stays complete at the edge", () => {
const longSnap = {
...snap,
hosts: [{ name: "h", reachable: true, rigs: [{ name: "r", pods: [{ name: "p", agents: [
{ name: "an-extremely-long-agent-name-x", runtime: "codex", spec: "", context: 9, tokens: null, status: "active", live: true },
] }] }] }],
};
const s = createViewState({ instanceId: "nav-long", getSnapshot: () => longSnap });
s.dispatch({ type: "drill", resource: "pod", name: "p", target: { host: "h", rig: "r" } });
const row = explorerPane(renderScreen(s.get(), longSnap, { cols: 120, rows: 32 }).lines).find((l) => l.includes("an-extre"))!;
expect(row).toMatch(/…/);
expect(row.trimEnd()).toMatch(/9%$/);
});
it("an expanded namespaced spec folder renders its child ONE level deeper, never a sibling (guard finding 2)", () => {
const nsSnap = {
...snap,
specs: [
...snap.specs,
{ name: "vault-specialist", kind: "agent" as const, runtime: "codex", namespace: "vault", usedByRigs: [] },
],
};
const s = createViewState({ instanceId: "nav-ns", getSnapshot: () => nsSnap });
s.dispatch({ type: "jump", section: "specs" });
s.dispatch({ type: "toggle-expand", key: "folder:vault" });
const screen = renderScreen(s.get(), nsSnap, { cols: 120, rows: 40 });
const pane = explorerPane(screen.lines);
const folder = pane.find((l) => l.includes("vault/"))!;
const child = pane.find((l) => l.includes("vault-specialist"))!;
const indentOf = (l: string) => (/^\s*(?:│ )*/.exec(l)?.[0] ?? "").length + (l.match(/├─|└─/)?.index ?? 0);
const branchCol = (l: string) => l.search(/├─|└─/);
expect(branchCol(child)).toBeGreaterThan(branchCol(folder)); // child branch sits deeper
void indentOf;
// PIN-1 untouched: the child's action is still the spec drill from the row model
const rows = computeExplorerRows(s.get(), nsSnap);
const childRow = rows.find((r) => r.key === "spec:vault-specialist")!;
expect(childRow.action).toEqual({ type: "drill", resource: "spec", name: "vault-specialist" });
});
it("pod rows carry their agent count right-aligned (moved out of the inline label)", () => {
+6 -2
View File
@@ -46,7 +46,10 @@ describe("live visual regressions", () => {
view.dispatch({ type: "content-select", index: tabIndex });
screen = renderScreen(view.get(), snap, { cols: 140, rows: 34 });
expect(screen.lines[screen.contentTargets[tabIndex]!.y - 1]!.split("│")[1]).toMatch(/^›/);
// pane delimiter located by its FIXED boundary (EXPL_W=30 → content at 31):
// the slice-17 navigator's │ rails would shadow a first-│ split (guard-
// sanctioned truthful floor update; the assertion is unchanged)
expect(screen.lines[screen.contentTargets[tabIndex]!.y - 1]!.slice(31)).toMatch(/^›/);
view.dispatch({ type: "content-select", index: termIndex });
screen = renderScreen(view.get(), snap, { cols: 140, rows: 34 });
@@ -54,7 +57,8 @@ describe("live visual regressions", () => {
view.dispatch({ type: "content-select", index: rowIndex });
screen = renderScreen(view.get(), snap, { cols: 140, rows: 34 });
expect(screen.lines[screen.contentTargets[rowIndex]!.y - 1]!.split("│")[1]).toMatch(/^›/);
// fixed-boundary pane delimiter (see the tab-focus pin above)
expect(screen.lines[screen.contentTargets[rowIndex]!.y - 1]!.slice(31)).toMatch(/^›/);
});
it("never emits a composed row wider than the terminal", () => {