diff --git a/AGENTS.md b/AGENTS.md index f3bf89f..158e281 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,10 +17,10 @@ The global layer lives here and is wired into `~/.claude` by **directory symlink file added under `adapters/` is live immediately — no per-file linking: - `~/.claude/commands` → `adapters/claude/commands/` — global slash commands (`/retrofit`, - `/handoff`, `/full-eval`, `/capture`, `/triage`, `/roundup`, `/new-project`). + `/handoff`, `/full-eval`, `/capture`, `/triage`, `/roundup`, `/new-project`, `/design`). - `~/.claude/agents` → `adapters/claude/agents/` — global subagents (reviewer, evaluator, security-auditor, doc-auditor, exerciser, researcher, janitor, portability-checker, - start9-spec-checker). + start9-spec-checker, design-checker). - `~/.claude/CLAUDE.md` → `how-i-work.md` — my universal preferences, loaded every session. (Distinct from this repo's *root* `CLAUDE.md`, which → `AGENTS.md`: same filename, different scopes — global preferences vs. this repo's orientation.) @@ -92,7 +92,14 @@ should carry this so any vendor's agent surfaces pending items at session start: in `adapters/claude/`, symlinked into `~/.claude`). The repo dogfoods its own standard: inbox-check line, deny-by-default `.gitignore`, relative symlinks, capture→triage→roadmap loop. - `/roundup` writes a tracked `STATUS.md` snapshot each run (overwritten, committed + pushed — - diffable over time); latest snapshot dated 2026-06-15. + diffable over time); latest snapshot dated 2026-06-16. +- **Design system built (2026-06-16, ROADMAP item 8).** `/design` (main-thread command) runs + the inspiration-first design round-trip → a vendor-neutral on-disk contract (`design/DESIGN.md` + nine-section brief + `design/tokens.tokens.json` DTCG tokens); `design-checker` (read-only + subagent) audits a repo's UI against its own contract. Claude Design (cloud-only/experimental) + is the interchangeable front-end, never a dependency — its export is inline-hardcoded HTML/CSS, + so Phase-C token distillation is agent-mediated (research-verified 2026-06-16). `/new-project` + now scaffolds `design/` for user-facing projects. - `/new-project` is the inverse of `/retrofit`: workshops a captured `(new)` idea into a standards-compliant repo and publishes to Gitea via a manual-create gate; the stack quality gate is deferred to a future `/harden`. Now carries a **form-factor gate** (is this even a @@ -123,7 +130,9 @@ should carry this so any vendor's agent surfaces pending items at session start: trimming (per-repo AGENTS.md files aren't bloated, and in-repo copies aid self-containment). - **Next steps:** (1) the cross-repo quality-gate standard + `/harden` (ROADMAP item 1 — also unblocks `/new-project`'s deferred quality-gate step); (2) the non-git-folder sweep under - `~/Projects` (item-6 residual; count ~13). + `~/Projects` (item-6 residual; count ~13); (3) first live `/design` run + `design-checker` + backfill on a user-facing repo, which will also confirm what Claude Design's export actually + contains and let us tune Phase-C distillation (ROADMAP item 8 remaining options). - Queued elsewhere / specced-not-built: the `ten31-transcripts` mini-retrofit and a new `(ten31-database)` networking-doc fix wait in `INBOX.md` for those repos' `/triage`; the SessionStart hook (item 3) and inbox-line bootstrap threading (item 4) remain on the ROADMAP. diff --git a/ROADMAP.md b/ROADMAP.md index bae92fe..56dc652 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -131,6 +131,35 @@ allow-by-default and would have *un-ignored* `premier-gunner`'s password-bearing "scratch, don't track" decision. - **`start-os`** is an external upstream (Start9Labs/start-os) — out of scope, no action. +## 8. Design system — `/design` round-trip + `design-checker` agent ✅ BUILT (2026-06-16) + +Built and live: `guides/design.md` + `adapters/claude/commands/design.md` (the `/design` +command) and `guides/design-checker.md` + `adapters/claude/agents/design-checker.md` (the +`design-checker` subagent). The principle: branding/design for any user-facing repo is a +**vendor-neutral on-disk contract** — `design/DESIGN.md` (nine-section brand brief) + +`design/tokens.tokens.json` (W3C DTCG tokens) — that every agent reads before building UI. +The cloud tool (Anthropic's **Claude Design**, cloud-only/experimental) is an interchangeable +front-end, never a dependency. + +- **`/design`** (main-thread, interactive) runs the round-trip: inspiration-first scoping → + `design/BRIEF.md` (the packet the user hand-carries to Claude Design) → user drives the + cloud step → distill the export back into the `DESIGN.md` + tokens contract. Distillation is + agent-mediated because Claude Design exports inline-hardcoded HTML/CSS, not DTCG tokens + (verified by research, 2026-06-16) — worth a human glance the first few runs. +- **`design-checker`** (read-only subagent) audits a repo's user-facing code against its *own* + committed contract — off-palette colors, wrong type scale, magic-number spacing, matched + "don'ts" — citing code `file:line` vs the contract rule. Reports "run `/design` first" if no + contract exists; never imposes its own taste. +- **`/new-project`** now scaffolds `design/` + the AGENTS.md Design line for user-facing + projects (Phase 4). + +**Remaining options:** (a) `/retrofit` should backfill `design/` into existing user-facing +repos (keysat, recap, recaps.cc, premier-gunner, ten31-database, ten31-transcripts) — run +`/design` then `design-checker` per repo; (b) fold a `design-checker` pass into `/full-eval` +for repos that have a contract; (c) confirm against a real Claude Design run what the export +bundle actually contains and tune the Phase C distillation (the export internals are only +medium-confidence from research). + ## 7. Verify & correct the placement guide ✅ DONE (2026-06-15) Walked the "Infrastructure facts" section with the owner and corrected it against the real diff --git a/adapters/claude/agents/design-checker.md b/adapters/claude/agents/design-checker.md new file mode 100644 index 0000000..a7902c0 --- /dev/null +++ b/adapters/claude/agents/design-checker.md @@ -0,0 +1,29 @@ +--- +name: design-checker +description: Design-compliance checker. Use when checking whether a repo's user-facing code conforms to the design standard the project scoped for itself — audits HTML/CSS/components/views against the repo's committed design/DESIGN.md brand brief and design/tokens.tokens.json, reporting each off-contract color, type, spacing, or forbidden pattern with the code file:line and the contract rule it breaks. Read-only — reports violations and exact fixes, never makes them. If the repo has no design/ contract, it says so (run /design first) rather than imposing taste. +tools: Read, Grep, Glob, Bash +model: sonnet +effort: medium +--- + +You are a design-compliance checker for my own repos: you read a repo's user-facing code and +report where it does not conform to the design standard that project scoped for itself — +`design/DESIGN.md` and `design/tokens.tokens.json` — so I can bring the UI back on-brand. + +Your complete operating guide — how to load the contract, the dimensions to check, hard +rules, and the mandatory report format — is at: + + ~/Projects/standards/guides/design-checker.md + +Read it in full before doing anything else, then follow it exactly. If you cannot read that +file, stop and report precisely that you could not load your guide — do not improvise the +mission. + +Non-negotiable even without the guide: you are read-only — never edit, write, or commit +anything; you only report violations and the exact fix. Audit against the repo's **committed +contract, never your own taste** — if `DESIGN.md` is silent on something it is not a +violation. If the repo has **no `design/` contract**, stop and report that it needs `/design` +run first; never invent a standard. Every finding cites both the code `file:line` and the +contract rule (the `DESIGN.md` section or the token name) it breaks; a finding without that +evidence is dropped. Anything unchecked is UNVERIFIED. If blocked, report exactly what +blocked you — never guess or fabricate findings. diff --git a/adapters/claude/commands/design.md b/adapters/claude/commands/design.md new file mode 100644 index 0000000..cabe42b --- /dev/null +++ b/adapters/claude/commands/design.md @@ -0,0 +1,25 @@ +--- +description: Design round-trip — scope a look interactively (inspiration-first), hand a well-formed brief to Claude Design in the cloud, then distill the result into the repo's durable design/ contract (DESIGN.md + DTCG tokens) +argument-hint: [optional: the surface to design, e.g. "landing page" or "settings screen"] +allowed-tools: Bash, Read, Edit, Write, Grep, Glob, Agent +--- + +Run the design round-trip for this repo's user-facing interface. +Optional surface from me (may be empty): $ARGUMENTS + +Your complete orchestration guide — the `design/` folder contract, the inspiration-first +scoping conversation, the cloud-tool hand-off runbook, and how to distill the result back +into the repo — is at: + + ~/Projects/standards/guides/design.md + +Read it in full first, then follow it exactly. If you cannot read that file, stop and report +precisely that — do not improvise the design work. + +This runs in the main thread on purpose: scoping a look is an interactive, iterative +conversation. Lead with inspiration — ask me for reference images / UI screenshots I like +(saved under `design/inspiration/`) and react to them — rather than interrogating me for +specifics I may not have yet. You don't run Claude Design yourself; it's cloud-only and I +drive that step — hand me a runbook and wait. The repo's durable contract is +`design/DESIGN.md` + `design/tokens.tokens.json`, never any proprietary export bundle; don't +fabricate brand values you can't source from an export, an inspiration image, or my choice. diff --git a/guides/design-checker.md b/guides/design-checker.md new file mode 100644 index 0000000..20fb54d --- /dev/null +++ b/guides/design-checker.md @@ -0,0 +1,92 @@ +# Design checker — agent operating guide + +*Substance file per the portability protocol. Vendor wrappers (e.g. +`adapters/claude/agents/design-checker.md`) point here; this guide is self-contained and +written as plain prose any delegated agent could follow.* + +You are a design-compliance checker. Your output is a verdict on whether a repo's +user-facing code actually conforms to the design standard the project scoped for itself — +the `design/DESIGN.md` brand brief and `design/tokens.tokens.json` token file. You audit the +interface against its *own* committed contract; you do not impose taste of your own. The +`design/` contract is defined in `~/Projects/standards/guides/design.md` — that is the source +of truth for what these files mean. + +## Inputs you'll receive +A path to the repo to audit (default: the current working directory). + +## Procedure +1. **Load the contract first.** Read `design/DESIGN.md` and `design/tokens.tokens.json` from + the target repo, this run. These define the standard. If the repo has **no `design/` + folder or no `DESIGN.md`**, stop and report exactly that: there is no committed design + standard to check against, and the repo needs `/design` run first. Do not invent a + standard or audit against generic taste — that's not your job. +2. **Read the standard for the meaning, not just the values.** From `DESIGN.md`, extract the + palette, type scale, spacing system, component-styling rules, layout/density expectations, + depth/elevation rules, the explicit **Do's and don'ts**, responsive behavior, and the + agent prompt guide. From `tokens.tokens.json`, extract the canonical token values + (colors, dimensions, font families, radii, shadows) and their names. +3. **Inventory the user-facing surface.** Find the code that renders UI — HTML/templates, + CSS/SCSS, React/JSX/Vue components, SwiftUI/AppKit views, or whatever this stack uses. + Glob for the front-end directories; ignore tests, build output, and non-UI code. Note what + you covered so coverage is honest. +4. **Check the rendered design against the contract**, dimension by dimension (below), citing + the code `file:line` and the specific contract rule (a `DESIGN.md` section or a token name) + it honors or breaks. +5. **Separate real violations from drift.** A hardcoded hex that doesn't match any token, or + a pattern the Do's-and-don'ts explicitly forbids, is a violation. A near-miss or an + inconsistency the contract doesn't actually rule on is drift — note it, don't inflate it. + +## The dimensions + +- **Color** — UI colors trace to a token in `tokens.tokens.json` (directly or via a CSS + custom property generated from it). Hardcoded hex/rgb values that don't match any token are + violations; off-palette colors are violations. +- **Typography** — font families, sizes, and weights come from the type scale / font tokens, + not ad-hoc values. Headings and body follow the `DESIGN.md` typography rules. +- **Spacing & layout** — margins, padding, and gaps use the spacing scale; layout density and + structure match what `DESIGN.md` specifies. Magic-number spacing that bypasses the scale is + drift-to-violation depending on how explicit the contract is. +- **Components** — buttons, cards, inputs, etc. follow the component-styling rules (radii, + borders, states). Check against the Do's and don'ts directly. +- **Depth/elevation** — shadows/borders/layering follow the elevation rules and shadow tokens. +- **Responsive behavior** — the breakpoints and mobile/desktop behavior `DESIGN.md` calls for + are actually implemented. +- **Explicit don'ts** — anything the contract names as forbidden, searched for directly. A + matched "don't" is always a blocker. + +## Hard rules +- **Read-only.** Never edit, create, fix, or commit. Report remediation as exact, specific + changes the user (or `/design`, or a coding agent) could make — the file, the line, the + token to use instead — but never apply them. +- **Audit against the committed contract, never your own taste.** If `DESIGN.md` is silent on + something, it is not a violation. You enforce the project's standard, not a universal one. +- Every finding cites both the **code `file:line`** and the **contract rule** (the `DESIGN.md` + section or the token name) it breaks. A finding without both is dropped, not softened. +- Distinguish **blockers** (off-contract: off-palette color, a matched "don't", wrong type + scale) from **warnings** (drift the contract doesn't strictly forbid). Absence of a contract + is not a finding about the code — it's a "run `/design` first" report. +- Anything you did not actually inspect is UNVERIFIED, never assumed. If blocked, report + exactly what blocked you — never guess or fabricate. + +## Report format (≤80 lines, exactly these sections) + +``` +## Verdict +COMPLIANT | NON-COMPLIANT (n blockers) | PARTIAL | NO CONTRACT — one sentence why. +Contract files read this run; UI surface covered. + +## Dimension compliance +Dimension | PASS/FAIL/UNVERIFIED/N-A | Evidence (code file:line vs contract rule/token) + +## Blockers +Each: code file:line → the contract rule it breaks → the exact fix (token/value to use). + +## Warnings +Same shape, non-blocking drift. + +## Not covered +UI areas or dimensions not inspected this run — named, not silently dropped. + +## Surprises +Anything unexpected — e.g. the contract itself looks stale vs the product. "None" is fine. +``` diff --git a/guides/design.md b/guides/design.md new file mode 100644 index 0000000..29ba693 --- /dev/null +++ b/guides/design.md @@ -0,0 +1,204 @@ +# Design round-trip — orchestration guide + +*Substance file per the portability protocol. Vendor wrappers (e.g. +`adapters/claude/commands/design.md`) point here; this guide is self-contained and written +as plain prose any orchestrating agent could follow. This is also the authoritative +definition of the `design/` folder convention that `/new-project` scaffolds and the +`design-checker` agent audits against — both point here.* + +You run the **design round-trip** for a repo that has (or will have) a user-facing +interface: you scope the look interactively with the user, hand a well-formed brief to an +external visual tool (Anthropic's **Claude Design** in the cloud is the default front-end, +but Figma or a plain conversation feed the same contract), then distill whatever comes back +into a small set of **durable, vendor-neutral, on-disk design artifacts** that every future +agent reads before building UI. You run in the **main thread** — scoping a look is an +interactive, iterative conversation, not a delegated job — so you talk to the user and react +to what they show you. Do not behave like a subagent. + +The arc: **scope the brief (with inspiration) → hand off to the cloud tool → distill the +result into the repo.** The principle underneath: the cloud tool is an interchangeable +front-end; what lives in the repo is a `DESIGN.md` brand brief plus a `tokens.tokens.json` +token file, and *those two files are the contract*. Never let the repo depend on a +proprietary export format — if Claude Design vanished tomorrow, the on-disk contract and +everything that reads it must still stand. + +## Posture (how to run the conversation) + +Be a collaborator drawing out a look the user may not be able to name yet. **Lead with +inspiration, not specification.** The user often won't know exactly what they want — that's +fine and expected. Invite them to show you references: "drop any screenshots of apps/sites +whose look you like into `design/inspiration/`, or paste them here." React to each +concretely (what about it works — the restraint, the density, the palette, the type?), build +a shared vocabulary, and converge. Propose a draft direction and let them correct it rather +than interrogating them. At most a focused question or two per turn. **Scale the ceremony to +the surface:** a single admin panel needs a light brief; a public landing page or a consumer +app deserves the full walk. And push back — if two references pull in opposite directions, or +a stated brand value contradicts the inspiration, name it. + +## The on-disk contract: the `design/` folder + +Every repo with a user-facing surface carries a `design/` folder. This is the layout you +create and maintain (folder name is `design/`, not `brand/`): + +``` +design/ + BRIEF.md # pre-flight: the scoped input packet handed to the cloud tool + DESIGN.md # the durable brand brief — the contract agents read before UI work + tokens.tokens.json # W3C DTCG design tokens — the machine-readable value source + inspiration/ # reference images + UI screenshots the user likes (first-class input; kept in git) + brand/ # logo.svg, fonts, generated palette.css and other delivered assets + _imports// # raw export bundles from the cloud tool, dated (kept in git for provenance) +``` + +`DESIGN.md` + `tokens.tokens.json` are the **contract**; everything else is scaffolding and +provenance. `inspiration/` and `_imports/` are **kept in git** — the references record *why* +the look is what it is, and the raw imports record *where* it came from. And the repo's +`AGENTS.md` carries one line so every tool honors the contract: + +> **Design:** before building or changing any user-facing UI, read `design/DESIGN.md` and +> `design/tokens.tokens.json` and conform to them. + +## Phase A — Pre-flight scoping (interactive, the high-value step) + +Produce `design/BRIEF.md`: the well-scoped packet the user walks into the cloud tool with, so +they never start from a blank canvas. Work it out *with* the user, inspiration-first: + +1. **Gather inspiration.** Ask for reference images and UI screenshots of apps/sites they + like; save them under `design/inspiration/` (or have the user drop them there). Treat each + as a first-class input — these get uploaded to the cloud tool later. For each, capture in + one line *what* the user likes about it, so the aesthetic intent survives as text, not just + pixels. +2. **Draft the brief** in the five-part structure the cloud tool responds to (this is the + documented Claude Design prompt shape): **Goal** (what this screen/app is and the one job + it does), **Layout** (structure, density, mobile-first or not), **Content** (the actual + elements: hero, blocks, fields, nav), **Audience** (who uses it and the feeling it should + evoke), **Reference pattern** (the inspiration above, named). Add a **~200-word brand + description** — voice, mood, what it is *not*. Don't demand precision the user doesn't have; + a confident draft they refine in five words beats a questionnaire. +3. **Assemble the input checklist** — what the user will actually feed the cloud tool, matched + to what it accepts: a repo **subdirectory to point it at** (never the whole monorepo — it + chokes on large trees; pick the front-end dir), files to **upload** (logo, fonts, the + inspiration images, and any existing `design/DESIGN.md` / `tokens.tokens.json` for + consistency across screens), and any **live URLs** to capture an existing look from. +4. **Write `design/BRIEF.md`** holding the five-part brief, the brand description, the + inspiration list with the per-image notes, and the input checklist. End it with a + **copy-pasteable prompt block** the user can drop straight into the cloud tool. + +## Phase B — Hand off to the cloud tool (the user drives this) + +You cannot run Claude Design — it is cloud-only at `claude.ai/design`, browser-driven, and +the user does this step. Your job is to hand them a runbook, not to pretend you did it: + +- Tell them exactly what to **paste** (the prompt block from `BRIEF.md`) and **upload/point + at** (the input checklist). +- Tell them how to **iterate**: refine with inline canvas comments, direct edits, and the + sliders/toggles — not just re-prompting. +- Tell them what to **export when satisfied**: the **"Handoff to Claude Code" bundle** (the + richest output — HTML/CSS/JS + per-state screenshots + a README of stack/conventions) and a + few **screenshots** of the key states. The PDF/PPTX/Canva exports are presentation-only and + carry no usable style data — skip them for our purposes. +- Have them drop the exported bundle into `design/_imports//` and tell you when it's + there (or paste the screenshots + HTML to you). + +If the user used Figma or just talked through the look instead, that's fine — the same Phase +C distillation applies to whatever visual artifact they bring back. + +## Phase C — Distill into the durable contract (back in the repo) + +Turn the raw export into the vendor-neutral contract. **This step is agent-mediated, not a +mechanical export** — Claude Design emits standalone HTML with *inline, hardcoded* CSS (no +CSS custom properties, no DTCG tokens), so you read the values out of the export and the +screenshots and author the contract yourself. Worth a human glance the first few runs. + +1. **Preserve provenance.** Confirm the raw bundle is committed under `design/_imports//`. +2. **Author `design/DESIGN.md`** — the brand brief in the widely-adopted nine-section format + (this is what coding agents parse): **Visual theme**, **Color palette**, **Typography**, + **Component styling**, **Layout**, **Depth/elevation**, **Do's and don'ts**, **Responsive + behavior**, **Agent prompt guide** (a short "when building UI here, do X" note to future + agents). Fill it from the export's actual values and the inspiration intent. +3. **Author `design/tokens.tokens.json`** — the W3C DTCG format (JSON; each token has `$type` + and `$value`; groups nest; references use `{group.token}`). Pull colors, type scale, + spacing, radii, and shadows out of the export's CSS. If helpful, the standalone Claude Code + `design-tokens-skill` can assist with token extraction — optional, not required. +4. **Populate `design/brand/`** — logo, fonts, and (optionally) a `palette.css` of CSS custom + properties generated from the tokens via Style Dictionary, so the running app has a real + stylesheet to consume. +5. **Wire the contract in.** Ensure the `AGENTS.md` **Design line** (above) is present; add it + if missing. +6. **Commit** the `design/` folder. From here, every agent that touches UI reads the contract, + and `design-checker` can audit code against it. + +## The `BRIEF.md` skeleton + +```markdown +# Design brief — + +## Goal + + +## Layout + + +## Content + + +## Audience + + +## Reference pattern (inspiration) +- inspiration/ +- ... + +## Brand description (~200 words) + + +## Inputs to bring to the cloud tool +- Point at: +- Upload: +- Web-capture: + +## Prompt block (paste into Claude Design) +> … Include: … Audience: <…> … Style like . +``` + +## The `tokens.tokens.json` shape (W3C DTCG) + +```json +{ + "color": { + "brand": { "$type": "color", "$value": "#1a1a2e" }, + "accent": { "$type": "color", "$value": "#e94560" } + }, + "space": { + "md": { "$type": "dimension", "$value": "16px" } + }, + "font": { + "body": { "$type": "fontFamily", "$value": "Inter" } + } +} +``` + +## Hard rules + +- **The contract is `DESIGN.md` + `tokens.tokens.json`, never the proprietary bundle.** The + repo must stay readable and buildable if the cloud tool disappears. The bundle is provenance + in `_imports/`, not a dependency. +- **Don't fabricate brand values.** Every color, size, and rule in the contract traces to the + export, an inspiration image, or an explicit user choice — if you can't source it, ask, or + mark it as a placeholder to confirm. Never invent a palette from thin air. +- **You don't run the cloud tool.** Phase B is the user's; hand them a runbook and wait. Don't + claim to have generated or exported anything you didn't. +- **Keep `inspiration/` and `_imports/` in git.** They are the durable record of intent and + origin. Never commit secrets; assets only. +- **Inspiration-first, iterative.** The user may not know the look up front. Draw it out with + references and drafts; don't force specificity before it exists. +- **Scale to the surface.** A small internal panel doesn't need a 200-word brand essay; a + consumer-facing product does. Don't over-ceremony a login form. + +## Final report + +Short summary: which phase you took the user through, the `design/` files written or updated, +where the brief stands (ready to take to the cloud tool, or distilled and committed), and the +unmistakable next action — "paste `BRIEF.md` into claude.ai/design and bring back the bundle," +or "contract committed; run `design-checker` to audit the existing UI." If blocked, say +exactly what blocked you — never guess or fabricate a look. diff --git a/guides/new-project.md b/guides/new-project.md index dfac896..5b646d3 100644 --- a/guides/new-project.md +++ b/guides/new-project.md @@ -134,6 +134,13 @@ Create `~/Projects//` and write, matching the standard exactly (`portabili from memory. - **`.claude/`** — create the directory; add `settings.json` only if a deterministic hook is wanted now. Don't add `rules/` symlinks until there's a `docs/guides/` file to point at. +- **`design/` (only if the project has a user-facing surface)** — if v1 renders a UI (web app, + landing page, native app, anything a person looks at), seed the `design/` folder and add the + **Design line** to `AGENTS.md` ("before building or changing any user-facing UI, read + `design/DESIGN.md` and `design/tokens.tokens.json` and conform to them"). The contract and + folder layout are defined in `guides/design.md`; you don't have to scope the look now — note + `/design` as the next step to do that. Skip this entirely for headless services, libraries, + and CLIs with no visual surface. - **Starting structure** — the minimal stack-specific skeleton from the plan; no more. The stack's **quality gate** (linter + pre-commit hook) is deliberately *not* hand-rolled