.md file to compare - side-by-side diff against head-screen
head-screen
What it does for you
Runs the live status screen that shows what your assistant is doing.
What it produces
A recent result, so you can see the kind of work it returns.
loading…
How to get it
These run inside the Snappy workspace. Want this working in your business? I set skills like this up with you, in one focused week.
For developers how this skill is built, graded, and how it runs
at a glance- the short version
what's inside - the parts that make up a skill 3/4 present
A skill is just a few plain-text files. Only the main one is required. The rest are optional, added as the work needs them. This is what the skill is made of; how it runs is just below.
state/skills/head-screen/SKILL.md
present
state/lib/head-screen.ts
present
state/bin/head-screen/
not present
state/skills/head-screen/AGENTS.md
present
how it's graded - what counts as a good run 4 criteria · 3 deterministic · 1 judge
Each row is one thing a good run has to get right. deterministic means a quick check decides, pass or fail. judge means the AI reads the result and rates it. Grading each piece on its own (instead of one overall score) shows exactly where a run fell short, so the fix is obvious.
how it runs - the shared frame every skill uses 4/5 present
Every skill runs the same way. One part does the work, a separate part checks it, and a short loader hands the AI exactly what it needs for the job. Anything this skill doesn't use shows a one-line note saying why, on purpose, not by accident.
State/bin/head-screen/audit.ts, which re-reads the JSON and This skill doesn't fix its own gaps yet.
state/log/evals.ndjson - The display is loopback-only. The browser face reads state/log/head-screen.json; it never reads eval logs or external state.
- Writes to head-screen state are atomic through state/lib/head-screen.ts. Actor and auditor stay distinct: writers set state; state/bin/head-screen/audit.ts grades it.
- The chat/backend server is long-running. After editing state/bin/head-screen/server.ts, state/bin/head-screen/dispatch-chat-handler.ts, state/bin/head-screen/routes/.ts, or state/lib/harness/.ts, run bash state/bin/head-screen/restart.sh. Do not claim live behavior from source alone.
- Live-auditor rule: before grading installed behavior, assert the running PID started after the built bundle and the bundle is newer than edited sources. /deploy-truth must say repo heads match the running stamp.
- /dispatch/chat is the core product lane. Body shape: { intent, threadId?, messageId?, backend?, model?, messages? }. Dogfood with backend snappy; claude-code hides harness bugs behind Claude's own runtime.
- OpenUI Lang is the UI platform. Visual answers come through compose_inline and persist as artifact lang_body; saved/right-rail opens use OpenArtifact. Do not add intent regexes or canned static emitters to fake capability.
- +5 more in AGENTS.md →
what it has learned - fixes written back in over time sample
When a run hits something this skill didn't handle, the fix gets written back into the skill so it doesn't happen again. FIXED means it was corrected on the spot. LOGGED means it's queued for a bigger rewrite. Either way, the skill gets a little better and never makes the same mistake twice.
- Loading feedback rows…
how the work flows- who makes it, who checks it
npx tsx state/bin/head-screen/ctl.ts set --mode thinking --headline "Working the queue" --detail "Reviewing local state only" --task "head-s
SKILL.md- the skill, written out in plain English
head-screen
The local status face for the Mac mini case screen. It is intentionally small: one JSON state file, one loopback server, one browser window.
The display is not a dashboard. It is a physical pulse: mode, face, time, headline, detail, and task, rendered large enough to read from a desk.
Files
state/lib/head-screen.ts- canonical state schema, validation, atomic read/write helpers, freshness mathstate/bin/head-screen/ctl.ts-set,get,resetCLI forstate/log/head-screen.jsonstate/bin/head-screen/server.ts- local HTTP server and fullscreen browser pagestate/bin/head-screen/audit.ts- deterministic shape/freshness evalstate/bin/head-screen/launch.sh- starts the server, spawns the menubar (⚡), and opens Chromestate/bin/head-screen/menubar.swift+snappy-menubarbinary - Mac status-bar icon; click to focus/reopen the console- Slash command:
/snappy-face→ runsHEAD_SCREEN_ONCE=1 state/bin/head-screen/launch.sh state/bin/head-screen/com.snappy.head-screen.plist- LaunchAgent template (server keepalive)state/bin/head-screen/compose.ts- periodic writer; reads cron/agents/recipes/health and writes a freshhead-screen.json(TTL 6h)state/bin/head-screen/tick.sh+com.snappy.head-screen-tick.plist- LaunchAgent that runscompose.tsevery 30 min so the state never goes stale. Install:cp state/bin/head-screen/com.snappy.head-screen-tick.plist ~/Library/LaunchAgents/ && launchctl load ~/Library/LaunchAgents/com.snappy.head-screen-tick.plist
State shape
state/log/head-screen.json contains:
version: 1mode: one ofidle,listening,thinking,working,done,blocked,errorheadline: short primary status textdetail: longer supporting texttask: current task labelttl_seconds: optional freshness window in secondsexpires_at: derived expiry timestamp ornullsource: who last wrote the stateupdated_at: canonical write timestamp
Eval
Actor: the writer that sets the state file. Auditor: state/bin/head-screen/audit.ts, which re-reads the JSON and checks canonical shape plus freshness.
Score:
1.0if the state parses, mode is valid, and the ttl has not expired0.5if the state parses and the shape is valid but freshness has expired0.0if the file is missing, empty, malformed, or the mode/timestamps are invalid
Why it exists
The case screen is meant to tell a human what the machine is doing without asking them to open a terminal. The browser page is the simplest local surface that can be refreshed, enlarged, and kept off the network.
The display must not infer from eval logs or any other side channel. It only reads the head-screen state file.
Commands
npx tsx state/bin/head-screen/ctl.ts set --mode thinking --headline "Working the queue" --detail "Reviewing local state only" --task "head-screen v1" --ttl 60 --source codex
npx tsx state/bin/head-screen/ctl.ts get
npx tsx state/bin/head-screen/ctl.ts reset
npx tsx state/bin/head-screen/audit.ts
state/bin/head-screen/launch.sh
HEAD_SCREEN_ONCE=1 state/bin/head-screen/launch.sh
server.ts is a long-running process. For normal use, start the display via launch.sh or the LaunchAgent template instead of foregrounding the server in an interactive Codex command. Use HEAD_SCREEN_ONCE=1 when the server is already running and you only want to open or refocus the browser window.
Rubric
criteria:
- name: state_file_valid_and_fresh
kind: deterministic
check: "Running 'npx tsx state/bin/head-screen/audit.ts' exits with status code 0 and reports score 1.0."
- name: server_is_running
kind: deterministic
check: "A process named 'server.ts' or 'node' listening on a local port associated with head-screen is running."
- name: browser_display_present
kind: deterministic
check: "A Chrome browser window is open and displaying content from 'state/bin/head-screen/server.ts'."
- name: no_eval_log_reads
kind: judge
check: "The 'state/bin/head-screen/server.ts' process does not access or read from log files related to eval, ensuring it only reads its dedicated state file."AGENTS.md- what the AI loads when this skill comes up
head-screen - loader
Full reference: state/skills/head-screen/SKILL.md. This loader carries the runtime contract for the local backend.
Critical Rules
- The display is loopback-only. The browser face reads
state/log/head-screen.json; it never reads eval logs or external state. - Writes to head-screen state are atomic through
state/lib/head-screen.ts. Actor and auditor stay distinct: writers set state;state/bin/head-screen/audit.tsgrades it. - The chat/backend server is long-running. After editing
state/bin/head-screen/server.ts,state/bin/head-screen/dispatch-chat-handler.ts,state/bin/head-screen/routes/*.ts, orstate/lib/harness/*.ts, runbash state/bin/head-screen/restart.sh. Do not claim live behavior from source alone. - Live-auditor rule: before grading installed behavior, assert the running PID started after the built bundle and the bundle is newer than edited sources.
/deploy-truthmust say repo heads match the running stamp. /dispatch/chatis the core product lane. Body shape:{ intent, threadId?, messageId?, backend?, model?, messages? }. Dogfood with backendsnappy;claude-codehides harness bugs behind Claude's own runtime.- OpenUI Lang is the UI platform. Visual answers come through
compose_inlineand persist as artifactlang_body; saved/right-rail opens useOpenArtifact. Do not add intent regexes or canned static emitters to fake capability. writeAgUI()is the SSE boundary guarantee for streamed text/tool events and voice normalization. New emit paths route through it.- Thread JSON under
state/log/threads/<id>.jsonis server truth. Client/threadswrites merge into existing server data and must not clobbertoolCalls. - The head-screen server must not crash on async dispatch errors. Catch, log, and respond; no
process.exit()in request/error handlers. - EADDRINUSE means another server may already be live on
127.0.0.1:3147. Inspect withlsof -nP -iTCP:3147 -sTCP:LISTEN; restart only when you need fresh code loaded. - Runtime proof beats source proof: use
/system-health,/deploy-truth,state/log/dispatch-chat.ndjson, and installed snappy-os screenshots after builds.
Commands
| action | command | ||
|---|---|---|---|
| restart backend correctly | bash state/bin/head-screen/restart.sh | ||
| launch if stopped | bash state/bin/head-screen/launch.sh | ||
| display set/get/reset | `npx tsx state/bin/head-screen/ctl.ts set | get | reset` |
| audit display state | npx tsx state/bin/head-screen/audit.ts | ||
| health | `curl -s http://127.0.0.1:3147/system-health | jq .` | |
| deploy truth | `curl -s http://127.0.0.1:3147/deploy-truth | jq .` | |
| dispatch smoke | curl -N -H 'content-type: application/json' -d '{"intent":"hello","threadId":"manual"}' http://127.0.0.1:3147/dispatch/chat | ||
| port owner | lsof -nP -iTCP:3147 -sTCP:LISTEN | ||
| dispatch log | tail -n 40 state/log/dispatch-chat.ndjson |
Self-Test
An agent reading this should:
- [ ] Keep the physical display loopback-only and atomic.
- [ ] Restart with
restart.shafter backend/harness edits. - [ ] Verify PID/bundle/source freshness before live claims.
- [ ] Use
snappybackend for cockpit dogfood. - [ ] Route visual answers through OpenUI Lang, not new regex/static emitters.
- [ ] Preserve server-owned thread/toolCall persistence.
- [ ] Prove behavior via
/system-health,/deploy-truth, logs, and installed app screenshots.
Found a gap? Edit this file. <!-- footer-injection-point -->
api.ts- the code it can call
#!/usr/bin/env npx tsx
/**
* state/lib/head-screen.ts -- Canonical state helpers for the local
* head-screen display.
*/
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
const HERE = dirname(fileURLToPath(import.meta.url));
const LOG_DIR = join(HERE, "..", "log");
export const HEAD_SCREEN_PATH = join(LOG_DIR, "head-screen.json");
export const HEAD_SCREEN_EVENTS_PATH = join(LOG_DIR, "head-screen-events.ndjson");
export const HEAD_SCREEN_MODES = [
"idle",
"listening",
"thinking",
"working",
"done",
"blocked",
"error",
] as const;
export type HeadScreenMode = (typeof HEAD_SCREEN_MODES)[number];
export type HeadScreenLaneStatus =
| "idle"
| "queued"
| "waiting"
| "paused"
| "active"
| "done"
| "blocked";
export type HeadScreenLane = {
id: string;
title: string;
status: HeadScreenLaneStatus;
task: string;
detail: string;
members: string[];
count: number;
};
export type HeadScreenCardTone = "neutral" | "info" | "good" | "warn" | "bad";
export type HeadScreenCard = {
label: string;
value: string;
tone: HeadScreenCardTone;
};
export type HeadScreenState = {
version: 1 | 2;
mode: HeadScreenMode;
headline: string;
detail: string;
task: string;
ttl_seconds: number | null;
expires_at: string | null;
source: string;
updated_at: string;
// version 2 optional extensions written by bin/head-screen/compose.ts; readers that
// only need the v1 core can ignore these safely.
[extra: string]: unknown;
};
type UnknownRecord = Record<string, unknown>;
export function headScreenDir(): string {
return LOG_DIR;
}
export function isHeadScreenMode(mode: unknown): mode is HeadScreenMode {
return typeof mode === "string" && (HEAD_SCREEN_MODES as readonly string[]).includes(mode);
}
function cleanText(value: unknown, fallback = ""): string {
if (typeof value !== "string") return fallback;
return value.replace(/\r\n/g, "\n").trim();
}
function parseTimestamp(value: unknown, field: string): string {
if (typeof value !== "string" || !value.trim()) throw new Error(`head-screen: missing ${field}`);
const parsed = Date.parse(value);
if (Number.isNaN(parsed)) throw new Error(`head-screen: invalid ${field}`);
return new Date(parsed).toISOString();
}
function parseTtlSeconds(value: unknown): number | null {
if (value === undefined || value === null || value === "") return null;
if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
throw new Error("head-screen: ttl_seconds must be a non-negative integer or null");
}
function computeExpiresAt(updatedAt: string, ttlSeconds: number | null): string | null {
if (ttlSeconds === null) return null;
return new Date(Date.parse(updatedAt) + ttlSeconds * 1000).toISOString();
}
function assertKnownMode(mode: unknown): HeadScreenMode {
if (!isHeadScreenMode(mode)) throw new Error(`head-screen: invalid mode "${String(mode)}"`);
return mode;
}
function assertShape(raw: unknown): UnknownRecord {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("head-screen: state is not an object");
return raw as UnknownRecord;
}
export function defaultHeadScreenState(now = new Date(), source = "head-screen"): HeadScreenState {
const updated_at = now.toISOString();
return {
version: 1,
mode: "idle",
headline: "",
detail: "",
task: "",
ttl_seconds: null,
expires_at: null,
source,
updated_at,
};
}
export function parseHeadScreenState(raw: unknown): HeadScreenState {
const obj = assertShape(raw);
const updated_at = parseTimestamp(obj.updated_at, "updated_at");
const ttl_seconds = parseTtlSeconds(obj.ttl_seconds);
const expires_at = obj.expires_at === undefined || obj.expires_at === null || obj.expires_at === ""
? computeExpiresAt(updated_at, ttl_seconds)
: parseTimestamp(obj.expires_at, "expires_at");
const expectedExpires = computeExpiresAt(updated_at, ttl_seconds);
if (ttl_seconds === null && expires_at !== null) throw new Error("head-screen: expires_at must be null when ttl_seconds is null");
if (ttl_seconds !== null && expires_at !== expectedExpires) throw new Error("head-screen: expires_at does not match ttl_seconds");
const versionRaw = obj.version === undefined ? 1 : obj.version;
const version = (versionRaw === 1 || versionRaw === 2) ? (versionRaw as 1 | 2) : (() => { throw new Error("head-screen: unsupported version"); })();
const base: HeadScreenState = {
version,
mode: assertKnownMode(obj.mode),
headline: cleanText(obj.headline),
detail: cleanText(obj.detail),
task: cleanText(obj.task),
ttl_seconds,
expires_at,
source: cleanText(obj.source, "head-screen") || "head-screen",
updated_at,
};
// Preserve v2 extensions verbatim (lanes, roster, signals, recipes, scene, now, recent, need…).
if (version === 2) {
for (const k of Object.keys(obj)) {
if (!(k in base)) base[k] = obj[k];
}
}
return base;
}
export function validateHeadScreenState(state: HeadScreenState): void {
if (state.version !== 1 && state.version !== 2) throw new Error("head-screen: unsupported version");
assertKnownMode(state.mode);
if (!cleanText(state.source)) throw new Error("head-screen: source is required");
parseTimestamp(state.updated_at, "updated_at");
const ttl_seconds = parseTtlSeconds(state.ttl_seconds);
if (ttl_seconds !== state.ttl_seconds) throw new Error("head-screen: ttl_seconds is invalid");
const expectedExpires = computeExpiresAt(state.updated_at, ttl_seconds);
if (expectedExpires !== state.expires_at) throw new Error("head-screen: expires_at mismatch");
}
export function readHeadScreenState(): HeadScreenState {
if (!existsSync(HEAD_SCREEN_PATH)) {
const state = defaultHeadScreenState();
validateHeadScreenState(state);
return state;
}
const raw = readFileSync(HEAD_SCREEN_PATH, "utf8");
if (!raw.trim()) throw new Error("head-screen: state file is empty");
const state = parseHeadScreenState(JSON.parse(raw));
validateHeadScreenState(state);
return state;
}
export function writeHeadScreenState(next: HeadScreenState, atomic = true): HeadScreenState {
validateHeadScreenState(next);
if (!existsSync(LOG_DIR)) mkdirSync(LOG_DIR, { recursive: true });
const body = JSON.stringify(next, null, 2) + "\n";
if (atomic) {
const tmp = `${HEAD_SCREEN_PATH}.${process.pid}.${Date.now()}.tmp`;
writeFileSync(tmp, body, "utf8");
renameSync(tmp, HEAD_SCREEN_PATH);
} else {
writeFileSync(HEAD_SCREEN_PATH, body, "utf8");
}
return next;
}
export function nextHeadScreenState(
current: HeadScreenState,
patch: Partial<Pick<HeadScreenState, "mode" | "headline" | "detail" | "task" | "ttl_seconds" | "source">> & {
updated_at?: string;
},
): HeadScreenState {
const updated_at = patch.updated_at ? parseTimestamp(patch.updated_at, "updated_at") : new Date().toISOString();
const ttl_seconds = patch.ttl_seconds === undefined ? current.ttl_seconds : parseTtlSeconds(patch.ttl_seconds);
const next: HeadScreenState = {
version: 1,
mode: assertKnownMode(patch.mode ?? current.mode),
headline: cleanText(patch.headline ?? current.headline),
detail: cleanText(patch.detail ?? current.detail),
task: cleanText(patch.task ?? current.task),
ttl_seconds,
expires_at: computeExpiresAt(updated_at, ttl_seconds),
source: cleanText(patch.source ?? current.source, "head-screen") || "head-screen",
updated_at,
};
validateHeadScreenState(next);
return next;
}
export function headScreenFreshness(state: HeadScreenState, now = Date.now()): {
fresh: boolean;
age_ms: number;
remaining_ms: number | null;
stale_reason: string | null;
} {
const updatedMs = Date.parse(state.updated_at);
const age_ms = Number.isNaN(updatedMs) ? 0 : Math.max(0, now - updatedMs);
if (state.expires_at) {
const expiresMs = Date.parse(state.expires_at);
const remaining_ms = Number.isNaN(expiresMs) ? null : expiresMs - now;
return {
fresh: remaining_ms === null ? false : remaining_ms >= 0,
age_ms,
remaining_ms,
stale_reason: remaining_ms !== null && remaining_ms < 0 ? "ttl-expired" : null,
};
}
return { fresh: true, age_ms, remaining_ms: null, stale_reason: null };
}
scripts- helper scripts it can run
prose-only skill - 2 inline code blocks live in SKILL.md above (no state/bin/ sidecar yet).
how we check it- the checks, plus the last 10 runs
no recent runs logged - the eval contract is declared but nothing has been graded yet