see `state/skills/cockpit-layout/SKILL.md` Steps .md file to compare - side-by-side diff against cockpit-layout
cockpit-layout
description: "Triggers on prompt mention of 'cockpit-layout'."
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.
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/cockpit-layout/SKILL.md
present
state/lib/cockpit-layout.ts
present
state/bin/cockpit-layout/
not present
state/skills/cockpit-layout/AGENTS.md
present
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.
state/log/evals.ndjson - 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.)
- 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):
- 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)
- +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.
- Loading feedback rows…
how the work flows- step by step
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:
- Walks the canonical
state/tree - Creates the 7 top-level operator folders
- For each folder, drops symlinks pointing at the canonical files/dirs
- Writes a small
_about.mdat the top of each folder describing what
it contains and which sidebar items it powers
- Logs an eval row with
mode: shapebecause 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.tsfrom snappy-os. This catches silent JSON↔Swift Codable decode drops - the canary that bit DraftsScreen 2026-04-27 (server shippednote/latency_ms/edited_chars/original_chars/syntheticper approval row, SwiftApprovalRowdropped 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 insnappy-shell/Sources/SnappyShell/RootView.swift(lines ~57-72) renders the detail column as a fixedVStack { 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 -uand recovered viagit stash pop(lucky timing). - Batch E hit
git add -Aand shipped commit227b0efcontaining 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 add → git 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 freshxcodebuildfrom 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.shspawns a headlessclaude --dangerously-skip-permissions -pwhenever 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: commits632b670+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 (excludingstate/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, checkstate/log/auto-regen.logfor an overlapping headless dispatch and recover viagit commit --amend -m "<your-original-msg>"(orgit reset HEAD~N+ restage if files were taken). Per-attended-subagent rule: never start aclaude -pfrom inside anotherclaudesession 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.shdispatches a brief whose patterns are all already-elevated (a [FIXED] line for each landed instate/log/loader-feedback.logwithin the 24h window), the consumer's natural read is "nothing to do" → exit silently. That silence is indistinguishable from a hung or broken consumer tostate/lib/agi-loop-validator.ts, which firesagi-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 justpattern-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.mdshowsverdict=FAIL · consumer exit=-1 · consumer spawn error: spawn claude ENOENT, the regression isstate/lint/agi-loop-validator.ts:492callingspawn("claude", ...)with the Stop-hook's minimal PATH (no~/.claude/local/, no Homebrew shims). Fix lives in the harness - resolve absolute path viawhich claudeat startup or prependPATH=$HOME/.claude/local:/opt/homebrew/bin:$PATHin the Stop-hook wrapper before invoking node. Symmetric failure mode:consumer exit=143 · consumer hung past 120sis the consumer running but stalled - usually because the brief exceedsBUDGET_SECONDS; raiseSNAPPY_AGI_VALIDATE_BUDGET_SECONDSor 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:
- [ ] Know which lib/bin artifact backs this skill (or that it is prose-only)
- [ ] Know what to write to
state/log/evals.ndjsonafter invoking - [ ] 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
LOGGEDis 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: brandedin 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
| 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 | - | - |