agentleFS
fabioc-alohafabioc-aloha/Alex_ACT_ONE/skills/docs-shell/SKILL.md

docs-shell

Skill0 starsChanged 9 days agoMITView on GitHub

The single-page HTML shell (index.html + manifest.json at a repository root or stable subfolder) that renders concatenated markdown as browsable, GitHub-styled documentation with a two-line topnav, per-doc emoji icons, sticky page header, and sidebar TOC. Use when the user says 'shell', 'add a doc', 'add a chapter', 'landing page', 'sidebar', 'manifest', 'hero', 'nav-strip', 'shell theme', 'color scheme', 'polish the pages', 'render preview', 'add an area', or when authoring/editing content that appears in the root manifest. Also invoke when the shell misrenders (raw frontmatter visible, links broken across folders, missing hero, doc button not switching content, sticky header overlap).

The file

We split this skill into the rules it gives an agent

9 rules. About 0 are common, 4 are distinctive, and 5 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈4

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 5

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Do NOT fire when:

    line 23
  2. Very specific:

    Before rendering, loadMarkdown() removes three per-file blocks:

    line 83
  3. Distinctive:

    No network call, no API key, no dependency — the browser owns the voice, so quality varies by host and the shell does not try to hide that.

    line 100
  4. Distinctive:

    Chunked playback on a duration budget: Every utterance boundary costs an audible gap — measured near 80 ms, and pre-queueing the next …

    line 112
  5. Distinctive:

    Run the bundled capability audit before replacing a customized shell.

    line 203
  6. Very specific:

    Create the .md file (path relative to repo root).

    line 216
  7. Very specific:

    Create the .md file(s).

    line 222
  8. Very specific:

    Consider whether to bump defaultArea if this new area should be the landing.

    line 229
  9. Very specific:

    Add "icon": "🛒" (single emoji character) to the doc entry.

    line 234

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

docs-shell skill

Load-on-demand pointer for the parent agent. Full technical reference lives at references/shell-reference.md; this file carries the essentials so the agent can reason about the shell without re-reading the reference every time.

Adopting the shell in another project? Go straight to references/shell-reference.md § Adopting the shell in another project. The starter kit at starter/ alongside this file is the copy-paste bundle.

When to invoke

Fire this skill when any of these appears in the user's request:

  • Shell operations: "shell", "index.html", "manifest", "landing page", "sidebar", "hero", "quickJump"
  • Add or edit content: "add a doc", "add an area", "add a chapter", "new page", "polish the pages"
  • Rendering issues: "raw frontmatter showing", "link broken in shell", "nav duplicated", "hero missing", "doc button not switching"
  • Theme / UX: "color scheme", "dark mode", "shell theme", "re-theme", "layout"
  • Cross-navigation: "?area=", "?doc=", "deep link", "URL scheme"

Do NOT fire when:

  • The user is editing content of an existing doc and the change does not affect manifest, hero, or nav-strip.
  • The user asks about pre-rendered *.html artifacts outside the shell folder.

The 90% mental model

One stable shell root. Repository root is recommended because links and deployment are simplest there. A stable subfolder such as docs/ is also supported: keep index.html and manifest.json together and resolve every source from that folder. The retired pattern was multiple generated folder shells, not the use of one intentional subfolder shell.

Manifest drives everything. The shell reads manifest.json on load and renders whatever it declares. No filesystem discovery. Adding a doc = one JSON entry, no HTML changes.

Markdown stays authoritative. MD files are the shell's primary source: fetched, concatenated, decorated. The shell is a viewer; never generate HTML from markdown into the repo. Pre-built HTML reports (Flint chart output, exported dashboards, tabular reports) are supported as an escape hatch: doc entries whose sources[] are all .html link directly to the file instead of being wrapped by the shell. See "Add an HTML-source doc" under Common tasks.

Two-line topnav. Line 1 = brand slot + area buttons. Line 2 = documents of the active area. URL scheme is ?area=<id>&doc=<slug> with cascading fallbacks (see reference for the full table).

