OR Key
drop another .md file to compare - side-by-side diff against cockpit-layout

cockpit-layout

Lays out your workspace so opening it shows your business, not technical clutter.
description: "Triggers on prompt mention of 'cockpit-layout'."
personal 2 files 10 recent evals

What it does for you

Lays out your workspace so opening it shows your business, not technical clutter.

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

eval modeshape
categorySystem
stages2

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/cockpit-layout/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/cockpit-layout.ts present
code the skill can run
Reusable code this skill can call when it needs to.
Scripts
state/bin/cockpit-layout/ not present
helper scripts
Optional. Added when a skill has a few commands to run.
Loader
state/skills/cockpit-layout/AGENTS.md present
what the AI loads on the fly
Loaded automatically the moment this skill is needed. Kept short on purpose.

how it runs - the shared frame every skill uses 3/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
inferred
see `state/skills/cockpit-layout/SKILL.md` Steps from the run command
No worker is named directly, so the command this skill runs is treated as the worker.
checks the work The reviewer
inferred
shape gate an automatic check
The check is an automatic pass or fail on the shape of the result, run separately from the work itself.
frame
learns Self-correction
present
fixes itself learns from gaps
When a run hits a gap, the skill gets edited on the spot [FIXED] or queued for a bigger rewrite [LOGGED], so it keeps getting better.
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 unknown 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. Before claiming a page-redesign done, run npx tsx state/lint/codable-drift.ts from snappy-os. This catches silent JSON↔Swift Codable decode drops — the canary that bit DraftsScreen 2026-04-27 (server shipped note/latency_ms/edited_chars/original_chars/synthetic per approval row, Swift ApprovalRow dropped all five — decision pill rendered as a bare String instead of a rich struct). Lint exits 1 on any DRIFT (server keys with no Swift property) or MISSING (non-optional Swift property not shipped by server). If the page you're touching has a row in the lint output, fix the Codable BEFORE shipping the visual changes — the visual layer is the symptom; the silent drop is the bug. (As of 2026-04-27 the lint flagged 23 server-keys-dropped across 6 endpoints; after the Drafts/Bench/Evals fixes it dropped to 14.)
  2. Pre-mount screens (BundlePickerScreen and anything that shows before state.bundleStatus.mounted == true) have a restricted endpoint allowlist. The server (state/bin/head-screen/server.ts ~L786-797) gates most endpoints behind a 503 ejected-bundle guard. Pre-mount surfaces can ONLY call /healthz, /state, /bundle/status, /bundle/mount, /bundle/eject, /bundle-meta, and /events (SSE). Any other endpoint will return 503. Don't accidentally introduce dependencies on /full-state, /crons, /skills-catalog, /recent, etc. — they all return 503 pre-mount. If a pre-mount screen needs richer data, propose a new bootstrap-allowlisted endpoint server-side rather than reaching into post-mount territory.
  3. Multi-rail snappy-shell pages MUST wrap their body in a ScrollView(.vertical, showsIndicators: true). The parent in snappy-shell/Sources/SnappyShell/RootView.swift (lines ~57-72) renders the detail column as a fixed VStack { content; Spacer(minLength: 0); footer } inside .padding(28) — the footer is pinned. Any screen body that exceeds the viewport height gets silently clipped (the bottom rails just vanish, no scrollbar). Symptom: page "stuck halfway done" — operator can't reach the lower rails. Working pattern (HomeScreen, AsksScreen, RoutinesScreen, BenchScreen, DraftsScreen):
  4. Parallel-subagent commit-flow: NEVER use a worktree-wide stage or stash. When multiple subagents work on snappy-shell concurrently (the standard fan-out pattern), other agents' tracked modifications and untracked files are sitting in the worktree alongside yours. The following commands will sweep them ALL into your action — silently:
  5. git stash push -u / git stash -u (sweeps tracked + untracked into one stash)
  6. git add -A / git add . (stages every modified + new file in the worktree)
  7. +7 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- step by step

