Guidance for Claude Code (and any agent CLI) working in this repository to develop ease-design itself.
This is a runtime-neutral config stub. The real per-runtime adapters that teach a host CLI how to use ease-design as a design tool are generated by
ui init(Phase 6) — they are not this file.
ease-design is a multi-runtime design-cli. See README.md for the concept and the
implementation plan (kept in the internal design-engine repo) for the 8-phase plan.
knowledge/— plain-Markdown knowledge core. The host model reads these files directly; never duplicate their content into code.uibinary (src/cli.ts→dist/cli.js) — deterministic, no LLM calls. All non-LLM work (autofix, layout validation, token compilation, color math, registry, export) is aui <subcommand>, callable via Bash and identical on every runtime.
The canonical governing principles live in .specify/memory/constitution.md
(spec-kit). Specs for new work live in specs/NNN-slug/ (committed — any machine
resumes); cross-machine task state lives in GitHub issues (stage:* labels).
The rules below summarize repo practice; the constitution wins on conflict.
- Build / check before committing:
npm run typecheck,npm run lint,npm run build,npm test— all must pass (CI runs the same). - Keep the binary deterministic: pure transforms, no network, no model calls.
- Read selectively — open the
knowledge/file relevant to the task, not all of them. - Locked decisions live in
.specify/memory/constitution.md(ratified from the original brainstorm); do not relitigate them. The pre-spec-kit brainstorm archive is single-machine underplans/and may not exist on every clone.
A merge to main is not done until the README and changelog reflect it. Run this as the
final step of any task that merges — ideally BEFORE the merge commit so it ships in the same push
(if already pushed, backfill in a follow-up commit). README carries two jobs on different clocks:
a slow, taste-sensitive marketing body and a fast, mechanical changelog — never mix them.
- Classify the merge (significance gate — most merges stop cheaply here):
- INTERNAL — refactor/fix with no user- or agent-visible change, no story change → update nothing.
- USER-VISIBLE — observable behavior, flags, or output changed → changelog only.
- STORY-CHANGING — a new capability, a proof-level/claim change, a new demo, or a brand change → changelog and README marketing body.
- Act by class:
- USER-VISIBLE / STORY-CHANGING → append a dated entry to
CHANGELOG.md(newest first, Keep-a-Changelog verbs) and add a top row to the README## Changelogrecent-wave table (Date | Change | Commit). Keep that table to the recent wave; retire old rows toCHANGELOG.md. This path is mechanical (Sonnet-tier). - STORY-CHANGING only → also edit the README marketing body (hero, proof narrative, demos).
This surface is taste-sensitive: Opus edits, Fable audits against
brand/+knowledge/taste-rubric.md. Never let changelog noise leak into the marketing body.
- USER-VISIBLE / STORY-CHANGING → append a dated entry to
- Marketing prose is expensive; the gate exists so only story-changing merges pay for it. When in doubt between USER-VISIBLE and STORY-CHANGING, do the changelog line and ask before touching the hero.
-
A standard needs an emitter AND a linter. Prose-only standards drift — our own compiler violated the paired convention the knowledge already taught (L7). When you write a standard, ship the code that emits it and the check that fails without it, same commit if possible.
-
Every phase budgets one run on real data before it's "done". All eight dogfood findings (L1–L8) came from real projects/files, zero from fixtures. A green suite on a fresh fixture validates the mechanism, not the contract.
-
A missing-rule bug is fixed at the shared layer. Ask "which other consumer has this blind spot" first — the redirect-stub rule patched only into a11y-lint resurfaced in validate-layout (L1→L4). Extract the helper; don't patch where it surfaced.
-
Generated artifacts run the FULL linter set in their own tests. The generated specimen page shipped an unguarded animation because its gate ran 3 of 4 linters (missed taste-lint).
-
A field report is a symptom, not a diagnosis. Triage at the source before you spec. Of the dana dogfood's 11 findings, two were misdiagnosed — F5 said
--tokenswas discarded (it persists astokensUsed; the probe read the wrong key), F10 said the scan "can't see a UI below root" (it can; it spends its 4000-entry budget depth-first and alphabetically, then gives up silently). Worse: we re-ran F5's own probe script and reported "confirmed". A reporter without the source guesses causes; that is not a flaw in the report, it is what a report is. -
If it looks like a hole, first assume it is a decision — then go find the sentence that made it. Nine times in one session a deliberate design read as an accident: the alphabetical walk (
project-scan.ts:6— it is Art I determinism), the mode compiler "dropping"$extensions(figma-ds-tokens.ts:14— "preserved and documented"), the registry's two doors (figma-ds-registry.ts:13— the strict name pattern "exists for code-authored components, not scanned Figma inventory"),slice(0,5)(declared at:24), npm-link (README:132). Every one had a sentence next to the code saying why. Grep-sized reads manufacture false findings. -
Count before you target. A headline number is a hypothesis. In one session: a 20× coverage gap measured 3.5×; "68 missing components" measured 25; ten plausible
SKIP_DIRSadditions measured 0/9 while the one that mattered (.venv) was 8,187 files — 54% of a real repo's tree. Counting the breakpoint scale did not just confirm Tailwind's values, it proved the corpus usesrem— the accessible choice a guess would have missed. The cheapest rule in the repo; it paid three times before a line of code was written. -
A report is not evidence. Re-run the gate yourself, where it actually runs. An executor reported "4 pre-existing failures";
mainwas clean — it had committed before stashing, so the stash was a no-op. And agit checkoutrefused by a worktree silently gates the wrong tree: our own "GATE PASS" was theatre until it ran inside the worktree (the tell was two branches reporting the identical test count). -
A test can enshrine the bug, and a test can assert its own filesystem path.
ds-round-trip.test.tswas named "init → context → registry add → DS_TAMPERED" and asserted the tamper as correct — every green CI run certified that registering a component must break the design system. Separately, an unanchored/F\d+/iover a wrapper that embeds an absolute path passed for months and failed only from a worktree whose hash heldf826. When a gate result surprises you, suspect the assertion before the code. -
A mock must reproduce the STRICT API contract — what throws, what's required — not the happy path. Two spec-005 bugs survived a green suite because the test mock was more lenient than Figma: the sync
.mainComponentgetter throws underdocumentAccess: "dynamic-page"(mock returned a value), andset_namerejects an empty/undefined name (mock accepted it). Both only surfaced on the first live round-trip. When you mock an external API, encode its refusals; a permissive mock is a green light that means nothing. -
A feature branch is a time machine. Anchor every measurement on
main, and fetch first. A branch 59 commits behind reports a world that stopped existing weeks ago. In one session it produced four wrong facts: the package version (0.1.0;mainwas at0.2.0), a deadfigma-agentreference insetup.sh(mainhad cleaned it up), a "complete precedent" that turned out to live in zero commits, and a spec number (022) already taken onmainby a different feature. Read withgit ls-tree main <path>/git show main:<path>/git grep <pat> main -- <path>, never the working tree — and remember localmainitself goes stale:git fetchbefore you branch, or you build on a tip that moved yesterday. -
A zero is a claim. Prove it with a probe that CAN go red.
git grep -E "\bvsf\b"returned nothing and "Ring 0 is clean" went into a design doc; POSIX ERE has no\b, so the pattern matched an empty string.git grep -Pfound 21 hits. Same session, same class, second form: zsh does not word-split an unquoted$FILES, so a rewrite loop silently received one giant argument. Before trusting an empty result, feed the probe an input you know should match. A search that has never returned anything has not been shown to work. -
Baseline before you blame your own change. A full suite came back with 5 red files after a port. Four of them were red on clean
origin/mainbefore the change, and the fifth passed 92/92 in isolation — it only timed out under full-suite parallel load. Cost of finding out: one throwaway worktree atorigin/mainand one isolated re-run.mainis not assumed green; measure it, then diff against the measurement, and say which failures you inherited. -
A repo-wide rewrite needs an explicit file list, not a pattern. A
perl -pirenumberingspec 022 → spec 028for one feature also editedtests/spec-022-lifecycle.ts, which belongs to an unrelated spec that legitimately owns the number. Enumerate the files first, eyeball the list, then rewrite — and re-audit every touched file afterwards, because the damage looks exactly like the intended change. -
"Not merged" is not a finding — squash-merge makes every merged branch look unmerged. A squash lands new content under a NEW commit, so the branch's tip was never an ancestor of
mainandgit merge-base --is-ancestorreports it unmerged forever. Sweeping 15 abandoned worktrees, that heuristic flagged 12 of 12 merged branches as unmerged; grepping commit subjects was no better, because a merge reworded them. Two probes settled it:gh pr list --head <branch> --state allfor the PR's real state, andgit grepfor a distinctive symbol the branch introduced — is the CONTENT onmain, whatever commit carries it. That pair found the one branch in fifteen that genuinely still held work (a universalAGENTS.mdruntime adapter, four files absent frommain) and one uncommitted guard worth recovering. Before deleting a branch, prove its content landed; never infer it from ancestry. -
A GREEN assertion is a claim too. Feed it an input that MUST turn it red. The mirror of "a zero is a claim", and it cost a false green in the same repo: a test asserting the emitted build script contains
-anstayed green after-anwas deleted from the ffmpeg call, because it matched the comment explaining-antwo lines above. The probe caught the command's documentation, not the command. So when the file under assertion holds both instructions and prose — a script with#comments, HTML with<!-- -->, config with doc-strings — strip the commentary before matching and anchor to the line shape (/^\s+-an /m), nevertoContainover the whole file. And break every knob, not a sample: three of the four breaks in that session went correctly red, so stopping at two would have certified a guard that does not guard.
- Every coding task: apply the
/es-lazyladder — reuse codebase → stdlib → native platform → installed dep → one line → only then minimum code. Deliberate corner-cuts carry anes-debt: <ceiling>, <upgrade trigger>comment. - Every feature request: route through
/es-feature-dev(classify → brainstorm → plan → dev → review). Never skip straight to code on non-trivial work. - Use
CONTEXT.mdcanonical terms in all naming (code, plans, commits); never use terms listed under_Avoid_. New terms crystallise via/es-brainstormgrilling. - An es-* skill misled or lacks a rule? Do NOT patch the skill mid-task — record a
gapin the evolution ledger (/es-librarianbranchrecord);/es-librarian rungraduates it with a judge + human merge. - End of ANY substantial task (pipeline or not): Reflect — distil AT MOST ONE durable lesson. Product lesson →
.brv/CONTEXT.md; kit lesson →gap; nothing durable → record NOTHING.