Rendered reading surface. The shell does not expose raw Markdown controls. Relative links to sources already registered in the manifest route to their rendered shell pages; source files remain authoritative for authors. Parsed Markdown passes through DOMPurify before insertion, and Mermaid runs afterward in strict mode. CDN assets are exact-version and SHA-384 pinned. On narrow screens, both nav rows scroll horizontally instead of growing into multiple rows; the TOC becomes static, defaults collapsed unless the reader saved a preference, and caps expanded height at 360px so it cannot overlap content. Keyboard users get a skip link, accessible heading permalinks, visible focus states, and aria-current on active navigation. Touch users can always reach live copy feedback, and reduced-motion preferences disable animation and smooth scrolling. After Mermaid renders, the shell crops once to graph bounds, derives a natural width from the cropped viewBox and source font size, and shrink-wraps compact diagrams instead of stretching every SVG to page width. Contained scrolling is reserved for diagrams that cannot preserve a 13px desktop or 11px mobile label floor inside the available width.

Manifest schema, essential fields

{
  "brand": { "label": "…", "href": "index.html" },
  "theme": { "light": { "--accent": "#…" }, "dark": { "--accent": "#…" } },
  "defaultArea": "plan",
  "areas": [
    {
      "id": "plan",
      "label": "Plan",
      "folder": "plan",
      "defaultDoc": "about",
      "docs": [
        {
          "id": "mall",
          "label": "Mall Plan",
          "icon": "🛒",
          "title": "Mall Plan — role + modernization",
          "verified": "Phase 0 closed 2026-07-27",
          "hero": {
            "eyebrow": "Ch 05 · Mall Plan",
            "title": "Mall Plan",
            "subtitle": "Path A in-place bump to 3.0.0; no v2 fork per ADR-014."
          },
          "sources": ["plan/mall/README.md"]
        }
      ]
    }
  ]
}

Per-doc icon is an optional single emoji shown in the sticky page-title header. Empty or absent collapses via CSS :empty.

Per-doc hero.subtitle is the Big Idea (one-sentence thesis). hero.description is optional metadata preserved in the manifest but not rendered by default since 2026-07-28.

Source paths are relative to the manifest, whether that manifest lives at repository root or in a stable subfolder.

Full field-by-field walkthrough (types, required flags, purpose): references/shell-reference.md § Manifest schema.

What the shell auto-strips from source markdown

Before rendering, loadMarkdown() removes three per-file blocks:

  1. Leading YAML frontmatter — regex ^---\r?\n[\s\S]*?\r?\n---\r?\n?. LLM-only metadata.
  2. Nav-strips — regex <!-- nav-strip -->[\s\S]*?<!-- \/nav-strip -->\s*. Per-file navigation that would duplicate in concat view.
  3. Banner-strips — same mechanism for banner images.

Content docs may (and often should) keep frontmatter and nav-strips. GitHub honors them; the shell strips them cleanly.

Theme system

manifest.theme.light and manifest.theme.dark are optional maps of CSS custom properties. Absent = shell uses hardcoded defaults. Present = shell injects a <style> block with the declared vars, overriding the defaults. The injector accepts only ---prefixed keys with hex / rgb / hsl / named-color values so an untrusted manifest cannot smuggle arbitrary CSS.

Full override list at references/shell-reference.md § Every property you can override.

Read aloud

The shell reads the rendered page using the browser's built-in Web Speech API. No network call, no API key, no dependency — the browser owns the voice, so quality varies by host and the shell does not try to hide that.

PieceWhere
Controls#topnav-listen in the second nav row: play/pause #listen-toggle, settings gear #listen-settings. Two buttons, not three — see below
Settings popover#listen-panel — voice <select>, speed <input type="range"> (0.6–1.6), a #listen-markers checkbox for skip announcements, and a #listen-hint line naming what the host actually offers
Announcements#listen-status, an .sr-only role="status" live region
LogicsetupReadAloud(), called from the bootstrap sequence