1 generator
invoke
see `state/skills/cockpit-layout/SKILL.md` Steps section
2 data
eval log
`state/log/evals.ndjson` (skill: "cockpit-layout")

SKILL.md- the skill, written out in plain English

cockpit-layout - make the bundle look like the OS

When you open a snappy-os bundle in Finder, what you see should BE the sidebar. Not the engine room. The sidebar items collapse into 6 natural families per state/log/agents-md-feedback.log cockpit-recon-2026-04-26:

  • catalog/ - what the bundle CAN do (skills, apis, clients, agents, loaders)
  • ledger/ - what HAS happened (one evals.ndjson, six lenses)
  • memory/ - what's REMEMBERED (journal, daily, observations, asks)
  • inbox/ - incoming signals + queued work (observations, feedback, regen, meetings)
  • activity/ - what's HAPPENING (dispatches, commands, chain)
  • artifacts/ - what was PRODUCED (drafts, gallery)
  • meta/ - bundle ABOUT itself (env-vars, gateway, architecture, home)

This skill is idempotent - it can run repeatedly. Existing canonical files under state/ are left untouched; the operator surface is built from symlinks and one tiny generated _about.md per folder explaining what that folder is for.

Steps

npx tsx state/lib/cockpit-layout.ts

That command:

  1. Walks the canonical state/ tree
  2. Creates the 7 top-level operator folders
  3. For each folder, drops symlinks pointing at the canonical files/dirs
  4. Writes a small _about.md at the top of each folder describing what

it contains and which sidebar items it powers

  1. Logs an eval row with mode: shape because the auditor walks the

resulting tree and asserts every sidebar item has a path

Eval

shape. Auditor reads Route.swift-equivalent list (hardcoded in the lib), checks each section has either a folder or a symlink at the bundle root. Score 1.0 when every section resolves; partial credit per unresolved.

Why this is the bundle thesis

The thesis: a snappy-os bundle is the unit of distribution. The Swift shell renders whatever the bundle contains. Today the bundle's filesystem looks like a Node project; the operator sees node_modules/, package-lock.json, scattered markdown. After this skill runs, the bundle LOOKS like the OS in Finder. Same data, different surface.

AGENTS.md- what the AI loads when this skill comes up

cockpit-layout - loader

Per-turn rules for the cockpit-layout skill. Full reference: state/skills/cockpit-layout/SKILL.md. Do not skip these.

Critical Rules

  • Before claiming a page-redesign done, run npx tsx state/lint/codable-drift.ts from snappy-os. This catches silent JSON↔Swift Codable decode drops - the canary that bit DraftsScreen 2026-04-27 (server shipped note/latency_ms/edited_chars/original_chars/synthetic per approval row, Swift ApprovalRow dropped all five - decision pill rendered as a bare String instead of a rich struct). Lint exits 1 on any DRIFT (server keys with no Swift property) or MISSING (non-optional Swift property not shipped by server). If the page you're touching has a row in the lint output, fix the Codable BEFORE shipping the visual changes - the visual layer is the symptom; the silent drop is the bug. (As of 2026-04-27 the lint flagged 23 server-keys-dropped across 6 endpoints; after the Drafts/Bench/Evals fixes it dropped to 14.)

