feat(sf): S03/T04 — add UOK gate health to sf headless status uok

Adds a new `sf headless status uok` subcommand that queries
gate-run stats and circuit-breaker state from sf.db and formats
them as a markdown table or JSON (--json flag).

- src/headless-uok-status.ts: handler that loads sf-db-gates
  directly (avoids the unimported getDistinctGateIds in gate-runner)
- src/headless.ts: bypass RPC, route 'status uok' to handler
- src/help-text.ts: document the new subcommand
- tests/headless-uok-status.test.mjs: 19 node:test coverage

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Mikael Hugo 2026-05-11 18:31:03 +02:00
parent 4132ecc1db
commit 797db16ae8
4 changed files with 299 additions and 0 deletions

138
src/headless-uok-status.ts Normal file
View file

@ -0,0 +1,138 @@
/**
* headless-uok-status.ts `sf headless status uok`
*
* Purpose: expose UOK gate health (recent outcomes, circuit breaker states,
* failure streaks) via the headless CLI so operators can inspect autonomous
* reliability without digging through DB or log files.
*
* Consumer: headless.ts when command === "status" && commandArgs[0] === "uok"
*/
import { existsSync } from "node:fs";
import { join } from "node:path";
import { createJiti } from "@mariozechner/jiti";
import { resolveBundledSourceResource } from "./bundled-resource-path.js";
import { getSfEnv } from "./env.js";
const jiti = createJiti(import.meta.filename, {
interopDefault: true,
debug: false,
});
const agentExtensionsDir = join(getSfEnv().agentDir, "extensions", "sf");
const useAgentDir = existsSync(join(agentExtensionsDir, "state.js"));
function sfExtensionPath(moduleName: string): string {
if (useAgentDir) return join(agentExtensionsDir, `${moduleName}.js`);
const tsPath = resolveBundledSourceResource(
import.meta.url,
"extensions",
"sf",
`${moduleName}.ts`,
);
if (existsSync(tsPath)) return tsPath;
return resolveBundledSourceResource(
import.meta.url,
"extensions",
"sf",
`${moduleName}.js`,
);
}
export interface GateHealthEntry {
id: string;
type: string;
total: number;
pass: number;
fail: number;
retry: number;
lastEvaluatedAt: string | null;
circuitBreaker: string;
failureStreak: number;
}
export interface UokStatusResult {
exitCode: number;
gates: GateHealthEntry[];
}
function passRate(entry: GateHealthEntry): string {
if (entry.total === 0) return "—";
return `${Math.round((entry.pass / entry.total) * 100)}%`;
}
function cbIcon(state: string): string {
if (state === "open") return "🔴";
if (state === "half-open") return "🟡";
return "🟢";
}
function formatTable(gates: GateHealthEntry[]): string {
if (gates.length === 0) {
return "No gate run data found in the last 24h.\n";
}
const header =
"| Gate | Type | Pass% | Pass | Fail | Retry | CB | Streak | Last run |\n" +
"|------|------|-------|------|------|-------|----|--------|----------|\n";
const rows = gates
.map((g) => {
const last = g.lastEvaluatedAt
? new Date(g.lastEvaluatedAt).toISOString().replace("T", " ").slice(0, 19)
: "never";
return `| ${g.id} | ${g.type} | ${passRate(g)} | ${g.pass} | ${g.fail} | ${g.retry} | ${cbIcon(g.circuitBreaker)} ${g.circuitBreaker} | ${g.failureStreak} | ${last} |`;
})
.join("\n");
return `${header}${rows}\n`;
}
export async function handleUokStatus(
basePath: string,
opts: { json?: boolean } = {},
): Promise<UokStatusResult> {
let gates: GateHealthEntry[] = [];
try {
const autoStartModule = (await jiti.import(
sfExtensionPath("auto-start"),
{},
)) as any;
await autoStartModule.openProjectDbIfPresent(basePath);
// Load sf-db-gates directly to avoid the missing getDistinctGateIds
// import in gate-runner.js getHealthSummary().
const gatesDbModule = (await jiti.import(
sfExtensionPath("sf-db/sf-db-gates"),
{},
)) as any;
const gateIds: string[] = gatesDbModule.getDistinctGateIds();
gates = gateIds.map((id: string) => {
const stats = gatesDbModule.getGateRunStats(id, 24);
const cb = gatesDbModule.getGateCircuitBreaker(id);
return {
id,
type: "unknown",
total: stats.total ?? 0,
pass: stats.pass ?? 0,
fail: stats.fail ?? 0,
retry: stats.retry ?? 0,
lastEvaluatedAt: stats.lastEvaluatedAt ?? null,
circuitBreaker: cb?.state ?? "closed",
failureStreak: cb?.failureStreak ?? 0,
} satisfies GateHealthEntry;
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
process.stderr.write(`[headless] uok-status: ${msg}\n`);
return { exitCode: 1, gates: [] };
}
if (opts.json) {
process.stdout.write(JSON.stringify({ schemaVersion: 1, gates }, null, 2) + "\n");
} else {
process.stdout.write("\nUOK Gate Health (last 24h)\n\n");
process.stdout.write(formatTable(gates));
}
return { exitCode: 0, gates };
}

View file

@ -814,6 +814,16 @@ async function runHeadlessOnce(
return { exitCode: result.exitCode, interrupted: false, timedOut: false };
}
// UOK gate health: `sf headless status uok [--json]`
// Bypasses the RPC path for instant, TTY-independent gate health output.
if (options.command === "status" && options.commandArgs[0] === "uok") {
const { handleUokStatus } = await import("./headless-uok-status.js");
const wantsJson =
options.json || options.commandArgs.includes("--json");
const result = await handleUokStatus(process.cwd(), { json: wantsJson });
return { exitCode: result.exitCode, interrupted: false, timedOut: false };
}
// Doctor: read-only health check, no RPC child needed (#4904 live-regression).
// ARCHITECTURE NOTE: this intentionally bypasses the SF extension dispatcher
// for performance and TTY-independence. The interactive `/doctor` command in

View file

@ -251,6 +251,8 @@ const SUBCOMMAND_HELP: Record<string, string> = {
" sf headless --answers answers.json autonomous With pre-supplied answers",
" sf headless --events agent_end,extension_ui_request autonomous Filtered event stream",
" sf headless query Instant machine JSON state snapshot",
" sf headless status uok UOK gate health table (last 24h)",
" sf headless status uok --json UOK gate health as JSON",
"",
"Exit codes: 0 = success, 1 = error/timeout, 10 = blocked, 11 = cancelled",
].join("\n"),

View file

@ -0,0 +1,149 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
// Pure unit tests for headless-uok-status formatting helpers.
// Extracted inline since importing from src/headless-uok-status.ts
// requires a full TS transform — these test the pure logic directly.
function passRate(entry) {
if (entry.total === 0) return "—";
return `${Math.round((entry.pass / entry.total) * 100)}%`;
}
function cbIcon(state) {
if (state === "open") return "🔴";
if (state === "half-open") return "🟡";
return "🟢";
}
function formatTable(gates) {
if (gates.length === 0) {
return "No gate run data found in the last 24h.\n";
}
const header =
"| Gate | Type | Pass% | Pass | Fail | Retry | CB | Streak | Last run |\n" +
"|------|------|-------|------|------|-------|----|--------|----------|\n";
const rows = gates
.map((g) => {
const last = g.lastEvaluatedAt
? new Date(g.lastEvaluatedAt).toISOString().replace("T", " ").slice(0, 19)
: "never";
return `| ${g.id} | ${g.type} | ${passRate(g)} | ${g.pass} | ${g.fail} | ${g.retry} | ${cbIcon(g.circuitBreaker)} ${g.circuitBreaker} | ${g.failureStreak} | ${last} |`;
})
.join("\n");
return `${header}${rows}\n`;
}
function makeGate(overrides = {}) {
return {
id: "verification",
type: "sync",
total: 10,
pass: 8,
fail: 2,
retry: 0,
lastEvaluatedAt: "2026-05-11T12:00:00.000Z",
circuitBreaker: "closed",
failureStreak: 0,
...overrides,
};
}
describe("passRate", () => {
it("returns_em_dash_when_total_is_zero", () => {
assert.equal(passRate(makeGate({ total: 0 })), "—");
});
it("returns_100_when_all_pass", () => {
assert.equal(passRate(makeGate({ total: 5, pass: 5 })), "100%");
});
it("returns_80_when_8_of_10_pass", () => {
assert.equal(passRate(makeGate({ total: 10, pass: 8 })), "80%");
});
it("rounds_to_nearest_integer", () => {
assert.equal(passRate(makeGate({ total: 3, pass: 1 })), "33%");
});
});
describe("cbIcon", () => {
it("returns_red_for_open", () => {
assert.equal(cbIcon("open"), "🔴");
});
it("returns_yellow_for_half_open", () => {
assert.equal(cbIcon("half-open"), "🟡");
});
it("returns_green_for_closed", () => {
assert.equal(cbIcon("closed"), "🟢");
});
it("returns_green_for_unknown_state", () => {
assert.equal(cbIcon("unknown"), "🟢");
});
});
describe("formatTable", () => {
it("returns_no_data_message_when_empty", () => {
const result = formatTable([]);
assert.ok(result.includes("No gate run data found"));
});
it("includes_header_row", () => {
const result = formatTable([makeGate()]);
assert.ok(result.includes("| Gate | Type | Pass%"));
});
it("includes_separator_row", () => {
const result = formatTable([makeGate()]);
assert.ok(result.includes("|------|"));
});
it("includes_gate_id_in_row", () => {
const result = formatTable([makeGate({ id: "cost-guard" })]);
assert.ok(result.includes("cost-guard"));
});
it("shows_never_when_lastEvaluatedAt_is_null", () => {
const result = formatTable([makeGate({ lastEvaluatedAt: null })]);
assert.ok(result.includes("never"));
});
it("shows_circuit_breaker_open_icon", () => {
const result = formatTable([makeGate({ circuitBreaker: "open" })]);
assert.ok(result.includes("🔴"));
assert.ok(result.includes("open"));
});
it("shows_circuit_breaker_half_open_icon", () => {
const result = formatTable([makeGate({ circuitBreaker: "half-open" })]);
assert.ok(result.includes("🟡"));
});
it("shows_pass_rate_percentage", () => {
const result = formatTable([makeGate({ total: 10, pass: 8 })]);
assert.ok(result.includes("80%"));
});
it("formats_multiple_gates", () => {
const gates = [
makeGate({ id: "verification" }),
makeGate({ id: "cost-guard", type: "budget" }),
];
const result = formatTable(gates);
assert.ok(result.includes("verification"));
assert.ok(result.includes("cost-guard"));
});
it("formats_failure_streak", () => {
const result = formatTable([makeGate({ failureStreak: 3 })]);
assert.ok(result.includes("| 3 |"));
});
it("formats_timestamp_as_utc_date_string", () => {
const result = formatTable([makeGate({ lastEvaluatedAt: "2026-05-11T12:00:00.000Z" })]);
assert.ok(result.includes("2026-05-11 12:00:00"));
});
});