/* Generates the PLATFORM skill — the reference an agent opens when a user asks what the platform can do. ── Why generated ── Because a hand-written one rots, and we measured it. Spotlight's settings list was hand-written and drifted within two days: nine rows against twenty-one sections, and a route that does not exist. `CLAUDE.md` claimed 27 permission entries against 18, and twenty PM2 processes against thirteen, both for over a week. A skill is worse than a doc when it is wrong, because an agent quotes it with confidence and the user has no way to tell. So the parts a registry can answer are derived here, at generation time, from the registry — and the prose is limited to what no registry can express. ── The split, and where the line is ── GENERATED permissions: key, label, kind, api prefixes, sockets, frontend routes keyboard shortcuts PROSE what the platform IS, what a permission KIND means for a user, where to look next Run `bun run skills:platform`. `bun run docs:check` regenerates into memory and fails if the committed file differs, so a stale skill is a failed check rather than a confident wrong answer. */ import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs'; import { join, resolve } from 'node:path'; const ROOT = resolve(import.meta.dir, '..'); export const SKILL_PATH = join(ROOT, '..', 'capabilities', 'skills', 'platform', 'SKILL.md'); const KIND_MEANING: Record = { core: 'every account has it and it cannot be denied — the caller acting on themselves', app: 'grantable per role, at read or write', confined: 'grantable, but only to an account that has its own Linux user; without one the grant resolves to nothing', execution: 'runs as the server owner in the owner’s home. NEVER grantable, at any level, structurally', admin: 'the server owner only', }; export async function renderPlatformSkill(): Promise { const { PERMISSIONS } = await import('../src/servers/permissions/registry'); const { SHORTCUT_SECTIONS } = await import('../src/apps/officer-web/state/shortcuts'); const enCatalog = (await import('../src/apps/officer-web/locales/en-GB.json')).default as Record; /** Resolve a dot-path against the English catalog. This doc is for agents and stays in one language. */ const enString = (key: string): string => { const value = key.split('.').reduce((node, part) => { if (node && typeof node === 'object') return (node as Record)[part]; return undefined; }, enCatalog); if (typeof value !== 'string') throw new Error(`gen-platform-skill: no English string for "${key}"`); return value; }; const byKind = new Map(); for (const p of PERMISSIONS) { const list = byKind.get(p.kind) ?? []; list.push(p); byKind.set(p.kind, list as typeof PERMISSIONS); } const lines: string[] = []; const out = (s = '') => lines.push(s); out('---'); /* `name` is an IDENTIFIER, not a label: lowercase-hyphenated and equal to the directory, per both harnesses' rules. The human name is `title`. This generator emitted `name: Officer platform` and was missed when c24859c fixed every skill by hand — so the file on disk was right and running this script put the old shape back. `docs:check` then failed on a line count that nobody could fix by regenerating, which is the worst place for a drift check to point. */ out('name: platform'); out('title: Officer platform'); // The description is the ENTIRE interface: the agent sees only this before deciding to open the body. // Vague makes a skill useless rather than merely verbose — see skills/load.ts. out('description: >-'); out(' Use when the user asks what Officer can do, how to use a feature, what an endpoint is, why something is'); out(' refused, or how permissions and accounts work — "how do I start a commander", "can a member see my files",'); out(' "what is the API for tasks". Carries the permission registry, every API prefix and socket, the frontend'); out(' routes and the keyboard shortcuts, derived from the code rather than written by hand.'); out('version: 1'); out('---'); out(); out(''); out(); out('# What Officer is'); out(); out('One self-hosted app for one person — the **owner** — that also admits additional accounts holding a'); out('strict subset of it. The owner is user id 1, role `Super Admin`, and bypasses every permission check.'); out('Everyone else gets what their ROLE is granted, and nothing else: grants are keyed on role, never on a'); out('user, and **absence denies** — there is no row meaning "no".'); out(); out('So "can this account do X" has three possible answers, and the third is the one people miss: yes,'); out('no, and *not shareable at all*.'); out(); out('# Permissions'); out(); out('The authority for this is `src/servers/permissions/registry.ts`. Mounting an API router without an'); out('entry there makes the server **refuse to boot** — so this list is complete by construction, not by'); out('diligence.'); out(); for (const [kind, meaning] of Object.entries(KIND_MEANING)) { const entries = byKind.get(kind) ?? []; if (!entries.length) continue; out(`## \`${kind}\` — ${meaning}`); out(); for (const p of entries) { const bits: string[] = []; if (p.api?.length) bits.push(`API \`${p.api.join('`, `')}\``); if (p.ws?.length) bits.push(`socket \`${p.ws.join('`, `')}\``); if (p.routes?.length) bits.push(`screen \`${p.routes.join('`, `')}\``); out(`- **${p.label}** (\`${p.key}\`) — ${p.description}`); if (bits.length) out(` - ${bits.join(' · ')}`); if (p.selfService?.length) { out(` - acts on the caller themselves, allowed with no grant: \`${p.selfService.join('`, `')}\``); } if (p.ownerOnly?.length) out(` - owner only, even when granted: \`${p.ownerOnly.join('`, `')}\``); if (p.readOnlyWrites?.length) { out(` - a POST that is really a read, allowed at read level: \`${p.readOnlyWrites.join('`, `')}\``); } } out(); } out('# Keyboard shortcuts'); out(); out('Derived from `src/apps/officer-web/state/shortcuts.ts`. These have moved several times; quote this'); out('list rather than remembering one.'); out(); // The registry holds catalog keys since the shell was localized; this doc is agent-facing and stays // English, so it resolves against en-GB rather than the reader's locale. for (const section of SHORTCUT_SECTIONS) { out(`## ${enString(section.titleKey)}`); if (section.noteKey) { out(); out(enString(section.noteKey)); } out(); for (const s of section.shortcuts) out(`- \`${s.keys}\` — ${enString(s.descriptionKey)}`); out(); } out('# What is NOT in here'); out(); out('- **Installed plugins.** They vary per machine and per install, so a generated list would be wrong on'); out(' every other server. Ask the platform: `GET /api/plugins` for what is installed here.'); out('- **Request and response shapes.** This says which prefix a permission covers, not what a route'); out(' accepts. Read the router under `src/servers/api//` when the shape matters.'); out('- **Anything about a commander beyond its permission.** That has its own skill — open it for what a'); out(' commander is, how one is started, and how to help somebody shape one.'); out(); out('# When you are about to guess'); out(); out('Do not. This file is generated from the registry, so a question it does not answer is a question the'); out('registry does not answer either — say so, and read the code. Every documented count in this repository'); out('that was maintained by hand has been wrong, and an agent quoting a stale number sounds exactly as'); out('certain as one quoting a true one.'); return `${lines.join('\n')}\n`; } if (import.meta.main) { const body = await renderPlatformSkill(); mkdirSync(join(SKILL_PATH, '..'), { recursive: true }); const existing = existsSync(SKILL_PATH) ? readFileSync(SKILL_PATH, 'utf-8') : null; if (existing === body) { console.log('skills:platform — already current'); } else { writeFileSync(SKILL_PATH, body); console.log(`skills:platform — wrote ${SKILL_PATH} (${body.split('\n').length} lines)`); } }