OR Key
drop another .md file to compare - side-by-side diff against head-screen

head-screen

Runs the live status screen that shows what your assistant is doing.
personal 2 files

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.

Work with me
For developers how this skill is built, graded, and how it runs

at a glance- the short version

actorWriter that sets the state
auditorState/bin/head-screen/audit.ts, which re-reads the JSON and
eval modeauto
categorySystem
stages1
dependslog, eval

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.

The skill
state/skills/head-screen/SKILL.md present
the skill itself, in plain text
The main file. It says what the skill is and lays out the steps in plain English.
Code
state/lib/head-screen.ts present
code the skill can run
Reusable code this skill can call when it needs to.
Scripts
state/bin/head-screen/ not present
helper scripts
Optional. Added when a skill has a few commands to run.
Loader
state/skills/head-screen/AGENTS.md present
what the AI loads on the fly
Loaded automatically the moment this skill is needed. Kept short on purpose.

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.

name
kind
check
state_file_valid_and_fresh
deterministic
Running 'npx tsx state/bin/head-screen/audit.ts' exits with status code 0 and reports score 1.0.
server_is_running
deterministic
A process named 'server.ts' or 'node' listening on a local port associated with head-screen is running.
browser_display_present
deterministic
A Chrome browser window is open and displaying content from 'state/bin/head-screen/server.ts'.
no_eval_log_reads
judge
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.

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.

makes the work The worker
present
Writer that sets the state the worker
Does the actual work. Whatever it produces is what gets checked next.
checks the work The reviewer
present
State/bin/head-screen/audit.ts, which re-reads the JSON and the checker
A separate checker grades the work, so the part that made it can't approve its own work.
frame
learns Self-correction
not present

This skill doesn't fix its own gaps yet.

tidies up Background fixes
present
queued for rewrite runs in the background
Bigger fixes that can't be made on the spot get queued and rewritten in the background later.
remembers Run history
present
state/log/evals.ndjson auto runs
Every run is written down here, so the next time this skill is used it already knows how the last runs went.
Critical rules the things this skill must not get wrong
  1. The display is loopback-only. The browser face reads state/log/head-screen.json; it never reads eval logs or external state.
  2. 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.
  3. 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.
  4. 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.
  5. /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.
  6. 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.
  7. +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.

  1. Loading feedback rows…

how the work flows- who makes it, who checks it

inputs logeval
actor Writer that sets the state
auditor State/bin/head-screen/audit.ts, which re-reads the JSON and
1 auditor
npx
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 math
  • state/bin/head-screen/ctl.ts - set, get, reset CLI for state/log/head-screen.json
  • state/bin/head-screen/server.ts - local HTTP server and fullscreen browser page
  • state/bin/head-screen/audit.ts - deterministic shape/freshness eval
  • state/bin/head-screen/launch.sh - starts the server, spawns the menubar (⚡), and opens Chrome
  • state/bin/head-screen/menubar.swift + snappy-menubar binary - Mac status-bar icon; click to focus/reopen the console
  • Slash command: /snappy-face → runs HEAD_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 fresh head-screen.json (TTL 6h)
  • state/bin/head-screen/tick.sh + com.snappy.head-screen-tick.plist - LaunchAgent that runs compose.ts every 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: 1
  • mode: one of idle, listening, thinking, working, done, blocked, error
  • headline: short primary status text
  • detail: longer supporting text
  • task: current task label
  • ttl_seconds: optional freshness window in seconds
  • expires_at: derived expiry timestamp or null
  • source: who last wrote the state
  • updated_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.0 if the state parses, mode is valid, and the ttl has not expired
  • 0.5 if the state parses and the shape is valid but freshness has expired
  • 0.0 if 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.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.
  • 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>.json is server truth. Client /threads writes merge into existing server data and must not clobber toolCalls.
  • 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 with lsof -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

actioncommand
restart backend correctlybash state/bin/head-screen/restart.sh
launch if stoppedbash state/bin/head-screen/launch.sh
display set/get/reset`npx tsx state/bin/head-screen/ctl.ts setgetreset`
audit display statenpx tsx state/bin/head-screen/audit.ts
health`curl -s http://127.0.0.1:3147/system-healthjq .`
deploy truth`curl -s http://127.0.0.1:3147/deploy-truthjq .`
dispatch smokecurl -N -H 'content-type: application/json' -d '{"intent":"hello","threadId":"manual"}' http://127.0.0.1:3147/dispatch/chat
port ownerlsof -nP -iTCP:3147 -sTCP:LISTEN
dispatch logtail -n 40 state/log/dispatch-chat.ndjson

Self-Test

An agent reading this should:

  1. [ ] Keep the physical display loopback-only and atomic.
  2. [ ] Restart with restart.sh after backend/harness edits.
  3. [ ] Verify PID/bundle/source freshness before live claims.
  4. [ ] Use snappy backend for cockpit dogfood.
  5. [ ] Route visual answers through OpenUI Lang, not new regex/static emitters.
  6. [ ] Preserve server-owned thread/toolCall persistence.
  7. [ ] 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

rubric auto no rubric declared
recent no runs actor/auditor: unverifiable
deps log eval

no recent runs logged - the eval contract is declared but nothing has been graded yet