Important nuance - trace the page's actual data path, not the lint slug. The lint flags endpoint=X struct=Y, but the page you're working on may consume a different curated endpoint with a different Codable. Example 2026-04-27: lint flagged /recent → RecentEval but EvalsScreen actually consumes /recent-evals → EvalRow - fixing only the surfaced struct would have closed the lint with the page still blind. Always: read the screen's dataClient.<method> calls + their as: <Type>.self decode targets, then verify ALL of those Codables are clean against the live endpoints they hit. The lint surfaces a class of bug; the data path tells you which struct of that class your page reads.

  • Pre-mount screens (BundlePickerScreen and anything that shows before state.bundleStatus.mounted == true) have a restricted endpoint allowlist. The server (state/bin/head-screen/server.ts ~L786-797) gates most endpoints behind a 503 ejected-bundle guard. Pre-mount surfaces can ONLY call /healthz, /state, /bundle/status, /bundle/mount, /bundle/eject, /bundle-meta, and /events (SSE). Any other endpoint will return 503. Don't accidentally introduce dependencies on /full-state, /crons, /skills-catalog, /recent, etc. - they all return 503 pre-mount. If a pre-mount screen needs richer data, propose a new bootstrap-allowlisted endpoint server-side rather than reaching into post-mount territory.
  • Multi-rail snappy-shell pages MUST wrap their body in a ScrollView(.vertical, showsIndicators: true). The parent in snappy-shell/Sources/SnappyShell/RootView.swift (lines ~57-72) renders the detail column as a fixed VStack { content; Spacer(minLength: 0); footer } inside .padding(28) - the footer is pinned. Any screen body that exceeds the viewport height gets silently clipped (the bottom rails just vanish, no scrollbar). Symptom: page "stuck halfway done" - operator can't reach the lower rails. Working pattern (HomeScreen, AsksScreen, RoutinesScreen, BenchScreen, DraftsScreen):
  var body: some View {
      ScrollView(.vertical, showsIndicators: true) {
          VStack(alignment: .leading, spacing: 18) {
              rail1; rail2; ... railN
          }
          .frame(maxWidth: .infinity, alignment: .leading)
          .padding(.bottom, 16)
      }
  }