Behavior worth knowing before you change it:

  • Chunked playback on a duration budget. Every utterance boundary costs an audible gap — measured near 80 ms, and pre-queueing the next utterance does not close it, so the only way to reduce the pauses is to produce fewer seams. The cap is therefore a duration budget rather than a fixed character count: CHUNK_SECONDS × CHARS_PER_SECOND × rate, clamped to CHUNK_MIN/CHUNK_MAX, so a faster rate earns longer chunks. A ceiling still exists because Chromium can cut an utterance that runs far past fifteen seconds. Each chunk carries a length-derived timeout as a backstop, because Chromium can drop an utterance without ever firing onend. A period only ends a sentence when a space follows it and the preceding word is not a known abbreviation, so 0.9.0, README.md, and "vs." are not read as three sentences. A single long sentence is allowed to overrun the budget and is only word-wrapped past a hard ceiling, because a break mid-sentence is the worst-sounding seam of all.
  • A keep-alive pump guards the long chunks. Chromium can stop speaking partway through a long utterance unless the queue is nudged, so a timer pauses and resumes while speaking. Measured as a no-op where the fault is absent (identical duration with and without), which makes it cheap insurance rather than a workaround with a cost.
  • Skips what does not survive being read. Tables, code blocks, Mermaid diagrams, and inline SVG are announced (Table skipped.) rather than spoken, because a table read cell by cell is noise and a code block is worse. The announcement is deliberately terse and a run of adjacent skips says it once: a doc-heavy corpus can carry over a thousand of these, and the announcement is interruption, not content. The #listen-markers checkbox turns them off entirely and persists that choice. Nav strips are dropped silently as chrome. Bare URLs become "link". Inline code becomes "code" only when it is both longer than INLINE_CODE_MAX and punctuation-dense: a CSS selector collapses, a long path or identifier does not, because the path carries more than the word "code" does. Every chunk is then checked after splitting, so a stray table pipe or a fragment that is nothing but a placeholder never reaches the voice.
  • Nav strips are detected by residue, not density. The test is what survives once the links and separators are removed. An earlier link-density ratio plus a "contains a middot anywhere" clause silently swallowed ordinary sentences that happened to carry two links, which is a worse failure than reading a breadcrumb: dropped prose is invisible, a spoken breadcrumb is merely noise.
  • Click to seek. Clicking any block while a session is live jumps playback there, forward or back. It stays inert until the reader has actually started, and ignores clicks on links and controls, modified clicks, and clicks that ended a text selection, so ordinary reading and copying are untouched. #content carries .is-seekable during a session to earn the pointer cursor.
  • Voice ranking. populateVoices() prefers whatever neural voice the host exposes (on Windows the Microsoft … Natural set) and demotes basic system voices, which are markedly worse for long-form prose.
  • The reader's choice persists. Voice, speed, and the skip-announcement setting are saved to localStorage under alexact.readaloud. Changing voice or speed while playing restarts the current chunk rather than the whole page; toggling announcements rebuilds the chunk list and resumes from the same block, because a rebuild renumbers every index after it.
  • Graceful absence. A host with no voices disables the control and says so in #listen-hint instead of failing silently.
  • Speech outlives the document. pagehide and beforeunload both cancel, or navigating away leaves the page talking.
  • No stop button. Play/pause carries the whole interaction and a third button was not earning its width in the nav. stopAll() still exists and still runs on page finish, doc switch, and unload; Escape reaches it.
  • [hidden] needs restating. Both .topnav-listen and .listen-panel carry an author display rule, which outranks the user-agent [hidden] { display: none } rule. Without an explicit .listen-panel[hidden] { display: none; } the popover renders permanently open while panel.hidden reports true, so the script looks correct and the page is visibly wrong. Keep that rule if you restyle the panel.

Settings popover dismissal

The popover overlaps the content, so it is aggressive about getting out of the way. Three separate paths close it, and the idle timer is deliberately shorter while speaking:

TriggerResult
Reader starts or resumes playbackCloses immediately — they are done configuring
Idle while playingAuto-closes after PANEL_IDLE_PLAYING_MS (4s)
Idle while stopped or pausedAuto-closes after PANEL_IDLE_STOPPED_MS (12s)
Click outside, gear again, or EscapeCloses

Two guards keep this from being hostile. Any interaction inside the panel (pointermove, input, change, focusin, and siblings) restarts the idle clock, and the auto-close defers rather than fires while focus is inside the panel, so a keyboard user is never interrupted mid-adjustment. When a close does steal focus from inside the panel, focus returns to the gear button rather than falling to <body>.

Keyboard: L toggles play/pause from anywhere outside a form field. Escape backs out one layer at a time — it closes the popover if the popover is open, and otherwise stops playback.

Common tasks

Audit an existing adopter before upgrade

Run the bundled capability audit before replacing a customized shell. It reads the shell, adjacent manifest, declared sources, and local HTML dependencies; it never rewrites them. Pass --project-root when the shell lives in a stable subfolder so the report labels its location correctly.

node skills/docs-shell/scripts/audit-docs-shell.mjs --shell index.html --project-root .
node skills/docs-shell/scripts/audit-docs-shell.mjs --shell docs/index.html --project-root . --json

Exit 0 means every required invariant passed. Exit 2 means the shell needs an upgrade and the report names each failed invariant. Exit 1 means the input or adjacent manifest is invalid. Optional capabilities and unknown manifest fields are informational; treat the latter as local extensions to preserve, not defects to erase.

Upgrade in five steps: audit, classify local extensions, preview the replacement, reapply only extensions still needed, then sweep every manifest route at desktop and mobile widths. A byte comparison alone is insufficient because valid adopters extend the manifest and renderer.

Add a chapter to an existing doc

  1. Create the .md file (path relative to repo root).
  2. Append its path to the target doc's sources[] array in the root manifest. Order = concat order.
  3. Reload the shell — no build step.

Add a new doc

  1. Create the .md file(s).
  2. Append a docs[] entry to the target area with id, label, optional icon, title, optional verified, optional hero, and sources[].
  3. Reload.

Add a new area

  1. Append an entry to the top-level areas[] with id, label, optional folder, defaultDoc, and a non-empty docs[].
  2. Consider whether to bump defaultArea if this new area should be the landing.
  3. Reload.

Add an emoji icon to a doc

Add "icon": "🛒" (single emoji character) to the doc entry. Rendered at 22px in the sticky page-title header.

Retheme

Edit manifest.theme.light and manifest.theme.dark. Most adopters override just --accent, --accent-emphasis, and the neutrals (--fg, --bg, --bg-subtle). Semantic colors (--success, --attention, --danger) usually stay at their GitHub Primer defaults for accessibility.

Fix a broken cross-folder link in a source

The shell prepends the source's base directory to relative links via rewriteRelativeLinks(). If a link doesn't resolve, confirm the source path in sources[] includes the full folder prefix (e.g. plan/mall/README.md, not just README.md), and check that the link isn't accidentally root-relative (/foo) when it should be relative (foo).

Add an HTML-source doc (Flint report, exported dashboard)

