No work step here. This is probably a skill that reads or coordinates, not one that produces something.
.md file to compare - side-by-side diff against snappy-chat-invariants
snappy-chat-invariants
What it does for you
The guarantees that keep your chat experience reliable.
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 2/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/snappy-chat-invariants/SKILL.md
present
state/lib/snappy-chat-invariants.ts
not present
state/bin/snappy-chat-invariants/
not present
state/skills/snappy-chat-invariants/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 - Never claim a feature is shipped without a corresponding invariant + lint. If you add a new structural guarantee to snappy-chat, the work is: (1) add the invariant to SKILL.md, (2) add the one-liner to this loader, (3) write or plan the lint. Do not stop at step 1.
- When you find a new bug class, add an invariant FIRST, then patch the instance. The invariant catalog is the fix for the class; the patch is the fix for the instance. Both are required. Patching without cataloging is how the class recurs.
- Planned lints are not shipped lints. All 11 invariant lints now exist and are wired into state/lint/check.ts (shipped 2026-05-01). Do not add new invariants without a paired lint file.
- Invariants #1–11 are ALL ENFORCED (2026-05-01). All 11 lint files exist and are wired into state/lint/check.ts. Invariants #1–7 shipped first; #8–11 (design-fidelity) shipped alongside them in the same session. The dogfood fallback (screenshot QA) is now a secondary gate, not the primary one.
- The "of course" framing is load-bearing. An invariant is only real when you can finish the sentence: "of course every X has Y — we lint it." If you cannot name the lint file (or it's marked planned), the invariant is aspirational; label it (planned) in SKILL.md.
- Headless consumer MUST emit writeback proof-of-life even when all patterns already-elevated. Silence is indistinguishable from hung consumer to agi-loop-validator.ts. Suppress-only writeback IS work.
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
what this step does
what this step does
what this step does
what this step does
what this step does
what this step does
what this step does
what this step does
what this step does
what this step does
what this step does
SKILL.md- the skill, written out in plain English
snappy-chat-invariants
Catalog of structural guarantees that govern the snappy-chat ↔ snappy-os boundary. Each invariant is a named contract; each contract has a lint that makes violation impossible before it ships.
The "of course" framing: just as snappy-os says "of course every skill has AGENTS.md - we lint it," snappy-chat says these things too. The invariants below are the catalog.
Boundary: invariants where snappy-os owns one side of the interface (server schema, dispatch format, chat-inject controls) may be wired into state/lint/check.ts. Pure snappy-chat frontend design discipline (tokens, card chrome, buttons, spacing, inline styles) is a cockpit-owned validation path. Those diagnostics may live here as reusable scripts, but snappy-os structural lint must not fail because a sibling app's CSS needs polish.
Invariants
1. Server/client Artifact schema parity
Invariant: Every field in the Artifact object the server persists and serves must match the TypeScript shape the client reads and renders - no missing keys, no phantom keys, no type drift.
Why: Wave 18 (2026-04-30) surfaced a refresh_url field that existed on the server response but was absent from the client type. The client silently discarded it; the "Refresh" button never wired up. A schema parity check would have caught this at build time, not during dogfood.
Enforced by: state/lint/schema-parity-check.ts - compares server-side Artifact interface against the TypeScript type in web/src/ at build time. Fails with a diff of mismatched keys.
Failure mode: Build fails. The diff is printed to stderr naming the exact mismatched field. The lint runs in CI and also as a pre-commit hook candidate.
2. No fully-static emitTriple in server.ts
Invariant: Every emitTriple(toolName, ...) call in server.ts must reference a dynamic data source. A call where the JSON payload is a hardcoded literal (no variable substitution, no runtime lookup) is dead weight - it means the "generative UI" is just a static template that could be a markdown block.
Why: The legacy KNOWN_COMPONENTS + per-shape regex block (145 LOC removed 2026-04-30) was full of static emitters that matched intent strings and returned hardcoded payloads. They made server.ts larger without adding capability and created false impressions of "working" features. The pattern recurs whenever someone adds a new shape under time pressure.
Enforced by: state/lint/no-static-emitter-data.ts - AST-walks server.ts for emitTriple calls where the third argument is an object literal containing no identifier references. Fails with line numbers.
Failure mode: Build fails. The lint output names the call site and flags it as a static emitter.
3. All system-prompt @<Op> references exist in BUILTINS
Invariant: Every @<Op> operator reference in any system-prompt fragment injected by server.ts must have a corresponding entry in the BUILTINS registry. A reference to an undefined operator produces no UI output and no error - it silently disappears, which is the worst failure mode possible for a chat surface.
Why: The INCREMENTAL_EDITING system-prompt fragment (added 2026-04-30) references @Patch as a taught syntax. If Patch is not in BUILTINS, every model-emitted patch block silently falls through the null-render path. The bug is invisible in dev because the model rarely emits patches in short test conversations; it surfaces in production multi-turn sessions.
Enforced by: state/lint/builtin-whitelist.ts - extracts all @<Op> references from system-prompt string literals in server.ts and cross-checks against the BUILTINS map. Fails with the undefined operator name.
Failure mode: Build fails. The lint prints the undefined operator and the source line where it was referenced.
4. DISPATCH_REGISTRY parser format matches server emit format
Invariant: Every entry in DISPATCH_REGISTRY (in web/src/dispatch-card.tsx) that parses a [[TOOL:Name]]...[[/TOOL]] block must expect the same format that server.ts actually emits for that tool name. Format includes: whether the body is JSON, OpenUI Lang text, or a function call string.
Why: The Lang entry parses the body as OpenUI Lang text. A new developer adding a shape might register it in DISPATCH_REGISTRY expecting JSON but wire the server to emit a function-call string - the parse succeeds (it's valid text) but renders garbage. The mismatch is caught only when the specific shape is exercised in a dogfood session.
Enforced by: state/lint/dispatch-contract.ts - reads a machine-readable format annotation from each DISPATCH_REGISTRY entry and compares it against the corresponding emitTriple call in server.ts. Fails when the annotations disagree.
Failure mode: Build fails. The lint names the tool and the conflicting format claims.
5. Every dispatch-registered shape has SaveArtifactButton via DispatchCardWrapper
Invariant: Any component registered in DISPATCH_REGISTRY that renders a user-facing result card must be wrapped in DispatchCardWrapper, which provides the SaveArtifactButton. A bare render without the wrapper means the user cannot save the result - violating the Wave 19 architecture contract.
Why: The pattern was established in Wave 19 to ensure every generative-UI output is saveable. Shapes added before the wrapper was introduced, or added by agents unfamiliar with the contract, skip the wrapper silently - the card renders but has no save affordance. Invariant shipped 2026-05-01.
Enforced by: state/lint/dispatch-wrapper-check.ts - verifies (A) the toolCalls.map block in GenAssistantMessage routes through <DispatchCardWrapper> with no bare return card bypasses, and (B) the DISPATCH_REGISTRY has the expected 115 saveable + 7 whitelisted entries. Wired into state/lint/check.ts. ENFORCED.
Whitelist (legitimate bypasses): FollowUpBlock (user-action, no save), ProgressList (live progress), PhaseDisclosure (structural), WorkingFolder (fs context), ContextPanel (conv context), FeedbackForm (form submit is the action), ConfirmDialog (modal, no artifact). Expand only when a lint failure surfaces a genuine exception.
Failure mode: state/lint/check.ts exits 1 and names the bypass location with line number.
6. Every chat-inject control verb in ALLOWLIST has a matching React handler
Invariant: Any verb string listed in the chat-inject ALLOWLIST (CHAT_INJECT_CONTROLS Set in routes/chat-inject.ts) must have a corresponding handler in chat-inject-controller.tsx. An ALLOWLIST entry without a handler means the verb is accepted by the server, pushed to the FIFO, popped by the client, and silently discarded - no error, no action.
Why: The chat-inject FIFO is the primary control channel for dogfood QA agents to drive the snappy-chat UI programmatically (WKWebView swallows synthetic clicks). As new control verbs are added (view-*, theme:*, select-thread), the ALLOWLIST on the server side and the if/else chain on the client side can drift apart. The drift is invisible until a dogfood subagent tries the verb and observes no UI change. QA Wave 21 caught one instance (view-pet was missing its client handler).
Enforced by: state/lint/chat-inject-contract.ts - extracts CHAT_INJECT_CONTROLS entries from routes/chat-inject.ts, extracts data?.control === "verb" patterns and startsWith("prefix:") wildcards from chat-inject-controller.tsx, and fails if any server verb lacks a client handler or vice versa. Server-only verbs (e.g. flush, which drains the queue without pushing to the client FIFO) are whitelisted with documented reasons inside the lint. Wired into state/lint/check.ts. ENFORCED 2026-05-01.
Verified: 11 client-side verbs verified (12 in ALLOWLIST, 1 server-only: flush). All server↔client pairs agree.
Failure mode: state/lint/check.ts exits 1 and names the out-of-sync verb with its file:line location.
7. Design Token Discipline in styles.css
Invariant: Every color, shadow, or border value in web/src/styles.css must reference a semantic token via var(--*). Raw oklch(), rgba(), hex #rrggbb, or named color literals (red, blue, etc.) are disallowed outside token-definition blocks (:root { } and :root[data-theme] { } blocks, where canonical values are defined).
Why: DESIGN.md declares a two-hue-axis token system (surfaces hue 95-106, accent hue 38-39). When a developer hard-codes oklch(62% 0.12 39) or rgba(0,0,0,0.16) directly into a rule, the theme-switch mechanism breaks silently - the value stays fixed across dark/light modes, and the single-source-of-truth principle (one token change updates every use site) is violated. Observed in current code: 8 instances including .health-dot-* tier colors, .file-drop-active drag target, .video-preview-frame pure-black background, and a user-message bubble shadow.
Enforced by: state/lint/design-token-discipline.ts - reads web/src/styles.css line by line, tracks :root { } token-definition blocks (allowed to have raw values), and flags any color-bearing property outside those blocks that contains raw oklch(), rgba()/rgb(), hex #xxx, or CSS named color keywords. Exempts: color-mix() and oklch(from var(...) ...) relative-color syntax (token-composing); :where() OpenUI overrides; @keyframes blocks; var(--x, fallback) fallback arguments. Standalone/cockpit-owned diagnostic; not part of snappy-os state/lint/check.ts.
Failure mode: The standalone diagnostic exits 1 and prints the offending file:line with the raw value and a suggested token family to use instead.
8. Card chrome contract - canonical radius + shadow tokens on card-class components
Invariant: Every card-class component in web/src/genui/*.tsx and web/src/components/*.tsx must use the canonical chrome tokens from DESIGN.md: borderRadius must be var(--radius-lg) or var(--radius-md) (not var(--radius) or raw numeric values), and boxShadow must be var(--shadow-card) or var(--shadow-soft) (not var(--shadow-md) or hardcoded rgba/hex). A "card-class" element is one with padding + border-or-radius + shadow in the same inline style object.
Why: 2026-05-01 design critique found HTMLPreview (web/src/genui/html-preview.tsx:139-141) using var(--radius) + var(--shadow-md) while sibling dispatch cards use var(--radius-lg) + var(--shadow-card). The visual inequality persists because each shape is authored independently with no linted contract - without enforcement, every new shape is a coin flip on which tokens the author reaches for. The result is an inconsistent depth vocabulary across the dispatch card catalog.
Enforced by: state/lint/card-chrome-contract.ts - walks web/src/genui/*.tsx and web/src/components/*.tsx, extracts inline style={{ }} blocks, identifies card-class elements (padding + border + shadow heuristic), and flags: var(--radius) on a card, var(--shadow-md) on a card, raw numeric borderRadius (except 999 pill pattern), and hardcoded rgba/hex boxShadow. Whitelisted: artifact-card.tsx, brain-character.tsx (icon glyphs), platform-preview shapes (Slack/LinkedIn/Tweet/Email/WhatsApp/Apple - brand-faithful colors per DESIGN.md), carousel.tsx (imperative DOM mutation, not JSX prop). Standalone/cockpit-owned diagnostic. ENFORCED 2026-05-01.
Failure mode: The standalone diagnostic exits 1 and prints the offending file:line with the disallowed token and the canonical replacement to use.
9. Every button-like element uses the .btn class system or a whitelisted exception
Invariant: Every <button> element in web/src/ (excluding the deprecated genui/ directory) must either use the .btn class system (btn, btn--sm, btn--lg, btn--primary, btn--ghost, btn--icon) shipped in commit a76d29d, or use an existing named CSS class with documented semantics (dispatch-card-btn, artifacts-view-button, sidebar-*, message-actions__btn), or be a whitelisted component with a documented reason. Plain <button> elements with inline style={{ padding: ..., height: ..., background: ... }} outside the whitelist are forbidden.
Why: Design critique 2026-05-01 surfaced three button systems with disagreeing dimensions: dispatch-card-btn at 32px height, artifacts-view-button at 33px, and the composer button at 26px. Inline-styled buttons are the vector for new bespoke heights entering the codebase without review. Every new developer context adds another one-off style block. Once .btn exists, there is no legitimate reason to reach for inline styles on a new button.
Whitelist (documented exceptions):
SaveArtifactButton(components/save-artifact-button.tsx) - heavy state machine, uses CSS vars only.FireButton(sidebar.tsx) - state machine (idle/firing/fired/error), tooltip-wrapped icon, CSS vars throughout.ProfileMenuItem(sidebar.tsx) - role=menuitem inside a dropdown, semantics differ from an action button.NavRow(sidebar.tsx) - inline style is layout-only (position: relative); button appearance controlled bysidebar-nav-rowCSS class.ArtifactReFireButton/ReFireIntentButton(components/artifacts-view.tsx) -artifactReFireButtonStyleconst uses CSS vars only; scheduled for migration to.btn--sm.
Enforced by: state/lint/button-contract.ts - walks every *.tsx under web/src/ (skipping genui/), finds <button elements, classifies each as GOOD (.btn class, named CSS class) or BAD (inline style with padding/height/background and no accepted className), skipping whitelisted components by scanning the surrounding function context. Exit 0 on pass, exit 1 with file:line + suggested fix on fail. Standalone/cockpit-owned diagnostic. ENFORCED 2026-05-01.
Failure mode: The standalone diagnostic exits 1 and prints the offending file:line with the suggested migration (className="btn btn--sm" or add to whitelist with documented reason).
10. Space grid - padding/margin/gap snap to 4/8/12/16/20/24/32
Invariant: Every padding, margin, and gap value in web/src/styles.css must use a pixel value from the approved grid: 0, 2, 4, 6, 8, 12, 16, 20, 24, 32, 40, 48, 64. Values between grid steps (5, 7, 9, 10, 11, 13-15, 17-19, 21-31, etc.) are disallowed. var(--*) token references, calc(), and clamp() expressions are exempt. Scrollbar widths (3-5px) are whitelisted per platform convention. Border-radius has its own allowed set: 2, 4, 6, 8, 10, 12, 16, 24, 999, 50%.
Why: The spacing audit (2026-05-01) found 207 off-grid values scoring 1.5/5 on visual rhythm. When arbitrary values like 7px, 9px, 10px, 14px, 18px, 22px appear in different rules, the UI reads as inconsistent even when the design tokens are correct - the eye perceives different weights of space that don't form a coherent rhythm. A single-source grid (4/8/12/16/24/32) ensures every gap is a recognizable step on the scale.
Enforced by: state/lint/space-grid.ts - reads web/src/styles.css line by line, tracks :root {} token-definition blocks (exempt), and flags any padding/margin/gap declaration whose px component is off-grid. Also audits border-radius against its own allowed set. Exits non-zero with top-20 file:line samples and total count. Standalone/cockpit-owned diagnostic. ENFORCED 2026-05-01.
Failure mode: The standalone diagnostic exits 1 and prints up to 5 file:line off-grid samples on stderr.
11. Inline style discipline - JSX style={{}} must use var(--*) tokens
Invariant: Every style={{}} prop in web/src/**/*.tsx that sets a color, spacing, or border-radius value must use a semantic token or an approved snap value. Specifically: color/background/borderColor/boxShadow must be var(--*) or a keyword (transparent, currentColor, inherit, none); padding/margin/gap must be var(--space-*) or snap to {0,2,4,6,8,12,16,20,24,32}; borderRadius must be var(--radius-*) or snap to {0,2,4,6,8,12,16,24,999}. Whitelisted: layout/display/flex/grid/position/overflow, width/height/opacity/zIndex, typography props, transform/transition, positional props (top/left/right/bottom).
Why: styles.css has Invariant #7 (design-token-discipline) and Invariant #10 (space-grid) enforcing token discipline in the CSS layer. But developers reach for style={{ padding: 14, color: "#616061" }} in JSX when iterating fast, bypassing both CSS lints. The result is an invisible two-tier system: CSS is clean, JSX is arbitrary. First observed during 2026-05-01 dogfood: slack-message-preview.tsx uses color: "#616061" (raw hex) and genui/budget-card.tsx uses gap: 10, both invisible to existing lints. 667 violations were found on first scan across 149 files - confirming the class exists at scale.
Enforced by: state/lint/inline-style-discipline.ts - scans all .tsx files under web/src/, extracts style={{ }} object literals, and flags raw string/number values for color/spacing/radius properties that don't conform to the token contract. Border shorthand values (1px solid var(--border)) are correctly allowed when they contain a var(--*) reference. Exits 1 with total count + top-10 file:line samples. Standalone/cockpit-owned diagnostic. ENFORCED 2026-05-01.
Failure mode: The standalone diagnostic exits 1 and prints the offending file:line with the property name, raw value, and which token family to use (var(--space-), var(--radius-), or var(--*) color token).
How to add a new invariant
- Name it plainly: "of course every X has Y."
- Write the bug class it prevents - cite a real wave or commit where it would have blocked a regression.
- Name the lint file that enforces it (
state/lint/<name>.ts). If the lint doesn't exist yet, mark it(planned). - Add it to this catalog AND add a one-line entry to
AGENTS.md. - If the lint is new, dispatch a subagent to write it. Do not ship the invariant without the lint (or a clear plan for the lint + a dogfood fallback).
Eval
This skill is a reference catalog. No actor function runs. Eval is shape: the file must parse, every invariant must have all four fields (Invariant / Why / Enforced by / Failure mode), and at least one lint must be wired (not planned).
Score convention:
| Outcome | Score |
|---|---|
| All four required fields present per invariant | 1.0 |
| Missing field in one invariant | 0.5 |
| File does not parse or invariant section absent | 0.0 |
AGENTS.md- what the AI loads when this skill comes up
snappy-chat-invariants - loader
Per-turn rules for the snappy-chat-invariants skill. Full reference: state/skills/snappy-chat-invariants/SKILL.md. Do not skip these.
Trigger conditions
Load this loader when the prompt mentions:
- "invariant" / "invariants"
- "structural guarantee"
- "snappy-chat invariants"
- "of course every" (the catalog framing)
- "lint" in combination with snappy-chat, dispatch-card, server.ts, or DISPATCH_REGISTRY
Critical Rules
- Never claim a feature is shipped without a corresponding invariant + lint. If you add a new structural guarantee to snappy-chat, the work is: (1) add the invariant to
SKILL.md, (2) add the one-liner to this loader, (3) write or plan the lint. Do not stop at step 1. - When you find a new bug class, add an invariant FIRST, then patch the instance. The invariant catalog is the fix for the class; the patch is the fix for the instance. Both are required. Patching without cataloging is how the class recurs.
- Planned lints are not shipped lints. All 11 invariant lints now exist and are wired into
state/lint/check.ts(shipped 2026-05-01). Do not add new invariants without a paired lint file. - Invariants #1-11 are ALL ENFORCED (2026-05-01). All 11 lint files exist and are wired into
state/lint/check.ts. Invariants #1-7 shipped first; #8-11 (design-fidelity) shipped alongside them in the same session. The dogfood fallback (screenshot QA) is now a secondary gate, not the primary one. - The "of course" framing is load-bearing. An invariant is only real when you can finish the sentence: "of course every X has Y - we lint it." If you cannot name the lint file (or it's marked
planned), the invariant is aspirational; label it(planned)in SKILL.md. - Headless consumer MUST emit writeback proof-of-life even when all patterns already-elevated. Silence is indistinguishable from hung consumer to
agi-loop-validator.ts. Suppress-only writeback IS work.
Invariant quick-reference
| # | Invariant | Lint file | Status |
|---|---|---|---|
| 1 | Server/client Artifact schema parity | schema-parity-check.ts | ENFORCED |
| 2 | No fully-static emitTriple in server.ts | no-static-emitter-data.ts | ENFORCED |
| 3 | All system-prompt @\<Op\> refs exist in BUILTINS | builtin-whitelist.ts | ENFORCED |
| 4 | DISPATCH_REGISTRY parser format matches server emit | dispatch-contract.ts | ENFORCED |
| 5 | Every dispatch shape wrapped in DispatchCardWrapper | dispatch-wrapper-check.ts | ENFORCED |
| 6 | Every chat-inject ALLOWLIST verb has React handler | chat-inject-contract.ts | ENFORCED |
| 7 | Design token discipline (var(--*) only in styles.css) | design-token-discipline.ts | ENFORCED |
| 8 | Card Chrome Contract - canonical radius + shadow tokens on card-class components | card-chrome-contract.ts | ENFORCED |
| 9 | Button System Contract - every button uses .btn class system or whitelisted exception | button-contract.ts | ENFORCED |
| 10 | Space Grid - padding/margin/gap snap to 4/8/12/16/20/24/32 | space-grid.ts | ENFORCED |
| 11 | Inline Style Discipline - JSX style={{}} must use var(--*) tokens (warn-level, non-blocking) | inline-style-discipline.ts | ENFORCED |
What to check on a given turn
| Task | Invariants to verify |
|---|---|
| Adding shape to DISPATCH_REGISTRY | #4 (format), #5 (wrapper) - both enforced by lint |
| New system-prompt fragment in server.ts | #3 (@Op refs exist in BUILTINS) - enforced by lint |
| Changing Artifact type on server | #1 (schema parity) - enforced by lint |
| New emitTriple call | #2 (no hardcoded literals) - enforced by lint |
| New chat-inject verb | #6 (add to ALLOWLIST AND wire React handler) - enforced by lint |
| Adding color/shadow/border to styles.css | #7 (use semantic tokens, no raw oklch/rgba/hex) - enforced by lint |
Dispatch-wrapper contract (Invariant #5)
Invariant #5 shipped 2026-05-01. Every shape rendered via DISPATCH_REGISTRY must be wrapped in <DispatchCardWrapper> to provide the SaveArtifactButton. The lint enforces (A) GenAssistantMessage's toolCalls map routes through the wrapper, (B) REGISTRY has 115 saveable + 7 whitelisted (no bare return statements).
Whitelist (legitimate bypass reasons): FollowUpBlock, ProgressList, PhaseDisclosure, WorkingFolder, ContextPanel, FeedbackForm, ConfirmDialog. Expand only when a lint failure surfaces a genuine exception.
If you discover a new shape should be whitelisted, add to the REGISTRY whitelist array in web/src/dispatch-card.tsx AND update this loader.
Chat-inject contract (Invariant #6)
Invariant #6 shipped 2026-05-01. Every verb in CHAT_INJECT_CONTROLS (server-side, routes/chat-inject.ts) must have a handler in client (chat-inject-controller.tsx).
Lint enforces: Server verbs ↔ client if/else handlers match; server-only verbs (e.g. flush) whitelisted with documented reasons.
Verified (2026-05-01): 11 client-side verbs, 12 ALLOWLIST entries, 1 server-only (flush). All pairs agree.
Adding a new verb: (1) add to CHAT_INJECT_CONTROLS in routes/chat-inject.ts, (2) wire the if (data?.control === "verb_name") handler in chat-inject-controller.tsx, (3) lint will verify parity on next check.ts run.
Design-token contract (Invariant #7)
Invariant #7 shipped 2026-05-01. Every color, shadow, or border in web/src/styles.css must use semantic tokens via var(--*). Raw values (oklch(), rgba(), hex #xxx, CSS color names) are disallowed outside :root { } definition blocks.
Lint enforces: Flags any property outside token-definition blocks containing raw color values; exempts color-mix(), oklch(from var(...)) relative-color syntax, @keyframes, and var(--x, fallback) fallback args.
Known violations (8 pre-existing, now enforced): .health-dot-* tier colors, .file-drop-active drag target, .video-preview-frame background, user-message bubble shadow - migrate to --color-* / --shadow-* tokens.
Adding a new style rule: (1) reference var(--token-name), (2) define the token in :root { } once (light+dark variants), (3) lint will verify on next check.ts run.
Self-Test
An agent reading this should correctly:
- [ ] Know ALL 11 invariants are enforced - every lint file exists and is wired in
state/lint/check.ts. - [ ] Know invariant #5 -
state/lint/dispatch-wrapper-check.ts, 122 shapes verified (115 saveable, 7 whitelisted). - [ ] Know invariant #6 -
state/lint/chat-inject-contract.ts, 11 verbs verified, 1 server-only (flush). - [ ] Know invariant #7 -
state/lint/design-token-discipline.ts, flags raw oklch/rgba/hex outside:root { }. - [ ] Know invariants #1-4 lint files shipped 2026-05-01:
schema-parity-check.ts,no-static-emitter-data.ts,builtin-whitelist.ts,dispatch-contract.ts. - [ ] Know invariant #8 -
state/lint/card-chrome-contract.ts, card-class components must use--radius-lg/--shadow-cardtokens. - [ ] Know invariant #9 -
state/lint/button-contract.ts, every button uses .btn class system or is whitelisted. - [ ] Know invariant #10 -
state/lint/space-grid.ts, padding/margin/gap must snap to 4/8/12/16/20/24/32 grid. - [ ] Know invariant #11 -
state/lint/inline-style-discipline.ts, JSX style={{}} must use var(--*) tokens (warn-level). - [ ] Add a new invariant to both SKILL.md AND this loader table before closing a turn.
- [ ] Cite the specific lint file when asked "what enforces X".
<!-- 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)] snappy-chat-invariants: <what was missing or fixed> [FIXED|LOGGED] action_kind=<kind>" >> 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.
action_kindvalues:shape-ok,skill-ran,loader-rewritten,pattern-elevated.
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.
api.ts- the code it can call
⚠ no api.ts - this skill has no typed action surface
scripts- helper scripts it can run
prose-only skill - no sidecar under state/bin/ yet. Steps, if any, are described in SKILL.md.
how we check it- the checks, plus the last 1 runs
| timestamp | verb | score | primary_issue | artifact |
|---|---|---|---|---|
| 2026-05-03 10:50Z | - | 1.00 | - | - |