If a screen has only ONE rail with its own internal scroll (e.g. a single big LazyVStack-in-ScrollView), the outer wrapper is unnecessary - but the moment a second rail / banner / KPI strip joins it, add the outer ScrollView. (2026-04-27 HomeScreen redesign shipped 9 stacked rails inside a plain VStack; subagent omitted ScrollView, page clipped at viewport, manual fix required.)

  • Parallel-subagent commit-flow: NEVER use a worktree-wide stage or stash. When multiple subagents work on snappy-shell concurrently (the standard fan-out pattern), other agents' tracked modifications and untracked files are sitting in the worktree alongside yours. The following commands will sweep them ALL into your action - silently:
  • git stash push -u / git stash -u (sweeps tracked + untracked into one stash)
  • git add -A / git add . (stages every modified + new file in the worktree)
  • git commit -a (commits every tracked-modified file, even ones you didn't touch)

Each of these has caused a real incident in the 2026-04-27 fan-outs:

  • First-run-polish hit git stash push -u and recovered via git stash pop (lucky timing).
  • Batch E hit git add -A and shipped commit 227b0ef containing 7 other agents' WIP files (RootView, Route.swift, CommandScreen, LevelsScreen, NeverDoScreen, ParityScreen, TurnScreen). It built clean so no harm, but those agents now have to reconcile commits made of their unfinished work - confusing and recoverable only because nothing was destroyed.

The safe pattern is always explicit pathspec, every time:

  # Stage ONLY your paths
  git add Sources/SnappyShell/Screens/MyScreen.swift Sources/SnappyShell/Onboarding/MyView.swift
  # Now the rebase is clean — other agents' WIP stays unstaged
  git pull --rebase origin main
  git commit -m "..."
  git push

If you genuinely need a stash (e.g. mid-flight verify), git stash push -- <your-paths-only>. Don't use -u unless you specifically need untracked sweep AND have verified no other agent has untracked WIP in the same worktree (rare).

Stash-pop after rebase can SILENTLY revert your work. Batch F + Batch D (2026-04-27) both hit this: git pull --rebase auto-stashed the worktree, the rebase landed parallel-agent commits, the stash-pop hit a conflict, and conflict resolution dropped the agent's files back to their pre-edit state - file sizes were "original" again, no error, no warning. Recovery: git checkout stash@{0} -- <your-files> (the dropped stash entry still exists; check git stash list, sometimes the work is in stash@{1} if you stashed twice during the run). Detection: AFTER git pull --rebase, do wc -l <your-files> or git diff <ref-before-pull> -- <your-files> to confirm your edits are still in the worktree. If a file shrank back to baseline, the stash-pop ate it.

"File has been modified externally" warnings ARE real after a parallel rebase. The Edit tool will refuse if the file was changed since your last Read - in a parallel-agent worktree, git pull --rebase constantly rewrites files you've already read. Re-Read the file after every git pull --rebase before attempting an Edit, OR grep the disk to verify what's actually there. (Batch D 2026-04-27 spent extra cycles because warnings sometimes pointed at content that LOOKED stale but was the actual current disk state - the warning is correct, your in-memory view of the file is wrong.)

TOCTOU race on git addgit commit: use git commit -- <pathspec> instead. Even with explicit pathspec on git add, the index is shared across the worktree - another parallel agent's git add between your add and commit can pollute your staging area, silently. Server-endpoint subagent (2026-04-27) saw its git add -- <renames> staging twice replaced by another agent's git add loader-match.ts before its git commit wrote the tree. The race-free pattern bypasses the index entirely:

  # ONE step — pathspec on commit limits what's committed regardless of index state
  git commit -m "your message" -- Sources/SnappyShell/Screens/MyScreen.swift Sources/SnappyShell/Onboarding/MyView.swift

For NEW (untracked) files you still need git add first, but follow it immediately with git commit -- <paths> so the pathspec is the source of truth, not the index. Recovery if you committed a polluted index: git reset HEAD~1, restage your paths only, recommit with pathspec on commit.

  • Parallel-subagent xcodebuild: each agent MUST set its own -derivedDataPath /tmp/snappy-shell-build-<batch-name> so concurrent compiles don't collide on shared build artifacts. SourceKit (the IDE indexer) WILL show stale "No such module 'SnappyCore'" diagnostics during/after a multi-agent xcodegen pass - those are IDE staleness, not real build breaks. Confirm with a fresh xcodebuild from the command line before treating SourceKit diagnostics as actionable. (2026-04-27 6-subagent wave produced 24 SourceKit "module missing" diagnostics post-merge; xcodebuild from CLI was clean.)

Sub-rule for new files: when a subagent adds a NEW .swift file to Sources/SnappyShell/Components/ (e.g. extracting KpiTile to a shared component), it MUST run xcodegen AFTER writing the file AND before its first xcodebuild. xcodegen-then-write-then-build leaves the new file unregistered in project.pbxproj, and every screen that references the new symbol fails with cannot find 'X' in scope. This is a real build break, not SourceKit staleness - distinguish by whether the symbol exists on disk: if the file is there but unfindable, you missed the post-write xcodegen. (2026-04-27 Round-2 Batch E created KpiTile.swift after its initial xcodegen, broke FilesScreen/EnvVarsScreen/RemoteInboxScreen.)

  • Auto-regen Stop-hook can clobber attended-subagent commits. state/hooks/snappy-os-auto-regen.sh spawns a headless claude --dangerously-skip-permissions -p whenever a Stop event fires, and the global CLAUDE.md commit-protocol applies to that child too - meaning during attended-subagent windows the auto-regen child can rewrite commit messages or stage unrelated files. Real incidents 2026-04-27: commits 632b670 + b524b95 (verb-loader subagent) had their messages swapped by a parallel auto-regen run. Mitigations now in place: (a) the hook gates on a dirty-worktree check (excluding state/log/*) and defers when WIP is present; (b) regen briefs include an explicit ANTI-CLOBBER preamble forbidding the child from running ANY git operations (commit/add/push/stash/rebase/reset) - the headless regen MUST be file-edits-only. As an attended subagent: if you see commit-message swap or unfamiliar staged files appear mid-session, check state/log/auto-regen.log for an overlapping headless dispatch and recover via git commit --amend -m "<your-original-msg>" (or git reset HEAD~N + restage if files were taken). Per-attended-subagent rule: never start a claude -p from inside another claude session if there's any chance of git activity in the parent - let the Stop hook handle it after the parent exits.
  • Headless brief consumer MUST emit a writeback line - even when the brief is fully satisfied. If state/regen/drain.sh dispatches a brief whose patterns are all already-elevated (a [FIXED] line for each landed in state/log/loader-feedback.log within the 24h window), the consumer's natural read is "nothing to do" → exit silently. That silence is indistinguishable from a hung or broken consumer to state/lib/agi-loop-validator.ts, which fires agi-loop-regression [ALERT] on consumer no-op (no edits, no feedback line). Real incident 2026-04-27: validator FAIL at 10:30:07Z, consumer exit=-1, consumer no-op (no edits, no feedback line), brief had 8 items but every pattern was already elevated at 07:34:07Z. The consumer is REQUIRED to log one writeback per pattern in the brief - even if it's just pattern-elevate <name> (N writebacks, M distinct subagents) — already covered by Rule X. No new edit needed; suppressing re-flag. [FIXED]. A "suppress-only" writeback IS work - it confirms the consumer ran, read the brief, and made a deliberate no-action decision. The validator treats writeback presence as proof-of-life regardless of whether files were edited. (See agents-md-feedback.log entries from 07:34:07Z for the canonical suppress-only format.)
  • AGI-loop validator alerts bottom out at the consumer harness, NOT a per-loader rule. When state/log/agi-loop-validation-latest.md shows verdict=FAIL · consumer exit=-1 · consumer spawn error: spawn claude ENOENT, the regression is state/lint/agi-loop-validator.ts:492 calling spawn("claude", ...) with the Stop-hook's minimal PATH (no ~/.claude/local/, no Homebrew shims). Fix lives in the harness - resolve absolute path via which claude at startup or prepend PATH=$HOME/.claude/local:/opt/homebrew/bin:$PATH in the Stop-hook wrapper before invoking node. Symmetric failure mode: consumer exit=143 · consumer hung past 120s is the consumer running but stalled - usually because the brief exceeds BUDGET_SECONDS; raise SNAPPY_AGI_VALIDATE_BUDGET_SECONDS or split the brief at drain-time. Don't try to elevate "agi-loop-regression" as a subagent rule when the alert root cause is harness - open the validator script and fix at the spawn site.

Commands

| ui dashboard | state/skills/cockpit-layout/resources/ui.openui | |invoke: see state/skills/cockpit-layout/SKILL.md Steps section |eval log: state/log/evals.ndjson (skill: "cockpit-layout")

Self-Test

An agent reading this should correctly:

  1. [ ] Know which lib/bin artifact backs this skill (or that it is prose-only)
  2. [ ] Know what to write to state/log/evals.ndjson after invoking
  3. [ ] Know the eval mode (auto / shape / manual) from the .md frontmatter

Self-report

If this loader fell short, append a line:

echo "[$(date -u +%FT%TZ)] cockpit-layout: <what was missing>" >> state/log/loader-feedback.log

<!-- kernel-ok: historical attribution for the PID-loader pattern. -->

Self-correcting loader (PID feedback)

This is the rule that makes loaders learn. Ported from snappy-kernel, where it demonstrably worked: the snappy-dom-cartographer skill mapped Skool admin UI once, every later ship-agent inherited the full map for free.

Before you finish, do two things:

1. Fix gaps (P - proportional). MANDATORY. If this loader didn't cover your case - if you had to read another file, run an undocumented command, work around a wrong selector, or discover a quirk - you MUST attempt an Edit to this AGENTS.md before you log.

  • Default action: edit this file. Surgical. Add the missing line, table

row, or code example where it belongs. No restructuring.

  • Only LOGGED is allowed when: the fix needs >10 lines, spans multiple

files, or requires a structural rewrite. In that case the state/regen/drain.sh queue picks it up asynchronously.

  • "I didn't have time" / "it's minor" / "the next agent will figure it out"

are NOT valid reasons. Minor gaps compound - that's the whole point of this loop.

  • Concrete example of a valid P-fix: discover agent-browser screenshot

takes path positionally not as --output <path>. One line in a table. Edit it in. Do not just log it.

  • The goal: the next agent never has to leave the loader.

2. Log the result.

echo "[$(date -u +%FT%TZ)] cockpit-layout: <what was missing or fixed> [FIXED|LOGGED]" >> state/log/loader-feedback.log
  • FIXED = you patched this loader inline (P-fix).
  • LOGGED = too large for inline; the PostToolUse enqueue + Stop-hook drain

will rewrite the loader from scratch on next session-end.

Do not skip this. Every agent run must leave the system better than it found it. The loader is the setpoint; you are the sensor; the gap is the error signal; closing the gap is the correction.

OpenUI Resource

  • Skill-owned OpenUI Lang resource: state/skills/cockpit-layout/resources/ui.openui. Read it before rendering or editing this skill's generated component surface.
  • Treat this resource as a first-class artifact of the skill, not a generic chat response. Improve it when the skill's user-facing output needs to become richer.
  • System resources compose OpenUI primitives and inherit SnappyChat tokens. Use ui_contract: branded in SKILL.md only for deliberate platform or client visuals.

api.ts- the code it can call

#!/usr/bin/env npx tsx
/**
 * cockpit-layout.ts — build the operator-facing top-level folder layout
 * for a snappy-os bundle.
 *
 * Reads `state/cockpit-routes.json` (canonical sidebar manifest) and
 * creates 7 top-level folders (catalog/, ledger/, memory/, inbox/,
 * activity/, artifacts/, meta/). Inside each folder, drops a symlink
 * pointing at the canonical state/-rooted source for every route
 * declared in that family. Writes a small `_about.md` per folder.
 *
 * Idempotent: existing symlinks are replaced with the current target;
 * existing _about.md files are overwritten with the freshly-generated
 * version.
 *
 * Phase 1 (today): symlinks only — original state/ tree untouched.
 * Phase 3 (later): when state/ moves to _engine/state/, this file gets
 * a one-line update and re-runs to point at the new root.
 *
 * Run:
 *   npx tsx state/lib/cockpit-layout.ts
 */

import { readFileSync, writeFileSync, mkdirSync, symlinkSync, existsSync, lstatSync, unlinkSync, statSync, readdirSync, appendFileSync } from "fs";
import { join, dirname, relative, resolve, basename } from "path";
import { fileURLToPath } from "url";

const HERE = dirname(fileURLToPath(import.meta.url));
const BUNDLE = resolve(HERE, "..", ".."); // state/lib → bundle root

type Route = {
  id: string;
  label: string;
  source: string;
  kind: "file" | "dir" | "glob" | "view";
  filter?: string;
  view?: string;
  alias_of?: string;
  phase2?: string;
};
type Family = {
  id: string;
  title: string;
  routes: Route[];
};
type Manifest = {
  version: number;
  description: string;
  families: Family[];
  engine_room_paths: string[];
};

const manifest: Manifest = JSON.parse(
  readFileSync(join(BUNDLE, "state/cockpit-routes.json"), "utf-8")
);

/** Replace existing symlink/file at path with a symlink pointing at target. */
function relink(linkPath: string, target: string): void {
  if (existsSync(linkPath) || isBrokenLink(linkPath)) {
    unlinkSync(linkPath);
  }
  symlinkSync(target, linkPath);
}

function isBrokenLink(p: string): boolean {
  try { return lstatSync(p).isSymbolicLink(); }
  catch { return false; }
}

const reportLines: string[] = [];
let routesResolved = 0;
let routesMissing = 0;
let symlinksCreated = 0;
let viewsGenerated = 0;

for (const family of manifest.families) {
  const familyDir = join(BUNDLE, family.id);
  mkdirSync(familyDir, { recursive: true });

  // _about.md — what is this folder, what sidebar items it powers.
  const aboutPath = join(familyDir, "_about.md");
  const aboutBody = generateAbout(family);
  writeFileSync(aboutPath, aboutBody);

  for (const route of family.routes) {
    const srcAbs = join(BUNDLE, route.source);

    // For "view" kind routes, write a small _<id>.md explainer
    // pointing at the source data file. The real rendering lives in
    // the Swift app or downstream readers; this is for filesystem
    // browsing.
    if (route.kind === "view") {
      const viewPath = join(familyDir, `${route.id}.md`);
      writeFileSync(viewPath, generateViewExplainer(route));
      viewsGenerated++;
      if (existsSync(srcAbs)) {
        routesResolved++;
        reportLines.push(`✓ ${family.id}/${route.id}.md → view of ${route.source}`);
      } else {
        routesMissing++;
        reportLines.push(`✗ ${family.id}/${route.id}.md → MISSING ${route.source}`);
      }
      continue;
    }

    // alias_of — skip linking, just record
    if (route.alias_of) {
      reportLines.push(`↩ ${family.id}/${route.id} alias of ${route.alias_of}`);
      continue;
    }

    // file/dir/glob — drop a symlink (or directory of symlinks for glob)
    const linkPath = join(familyDir, route.id);

    if (route.kind === "glob" && route.filter) {
      // Build a directory full of links to matching files/dirs under source.
      mkdirSync(linkPath, { recursive: true });
      const matches = expandGlob(srcAbs, route.filter);
      for (const m of matches) {
        const entry = join(linkPath, basename(m));
        const rel = relative(linkPath, m);
        relink(entry, rel);
        symlinksCreated++;
      }
      if (matches.length > 0) {
        routesResolved++;
        reportLines.push(`✓ ${family.id}/${route.id}/ ← ${matches.length} matches of ${route.source}/${route.filter}`);
      } else {
        routesMissing++;
        reportLines.push(`✗ ${family.id}/${route.id}/ ← 0 matches of ${route.source}/${route.filter}`);
      }
      continue;
    }

    // file or dir — single symlink
    if (!existsSync(srcAbs)) {
      // Phase-2 routes (krisp) may not exist yet; mark as pending.
      const tag = route.phase2 ? `pending (phase 2: ${route.phase2})` : "MISSING";
      routesMissing++;
      reportLines.push(`✗ ${family.id}/${route.id} → ${tag}: ${route.source}`);
      continue;
    }
    const rel = relative(familyDir, srcAbs);
    relink(linkPath, rel);
    symlinksCreated++;
    routesResolved++;
    reportLines.push(`✓ ${family.id}/${route.id} → ${route.source}`);
  }
}

// Top-level _about.md — explains the layout itself
const layoutAbout = `# snappy-os bundle layout

This bundle's filesystem mirrors the snappy-shell sidebar. Open any of
these folders to see the operator surface; the engine room lives under
\`state/\` (and will move to \`_engine/state/\` in a future restructure).

${manifest.families.map(f => `- **${f.id}/** — ${f.title}`).join("\n")}

The canonical source for sidebar items is \`state/cockpit-routes.json\`.
Re-run \`npx tsx state/lib/cockpit-layout.ts\` after editing the manifest
to rebuild the symlinks.

Last built: ${new Date().toISOString()}
Routes resolved: ${routesResolved}, missing: ${routesMissing}, symlinks: ${symlinksCreated}, views: ${viewsGenerated}
`;
writeFileSync(join(BUNDLE, "BUNDLE.md"), layoutAbout);

// Eval log — shape audit
const evalRow = {
  ts: new Date().toISOString(),
  skill: "cockpit-layout",
  verb: "build",
  ok: routesMissing === 0,
  score: routesMissing === 0 ? 1.0 : Math.max(0, 1 - routesMissing / (routesResolved + routesMissing)),
  routes_resolved: routesResolved,
  routes_missing: routesMissing,
  symlinks_created: symlinksCreated,
  views_generated: viewsGenerated,
  writer_id: "state/lib/cockpit-layout.ts",
  actor_session_id: process.env.CLAUDE_SESSION_ID ?? `cockpit-layout-${Date.now()}`,
  auditor_session_id: `cockpit-layout-shape-${Date.now()}`,
};
const evalsPath = join(BUNDLE, "state/log/evals.ndjson");
appendFileSync(evalsPath, JSON.stringify(evalRow) + "\n");

console.log(reportLines.join("\n"));
console.log(`\nlayout: ${routesResolved} routes resolved, ${routesMissing} missing, ${symlinksCreated} symlinks, ${viewsGenerated} views`);
console.log(`eval row appended → state/log/evals.ndjson (score=${evalRow.score.toFixed(2)})`);
process.exit(routesMissing === 0 ? 0 : 1);

// =============================================================
function generateAbout(family: Family): string {
  const lines: string[] = [];
  lines.push(`# ${family.id}/ — ${family.title}`);
  lines.push("");
  lines.push(`This folder is part of the snappy-os bundle's operator surface — the`);
  lines.push(`top-level layout that mirrors the snappy-shell sidebar. Routes here:`);
  lines.push("");
  for (const r of family.routes) {
    const target = r.kind === "view" ? `view of ${r.source}` : r.source;
    lines.push(`- **${r.id}** — ${r.label} → \`${target}\`${r.alias_of ? ` (alias of ${r.alias_of})` : ""}`);
  }
  lines.push("");
  lines.push(`Generated by \`state/lib/cockpit-layout.ts\` from`);
  lines.push(`\`state/cockpit-routes.json\`. Edit the manifest, re-run, refresh.`);
  lines.push("");
  lines.push(`Last built: ${new Date().toISOString()}`);
  return lines.join("\n");
}

function generateViewExplainer(route: Route): string {
  return [
    `# ${route.label} — view`,
    "",
    `This route is a **derived view** of \`${route.source}\`.`,
    `The view shape is \`${route.view ?? "unknown"}\`.`,
    "",
    `Open \`${route.source}\` to see the underlying data.`,
    `The snappy-shell screen for this route renders the view; the file`,
    `you're reading is a filesystem-side breadcrumb so the bundle's`,
    `layout reflects the sidebar.`,
    "",
    `Generated by \`state/lib/cockpit-layout.ts\`.`,
  ].join("\n");
}

function expandGlob(srcDir: string, pattern: string): string[] {
  // Two patterns supported by cockpit-routes.json today:
  //   "client-*"      → files/dirs in srcDir whose name starts with client-
  //   "*/AGENTS.md"   → AGENTS.md inside each subdir of srcDir
  if (!existsSync(srcDir)) return [];
  if (pattern.startsWith("*/")) {
    const tail = pattern.slice(2);
    const out: string[] = [];
    for (const e of readdirSync(srcDir, { withFileTypes: true })) {
      if (!e.isDirectory()) continue;
      const candidate = join(srcDir, e.name, tail);
      if (existsSync(candidate)) out.push(candidate);
    }
    return out;
  }
  if (pattern.endsWith("*")) {
    const prefix = pattern.slice(0, -1);
    return readdirSync(srcDir)
      .filter(n => n.startsWith(prefix))
      .map(n => join(srcDir, n));
  }
  if (pattern.startsWith("*")) {
    const suffix = pattern.slice(1);
    return readdirSync(srcDir)
      .filter(n => n.endsWith(suffix))
      .map(n => join(srcDir, n));
  }
  return [];
}

scripts- helper scripts it can run

prose-only skill - 1 inline code block live in SKILL.md above (no state/bin/ sidecar yet).

how we check it- the checks, plus the last 10 runs

rubric shape schema-shape check (no inline rubric)
recent mean 0.97 · 10 runs actor/auditor: unverifiable
deps none declared
timestamp verb score primary_issue artifact
2026-05-03 04:39Z - 0.97 - -
2026-05-02 16:40Z - 0.97 - -
2026-05-02 04:40Z - 0.97 - -
2026-05-01 22:41Z - 0.97 - -
2026-05-01 16:42Z - 0.97 - -
2026-05-01 10:42Z - 0.97 - -
2026-05-01 04:39Z - 0.97 - -
2026-04-30 22:40Z - 0.97 - -
2026-04-30 16:39Z - 0.97 - -
2026-04-30 04:39Z - 0.97 - -