For a doc whose sources[] are all .html files, the shell links the topnav button DIRECTLY at the file instead of injecting into the shell wrapper. Useful for reports that own their own cover, hero, typography, and print styles.

  1. Drop the HTML file(s) at a path relative to the manifest (typically alongside your .md sources).
  2. Append a doc entry to the target area's docs[] with id, label, optional icon, title, and sources set to the HTML file path(s):

    {
      "id": "report",
      "label": "Sales report",
      "icon": "📊",
      "title": "Sales by region, Q4",
      "sources": ["reports/sales-q4.html"]
    }
  3. To keep shell navigation visible inside the standalone report, load assets/report-topnav.js with defer. The script derives the shell root from its own URL, renders manifest-ordered navigation, preserves the report's spacing, and removes its spacer for print.
  4. Reload. Clicking the topnav button loads the HTML directly; the browser back button returns to whatever came before (the shell uses location.replace when redirecting, so the shell URL doesn't stack in history).

Rule: sources[] must be non-empty AND every entry must end in .html (case-insensitive) for direct-link behavior to fire. Mixed sources (.md + .html) fall through to the Markdown render pass, which would try to concat the HTML as text; keep the two shapes in separate doc entries.

Full rationale + design notes: docs/shell/README.md § HTML-source docs.

Anti-patterns

Anti-patternCorrection
Editing the shell HTML to add a docDocs are declared in manifest.json. HTML changes belong in the shell only when adding a new render behavior (chips, actions, brand icon).
Generating a static HTML file for a docThe shell IS the renderer. Just add the source .md to a docs[] entry's sources[].
Splitting one shell across multiple rootsKeep one intentional shell root. Repository root is recommended; a stable subfolder is supported when its index.html, manifest, sources, and report assets stay relative to that base.
Duplicating the shell's CSS into a .md fileContent docs are semantic markdown. The shell owns the visual layer.
Rendering hero copy that reads as AI marketinghero.subtitle is the Big Idea. If "important" or "central" substitutes without loss, the subtitle is decorative.
Adding an emoji icon that reads as decorationThe icon field is optional. Empty or absent collapses cleanly. Use it when the icon reinforces the doc's identity (a shopping cart for Mall Plan).
Leaving the read-aloud popover open during playbackIt overlaps the content it is reading. Playback start closes it, and the idle timer closes it. Do not remove either path without replacing it.
Shipping a bundled TTS voice or a cloud speech callThe Web Speech API is the whole point: no key, no network, no dependency. Host voice quality is the host's business — report it in #listen-hint, do not paper over it.
Verifying a show/hide change by asserting element.hiddenThe property flips even when an author display rule keeps the element painted. Assert getComputedStyle(el).display or the bounding box instead, on a freshly loaded page.
Reading a table aloud cell by cellRow text joined with commas is not prose. Skip the table and announce it so the listener knows to look at the screen.
Collapsing inline code on length aloneA long path still reads better than the word "code", and an element whose whole text is one code span collapses to a chunk that says nothing. Gate on punctuation density, then drop placeholder-only fragments.
Capping utterances at a fixed character countThe cap exists to dodge a duration limit, so express it as a duration. A fixed count makes a fast reader sit through seams they never needed, and each seam is an audible gap that pre-queueing cannot close.
Judging chrome by link densityOrdinary sentences carry links. Test what is left once the links and separators are removed; dropped prose is a silent failure, while a spoken breadcrumb is merely noise.
Making click-to-seek always liveChunks only exist once a session has started. Seeking on every click would hijack link clicks, copying, and text selection on a page nobody asked to have read aloud.

Optional features (CSS ready, opt-in)

FeatureWhere
Hero chips (hero.chips[])Reserved CSS .hero-chips / .chip. Extend renderHero() to enable.
Hero CTA buttons (hero.actions[])Reserved CSS .hero-actions / .btn. Extend renderHero() to enable.
Hero description paragraph (hero.description)Preserved in manifest, not rendered by default. Uncomment the description line in renderHero() to re-enable.
QuickJumps in topnav (quickJumps[])Reserved CSS. Some root shells keep line 1 minimal (areas only); the starter kit renders quickJumps for adopters who want them.

See references/shell-reference.md § Optional features for enable steps.

Starter kit for adopters

The complete adopter bundle lives at starter/:

starter/
├── ADOPTION.md             Portable fresh-adoption and upgrade safety guide.
├── index.html              Full working shell with quickJump CSS and wiring retained as an opt-in, built-in read-aloud, and the brand-icon <img> left commented out for adopters to enable.
├── manifest.json           Minimal single-area example. Every non-obvious choice has an inline $comment.
├── about.md                Working demo content with alerts, mermaid, syntax-highlighted code samples, and quickJump examples.
├── example-report.html     Standalone HTML report demonstrating the direct-link route.
└── assets/
    └── report-topnav.js    Optional persistent shell navigation for standalone reports.

To adopt: start with starter/ADOPTION.md, choose one stable shell root, copy the complete starter bundle there, edit manifest.json (change brand.label, add or remove theme overrides, add docs[] entries), and open index.html in a browser. Repository root is recommended; docs/ is supported when every manifest source is authored relative to it. Full walkthrough at references/shell-reference.md § Adopting the shell in another project.

Falsifiability

Revise this skill if any of the following fires:

  • A live root shell diverges from the starter's index.html such that copying the starter into another project no longer produces a working shell (byte-identity assumption broken).
  • A new adopter reports the starter's $comment fields do not surface a schema question they hit (the comments are meant to be self-documenting).
  • The two-line topnav or per-doc icon rendering changes shape without this skill being updated (drift between skill and shell).
  • Raw Markdown controls return, narrow-screen navigation wraps into tall rows, or the mobile TOC opens by default without an explicit saved preference.
  • A TOC remains sticky below 1100px, overlaps article content, or expands beyond 360px in the single-column layout.
  • A Markdown event-handler payload executes, DOMPurify failure falls back to unsanitized HTML, Mermaid leaves strict mode, or a CDN asset loses its integrity pin.
  • A Mermaid graph renders below 13px on desktop or 11px on mobile without contained scrolling, occupies less than half of its cropped SVG viewport, clips content after fitting, exceeds a 4:1 graph aspect ratio without a clear reason, or causes page-level horizontal overflow.
  • Zero adopters copy the starter in the observation window (skill is decorative for its intended audience).
  • The read-aloud settings popover stays open through playback, or auto-closes while the reader is still adjusting it (either direction means the dismissal model above is mistuned).
  • A host ships Web Speech voices that the ranking in populateVoices() orders worse than picking the first available voice.
  • Chunked playback stops needing the per-chunk timeout backstop because Chromium reliably fires onend (delete the backstop rather than carry it).
  • A restyle of .listen-panel or .topnav-listen drops the explicit [hidden] { display: none; } rule and the popover renders while the script reports it closed.
  • Removing the stop button costs a reader a reachable way out of playback (Escape, page finish, doc switch, and unload should cover it).
  • The skip list swallows content that was worth hearing, or the announced markers become noise on a table-dense page (either direction means the skip model is mistuned).
  • The nav-strip heuristic drops a real paragraph, or leaves a breadcrumb row being read aloud on most pages.
  • A chunk sized by the duration budget is cut off mid-sentence on a real host despite the keep-alive pump, or the pump itself introduces an audible artifact (drop CHUNK_SECONDS and re-measure rather than reverting to a fixed count).
  • Click-to-seek fires on a click the reader meant as a link, a copy, or a selection, or a listener cannot find how to jump because the pointer cursor is the only affordance.
  • A corpus sweep over the shell's own docs reports a spoken chunk with no letters or digits, a chunk that is only a placeholder word, or a skip-marker count that disagrees with the number of skippable elements in the page.
  • Two adopter audits classify intentional manifest extensions as required failures, or the audit mutates any file it reads.

Origin

Authored 2026-07-26, evolved through a per-folder pattern, and unified into a single root shell on 2026-07-28. Projects that adopted the earlier pattern may still run a per-folder shell from a local skill install.

Cross-links

  • references/shell-reference.md — canonical technical reference (manifest schema, theme, path rewriting, optional features, adoption, local rendering, troubleshooting)
  • starter/ — the adopter-facing starter kit
  • Related skills:
    • big-idea — how to author hero.subtitle copy
    • markdown-mermaid — Mermaid diagram authoring rules that fire when a doc contains a mermaid code block
    • svg-banner — branded SVG banner authoring

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.